Item34 : int constant대신 enum을 사용하라
JAVA에서는 두 가지 특별한 reference type을 제공하고 있다.
- class의 일종인 enum type
- interface의 일종인 annotation type
기존에는 어떠했나? 정수 열거패턴,
int enum pattern 이라고 알려진 기법을 사용했다.
- named [ int constant ]의 그룹을 선언하여 사용했다. ( JAVA에서는 static final로 선언 )
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
💥 정수열거 패턴의 많은 단점들
- type safety를 제공하지 않는다. 즉, orange를 기대하는 method에다가 apple을 전달 하더라도, 둘 다 같은 int형이기 때문에 compiler에서는 오류를 잡아내지도 못한다
public void orange_process(int ORANGE_VALUE){...}
- 각 constant의 이름에 접두사(ORANGE_,APPLE_)를 붙여줘야 한다.
- JAVA에서는 " int enum group 에게 namespace를 제공하지 않기 " 때문이다. 접두사를 통해서, 두 int enum group(APPLE 그룹, ORAGNE그룹 ) 에 동일한 이름을 가진 constant 가 있는경우 이름의 충돌을 방지 해야 한다.
- int enum constant를 "문자열로 출력 할 방법의 부재" .. print를 하면 그저 정수값이 출력된다 .
- 예를들어 , int enum constant의 name인 "APPLE_FUJI" 라는 문자열로 출력하고 싶어도 그럴 방법이 없다.
- 상수 그룹 내의 상수들을 iterate할 방법 이 없음
- 상수 그룹의 size를 구할 방법이 딱히 없다
기존에는 어떠했나? 문자열 열거패턴
정수 상수 대신, 문자열 상수를 사용하는 방법
- constant를 문자열로 출력하는 것은 가능하다.
- 하지만 이를 위해서 필드네임이 아닌, hard-coded 문자열 상수를 사용해야한다 -> 오타로 인한 버그 가능성 -> 이를 컴파일 타임에 잡지도 못함 -> 런타임 버그 발생
- 성능적으로 BAD ( 문자열 비교(equality) 를 사용하기 때문 )
JAVA에 enum의 도입(Java 5 에 도입)
JAVA에서의 열거타입이 다른언어들에 존재하는 열거타입보다 강력한 이유는 무엇일까?
- java의 Enumtype 은 완전한 Class << --- >> 다른 언어들에서 열거타입은 단순히 정수값
- 열거타입 정의는 컴파일러에 의해 class로 생성된다
Enum의 특징
- - 외부에서 접근할 수 있는 생성자는 제공 하지 않는다
- 상수(const) 하나당 자신의 “instance” 를 하나씩 생성해 둔다 → public static final 필드로 공개
- 그러면서, 외부에서 호출가능한 "생성자"는 없다 ==> "싱글톤 인스턴스". => 매번 같은 인스턴스를 사용하게 되는 것.
public enum Apple { FUJI, PIPPIN};
//는 사실상 대략적으로 아래와 같은 것
public enum Apple{
public static final Apple FUJI;
public static final Apple PIPPIN;
}
- final =⇒ Client가 인스턴스를 직접 생성 X, 확장 X
- 열거타입 선언으로 만들어진 인스턴스들은 “ 딱 하나씩만 ” 존재.
- 인스턴스 통제가 되는 것
- 열거타입은 "싱글톤"이라고도 말 할 수 있고, 싱글톤을 "원소가 하나뿐인 열거타입"이라고 할 수도 있다.
- 컴파일 타임 “타입 안정성”제공
- 각자의 네임스페이스 가짐 → “이름이 같은 상수” 공존 가능
public Enum Applie{ FUJI, PIPPIN};
public Enum Orange{ FUJI, PIPPIN};
- 메소드, 필드를 가질 수 있음
- 🚀Class 이므로 메소드,필드를 가질 수 있음.
- 고차원의 추상개념을 표현해낼 수 있는 것.
문자열 타입 ( 레거시) —> 열거타입파라미터
보기만 해도 끔찍하다
public String checkPrameterBehavior(String object){
if(object.equals("이렇게")){
return "이렇게한다";
}else if(object.equals("문자열을 통해")){
return "문자열통해한다";
}else return "타입쓰고싶다";
}
- 저 String 이 Enum type이 된다면..
- “타입체크도 되고 “,
- “문자열을 작성하는 과정에서 일어나는 오타로 인한 버그도 없어지겠지”
이 부분에 대한 예시는 아래에 이 링크로 가자.
메소드? 필드? → 데이터들의 연관관계 표현이 가능하다
즉, class의 특징( 연관된 데이터들을 모아두고 사용가능 )을 가질 수 있다는 것 이라 생각한다 → 객체지향적 프로그래밍
- 데이터들 간의 연관관계를 표현할 수 있다. → 상수 별로 데이터에 대한 중복이 일어나던 것을 해결
- 상태와 행위를 한 곳에서 처리 ( class의 특징 )
- 각 상수와 연관된 데이터를 상수자체에 두고 싶다 → 인스턴스 필드를 선언
이 때,
- Enum type의 모든 필드 → final 이어야 한다. ( 선언과 동시 or 생성자를 통해 런타임에 초기화)
- 열거타입은 근본적으로 “Immutable” 하기 때문.
- 필드는 public 보다는 private + getter 같은 접근자를 두는 것이 낫다.
(내 생각 ) 아래 코드에서, 표면중력에 대한 것은 사실 인스턴스 필드(surfaceGravity)를 두지 않고, 표면중력을 계산하는 로직 메소드를 따로 두고 그 때 마다 계산할 수도 있으나, 이를 구성하는 값들이 인스턴스필드로 선언되어있어 ( 열거타입에서 인스턴스필드는 final ) → 생성시에만 초기화가 되기에. “최적화”를 위하여 생성시에 미리 표면중력값을 계산해 두고 이것이 필요할 때면 그저 값을 넘기는 식으로 최적화를 할 수 있는 것 같다. ( 👏불필요하게 계산 로직메소드를 따로 두지 말자 )
// Enum type with data and behavior (159-160)
public enum Planet {
MERCURY(3.302e+23, 2.439e6),
VENUS (4.869e+24, 6.052e6),
EARTH (5.975e+24, 6.378e6),
MARS (6.419e+23, 3.393e6),
JUPITER(1.899e+27, 7.149e7),
SATURN (5.685e+26, 6.027e7),
URANUS (8.683e+25, 2.556e7),
NEPTUNE(1.024e+26, 2.477e7);
private final double mass; // In kilograms
private final double radius; // In meters
private final double surfaceGravity; // In m / s^2
// Universal gravitational constant in m^3 / kg s^2
private static final double G = 6.67300E-11;
// Constructor
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
// 표면중력을 미리 계산해 저장하고 있다.
surfaceGravity = G * mass / (radius * radius);
}
enum type “static” method : values(),toString()
열거 타입을 정의하면, 자바 컴파일러가 자동으로 특별한 메소드를 추가며 class를 생성한다. 이 때 추가되는 메소드에 아래의 values() 가 포함. ( toString()은 Enum class가 Object class 를 상속하기 때문에 )
- values() :해당 enum type에 정의된 상수들의 배열을 반환
- toString() : 상수”이름”을 “문자열”로 리턴. ( 당연히 override가능 )
for(Planet planet : Planet.values()){
// Enum타입의 planet 을 print 하면 -> 상수이름이 출력
System.out.printf("%s의 질량은 %f"", **planet**, planet.mass());
}
toString()을 재정의했다면 fromString()이라는 메소드도 고려해보자
- toString() : “열거타입” → String
그렇다면 String → “열거타입” 의 변환을 해 줄 수 있는 메소드도 있으면 좋지 않을까??
- EX) “String” 과 “열거타입 인스턴스”를 매핑해 둔 자료구조(Map<String,MyEnumtype> )를 사용하면 되지 않을까?
> 일단 아래 코드는 에러💥가 난다
public enum GumballState{
private static final Map<String, GumballState> map = new HashMap<>();
GumballState() {
for (GumballState value : values()) {
map.put(value); // 컴파일 에러
}
}
에러를 살펴보자 -> It is illegal to access static member 'map' from enum constructor or instance initializer
왜 이런 에러가 난 걸까?? -> 이는 "초기화 시점" 과 관련되어있다
- enum 인스턴스의 초기화 시점은 언제이길래 그럴까 ?? -> static initializer처럼 class load time 에 초기화 된다.
🚀 따라서 열거타입 “생성자가 실행되는 시점 “에는, “ static 필드들 “ 이 아직 초기화되기 전 이기 때문에, 이들을 참조를 하면 위험하다.
따라서, 이런 제약이 있음
It is a compile-time error to reference a static field of an enum type from constructors, instance initializers, or instance variable initializer expressions of the enum type, unless the field is a constant variable
위와 같은 코드가 컴파일 되지 못하도록 해 놓은 것
출처 : https://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.9.2
그렇다면 위의 map 을 초기화 시키려면 어떻게 해야할까?
static block내에서 Map을 초기화하는 것이 가능하다
public enum GumballState implements EnumString{
static{
for(GumballState state:values()){
map.put(state.toString(),state);
}
}
....
@Override
public Enum fromString(String name) {
return map.get(name);
}
}
이제 이 fromString(String name) 을 통해 리턴되는 열거타입이 실제로 해당 열거타입 인스턴스인지 확인해보자
@Test
public void testEnum(){
// given
GumballState state = GumballState.SOLD;
// when
Enum test = state.fromString("NO_QUARTER");
//then
Assertions.assertEquals(test,GumballState.NO_QUARTER);
}
새로운 상수를 추가 or 상수의 순서를 뒤바꾸더라도 recompile하지 않아도 됨
- 공개 되는건 오직 "필드 이름"
- 정수 열거 패턴과 달리, "상수 값" 이 클라이언트로 컴파일 되어가는게 아니 기 때문
아래는 이 말의 이해를 도와주는 예시로, 정수형 열거패턴을 사용할때와의 차이점을 보여준다.
Enum requires no recompilation of its clients - How?
먼저 경우를 두 가지로 나눠보자.
- int 상수 사용
public class Constants {
public static final int FOO = 1;
}
public class Client {
public static void main(String[] args) {
System.out.println(Constants.FOO);
}
}
이 상태에서 변경
public static final int FOO = 27;
Client는 recompile하지 않고, Constants만 recompile 하고 → Client를 실행.
출력되는 FOO는 여전히 1이다.
- Client class가 compiled될 때, 컴파일러에 의해, Client class로 이 값 자체가 복사되어왔기 때문이다.
- 반면에 열거타입을 사용한다면? 🚀 상수의 “제거가 아닌 수정” 을 한 경우에는, 열거타입의 경우에는, client측에서는 client의 recompiling 없이도 항상 옳바른 값을 얻을 수가 있다.
일부 상수를 제거한다면?
상수 추가 및 변경시에는 기존의 Client를 recompile 시킬 필요가 없다고 했었다. ( 클라이언트 코드에 "실제 값"이 들어가는 것이 아니기 때문에 )
그렇다면, 상수를 제거해도 그럴까?? ->> 아닐 거라는 직감이 올 것 이다.
먼저 제거한 상수 ”를 참조하지 않는 client에는 아무런 영향이 없다
- 하지만 제거한 상수를 참조하는 client라면?
▪ 클라이언트 프로그램 재컴파일 → 해당 상수 참조라인에 컴파일 에러
▪ Recompile을 하지 않고 실행하면 → 런타임에 같은 에러 발생
이 부분에 대해서는 클래스로더.. 링킹 과정의 이해가 필요할 듯 하다. 컴파일과 인터프리팅 과정 자체를 이해 못 하고 있는 듯;; 🤔🤔💥❓
열거타입에 숨기자 private method ( Item 15)
- 다른 클래스에서와 마찬가지로, client에 노출해야할 이유가 없다면
- 열거타입을 선언한 클래스 or 그 패키지에서만 유용한 기능 “private”이나 “package-private 메소드”로 구현
- 해당 범위에서만 사용가능한 기능
톱레벨, 멤버 클래스 열거타입(Item 24)
- 다른 클래스와 마찬가지로, [ 널리 쓰이는 열거 타입 → 톱레벨 클래스로 ]
- 특정 클래스에서만 쓰인다면 , [ 해당 클래스의 member 클래스로 ]
상수마다 동작이 달라야하는 경우 ?
1️⃣ 가장 심플 : switch, if else 구문으로, 상수에 따라 다른 로직을 실행하도록 작성
→ 가독성 매우 떨어짐.
→ 관리하기 위험 💥: 상수가 추가 될 때 마다, 해당 상수에 대한 로직을 추가해야 한다.
- 로직 추가하는 것을 잊고, 해당 상수에 대해 메소드 호출 → 💥 런타임 에러 ( 컴파일 에러를 안 줌.. )
2️⃣ 🚀 열거타입에 추상메소드 선언 → 각 상수에서 constant-specific class body를 선언하자🚀
- 즉, 각 상수에서, override 하는 것임.
- 비어있는 추상메소드로 선언했다면 → 상수 추가 ❗ && but, 추상메소드 재정의를 ❌(까먹음) → 컴파일 에러💥 를 주는 친절함 !!
- 당연히, 상수별 메소드 구현 에는, 상수별 데이터 ( 인스턴스 필드 ) 를 사용할 수 있다.
상수마다 메소드 구현 → “공유되는 로직”은 어떻게?
만약 위와 같이, 추상메소드로 선언해 둔다면 상수 마다 해당 메소드를 구현해 줘야한다.
만약 상수들끼리 공유하는 로직이 존재한다면? -> 중복하여 작성을 해줘야하는 걸까???
1️⃣ (BAD) 가장 심플 : switch,if else 구문 → 위에서 봤지만 문제가 있었음..
enum PayrollDay {
MONDAY, TUESDAY, WEDNESDAY,
THURSDAY, FRIDAY,
SATURDAY, SUNDAY;
private static final int MINS_PER_SHIFT = 8 * 60;
int pay(int minutesWorked, int payRate){
/*
1. 평일 : basePay + 오버타임에 대한 추가수당
2. 주말 : 모든 시간이 오버타임 페이. 시간당 basepay*1.5이므로
basePay 자체는 공통로직으로 두고 💥
1. 평일 -> 추가시간에 대하여 주중 오버타임에 대한 rate로 계산
2. 주말 -> 전체 시간에 대하여 basePay에 0.5를 더 곱해주면 오버타임페이가 된다.
*/
int basePay = minutesWorked * payRate; // 공통로직
// 오버타임 페이 -> 두 가지 종류 1.주중에 오버타임 2. 주말 모든 시간 (basepay*1.5)
int overtimePay;
switch(this){
case SATURDAY: case SUNDAY: // 주말
overtimePay = basePay/2;
beak;
default : // 주중
overtimePay = minutesWorked <= MINS_PER_SHIFT ? 0 :
( minutesWorked - MINS_PER_SHIFT)*payRate/2;
}
return basePay + overtimePay;
}
}
2️⃣ (BAD)상수별 메소드 구현 : 2가지 방법
- 잔업수당 계산코드를 모든 상수에 중복해서 넣기
- 평일 메소드들 사이에 중복 코드, 주말 메소드들 사이에 중복 코드...
- 헬퍼 메소드 작성 → 각 상수의 메소드 구현에서, 필요한 헬퍼메소드를 호출
- 매우 장황해지겠다 →가독성 떨어짐.
- 오류 발생 가능성 증가
3️⃣ (BAD)기본 메소드를 구현해둠 : ex) 평일 잔업수당 계산용 메소드로 overtimePay를 구현해두고, 주말상수들에서는, 해당 메소드를 override
- 문제점 : 💥 주말 수당을 받는 상수 추가시, 메소드 override를 까먹으면 → 평일 잔업수당으로 받게됨..
4️⃣ 🚀 전략 열거 타입 패턴🚀
( 일종의 Composition 처럼 느꼈다 _ 갈아끼울 수 있도록 , 그리고 그 갈아끼우는 애가 해당 기능 관련해서는 책임을 ㅏ하도록 기능을 분리 해 놓는 것 _ 전략패턴과 유사해서 "전략"이라는 이름을 붙여놓은 것 같다. )
상수 추가 시 , 해당 상수의 “잔업수당 🚀전략을 선택”하도록 하는 것 ( 생성자를 통해서 받도록 하면 되겠다 )
그렇다면, 잔업수당 전략은 어떤식으로 ? → private nested enum
- 각 전략에 맞는, 잔업수당 상수에서 , 해당 전략에 맞는 메소드를 구현한다. ( abstract method 를 선언해 놓아 → 강제적으로 오버라이드 하도록 한다 )
enum PayrollDay {
MONDAY(WEEKDAY), TUESDAY(WEEKDAY), WEDNESDAY(WEEKDAY),
THURSDAY(WEEKDAY), FRIDAY(WEEKDAY),
SATURDAY(WEEKEND), SUNDAY(WEEKEND);
private final PayType payType; // 전략 열거 ㅌ아ㅣㅂ
PayrollDay(PayType payType) { this.payType = payType; }
int pay(int minutesWorked, int payRate) {
return payType.pay(minutesWorked, payRate);
}
// The strategy enum type
enum PayType {
WEEKDAY {
int overtimePay(int minsWorked, int payRate) {
return minsWorked <= MINS_PER_SHIFT ? 0 :
(minsWorked - MINS_PER_SHIFT) * payRate / 2;
}
},
WEEKEND {
int overtimePay(int minsWorked, int payRate) {
return minsWorked * payRate / 2;
}
};
// 추가되는 전략이 있다면 -> 이 메소드를 오버라이드 하면 된다.
abstract int overtimePay(int mins, int payRate);
private static final int MINS_PER_SHIFT = 8 * 60;
int pay(int minsWorked, int payRate) {
int basePay = minsWorked * payRate;
return basePay + overtimePay(minsWorked, payRate);
}
}
}
switch문은 아예 쓰지 않는게 좋은가? no → 기존 열거타입의 상수별 동작을 혼합해서 넣고 싶을 때
- 드물게 하나의 상수에 대해서만 다른 동작을 하는 경우에는, switch문을 쓸 수 있겠다.
- 서드파티에서 가져온 Operation 열거타입이 있다고 하자.
각 연산의 반대 연산을 하고 싶다 .
public static Operation inverse(Operation op) {
switch(op) {
case PLUS: return Operation.MINUS;
case MINUS: return Operation.PLUS;
case TIMES: return Operation.DIVIDE;
case DIVIDE: return Operation.TIMES;
default: throw new AssertionError("Unknown op: " + op);
}
}
그래서 열거타입은 언제 쓰는게 좋을까?
- 메모리나 성능적으로 “정수상수”보다 그렇게 뛰어난 건 아님.
- “필요한 원소”를 “컴파일 타임” 에 다 알 수 있는 상수 집합
- ( ❓이부분은잘 모르겠다❓ )정의된 상수 개수가 영원히 고정불변일 필요는 없다. ==> 상수가 추가되어도, binary 수준에서 호환되도록 설계되었다.
- enum 은 컴파일 타임에 정의되어 있어야만 한다.
- 즉 런타임에 수정할 수는 없다.
- “동적” enumeration을 위해서는 차라리 Map<String, Object> 같은 Collection을 쓸 수 밖에..
직접 해보는 리팩토링
헤드퍼스트 디자인 패턴 스테이트 패턴의 첫 번째 예시코드는 다음과 같았다.
package headfirst.state.gumball;
import headfirst.state.gumball.mypractice.GumballState;
public class GumballMachine {
final static int SOLD_OUT = 0;
final static int NO_QUARTER = 1;
final static int HAS_QUARTER = 2;
final static int SOLD = 3;
int state = SOLD_OUT;
int count = 0;
public GumballMachine(int count) {
this.count = count;
if (count > 0) {
state = NO_QUARTER;
}
}
public void insertQuarter() {
if (state == HAS_QUARTER) {
System.out.println("You can't insert another quarter");
} else if (state == NO_QUARTER) {
state = HAS_QUARTER;
System.out.println("You inserted a quarter");
} else if (state == SOLD_OUT) {
System.out.println("You can't insert a quarter, the machine is sold out");
} else if (state == SOLD) {
System.out.println("Please wait, we're already giving you a gumball");
}
}
public void ejectQuarter() {
if (state == HAS_QUARTER) {
System.out.println("Quarter returned");
state = NO_QUARTER;
} else if (state == NO_QUARTER) {
System.out.println("You haven't inserted a quarter");
} else if (state == SOLD) {
System.out.println("Sorry, you already turned the crank");
} else if (state == SOLD_OUT) {
System.out.println("You can't eject, you haven't inserted a quarter yet");
}
}
public void turnCrank() {
if (state == SOLD) {
System.out.println("Turning twice doesn't get you another gumball!");
} else if (state == NO_QUARTER) {
System.out.println("You turned but there's no quarter");
} else if (state == SOLD_OUT) {
System.out.println("You turned, but there are no gumballs");
} else if (state == HAS_QUARTER) {
System.out.println("You turned...");
state = SOLD;
dispense();
}
}
public void dispense() {
if (state == SOLD) {
System.out.println("A gumball comes rolling out the slot");
count = count - 1;
if (count == 0) {
System.out.println("Oops, out of gumballs!");
state = SOLD_OUT;
} else {
state = NO_QUARTER;
}
} else if (state == NO_QUARTER) {
System.out.println("You need to pay first");
} else if (state == SOLD_OUT) {
System.out.println("No gumball dispensed");
} else if (state == HAS_QUARTER) {
System.out.println("No gumball dispensed");
}
}
public void refill(int numGumBalls) {
this.count = numGumBalls;
state = NO_QUARTER;
}
public String toString() {
StringBuffer result = new StringBuffer();
result.append("\\nMighty Gumball, Inc.");
result.append("\\nJava-enabled Standing Gumball Model #2004\\n");
result.append("Inventory: " + count + " gumball");
if (count != 1) {
result.append("s");
}
result.append("\\nMachine is ");
if (state == SOLD_OUT) {
result.append("sold out");
} else if (state == NO_QUARTER) {
result.append("waiting for quarter");
} else if (state == HAS_QUARTER) {
result.append("waiting for turn of crank");
} else if (state == SOLD) {
result.append("delivering a gumball");
}
result.append("\\n");
return result.toString();
}
}
- named int const 를 사용하여 “상태를 관리” 하고 있었다.
물론 책에서는 state 패턴을 사용
지금 생각해보니 → 헤드퍼스트 디자인 패턴에서는, 상태패턴을 사용하면서, “확장가능한 열거패턴” 이라고도 불리는 interface를 사용하는 State 패턴을 사용하여 해결하였다. 사실 이게 더 적절하다.
지금부터 Enum을 사용할 경우, 결국 조건문이 복잡하게 얽힐 가능성에 대해 보게 될 것이다.. 물론 현재 상황에서는 기존 코드보다야 낫지만.. 확장가능성을 생각한다면 State pattern이 더 맞는 듯 하다
하지만 나는 열거타입 연습겸, 열거타입으로 일종의 리팩토링 하는 것을 생각 해 보았다.
- Enum class 를 생성한다고 해 보자 단순히 const를 선언해 놓는 것 외에,
- 상수와 관련된 인스턴스 필드를 추가
- 공통 메소드를 선언 할 수 있겠다
GumballMachine에서는, state에 따라 각 메소드에서 다른 동작을 할 수 있도록 if else 문을 사용하고 있다.
→ 💥 번잡해 보인다
만약, Enum class에 insertQuarter() 같은 것을 추상 메소드를 선언 한다면????
- Enum class의 abstract method로 선언하여, 그저 현재 state.insertQuater() 를 호출하도록 할 수 있다.
- 하지만 .. 코드가 매우 장황해질 것. 하나의 상수에서 구현해야할 메소드가 너무 많음 ( insertQuarter, ejectQuarter, turnTrunk,dispense ..
public enum GumballState{
SOLD{
@Override
public void insertQuater() {
System.out.println("Please wait, we're already giving you a gumball");
},
....
따라서 , Enum 상수에 관련된 데이터로 각 상수에 대한 출력문 값들을 세팅했다
/**
* GumBallMachine -> GumballState 와 MyGumballMachine으로 나타냄
* */
public enum GumballState {
SOLD("delivering a gumball","Please wait, we're already giving you a gumball","Sorry, you already turned the crank","Turning twice doesn't get you another gumball!","A gumball comes rolling out the slot")
,NO_QUARTER("waiting for quarter","You inserted a quarter","You haven't inserted a quarter","You turned but there's no quarter","You need to pay first")
,SOLD_OUT("sold out","You can't insert a quarter, the machine is sold out","You can't eject, you haven't inserted a quarter yet","You turned, but there are no gumballs","No gumball dispensed")
,HAS_QUARTER("waiting for turn of crank","You can't insert another quarter","Quarter returned","You turned...","No gumball dispensed");
/*
default constructor를 주석처리하지 않으면 -> final instance field들에 대하여, might have not been initialized라는 컴파일 에러가 뜬다
GumballState() {
}
*/
GumballState(String state,String insertQuarter, String ejectQuarter, String turnCrank, String dispense) {
this.state = state;
this.insertQuarter = insertQuarter;
this.ejectQuarter = ejectQuarter;
this.turnCrank = turnCrank;
this.dispense = dispense;
}
// 각 상수에 대한 출력문 데이터를 관리
private final String state;
private final String insertQuarter;
private final String ejectQuarter;
private final String dispense;
private final String turnCrank;
public String getInsertQuarter() {
return insertQuarter;
}
public String getEjectQuarter() {
return ejectQuarter;
}
public String getDispense() {
return dispense;
}
public String getTurnCrank() {
return turnCrank;
}
public String getState() {
return state;
}
}
이를 사용하면 Machine의 코드는 아래와 같아진다.
public class MyGumballMachine {
GumballState state = GumballState.SOLD_OUT;
int count = 0;
public MyGumballMachine(int count) {
this.count = count;
if(count > 0){
state = GumballState.NO_QUARTER;
}
}
public void insertQuarter(){
System.out.println(state.getInsertQuarter());
switch (state){
case NO_QUARTER:
state = GumballState.HAS_QUARTER;
}
}
public void ejectQuarter(){
System.out.println(state.getEjectQuarter());
switch (state){
case HAS_QUARTER:
state = GumballState.NO_QUARTER;
}
}
public void turnCrank(){
System.out.println(state.getTurnCrank());
// An enum switch case label must be the unqualified name of an enumeration constant
// class 명 적지 말고 오직 enum 의 값만 적으면 된다
switch (state){
case HAS_QUARTER:
state = GumballState.SOLD;
dispense();
break;
}
}
public void dispense(){
System.out.println(state.getDispense());
switch (state){
case SOLD:
count--;
if(count == 0){
state = GumballState.SOLD_OUT;
}else{
state = GumballState.NO_QUARTER;
}
}
}
@Override
public String toString(){
StringBuffer bf = new StringBuffer();
bf.append("\\nMighty Gumball, Inc.");
bf.append("\\nJava-enabled Standing Gumball Model #2004\\n");
bf.append("Inventory: " + count + " gumball");
if (count != 1) {
bf.append("s");
}
bf.append("\\nMachine is ");
bf.append(state.getState());
bf.append("\\n");
return bf.toString();
}
}
달라진점
- 수 많은 if else구문을 줄이고 → 하나의 condition에 대한 switch구문으로 변경하였다.
- 하지만 문제는 이거다 → Enum을 사용하여 상태를 관리할 경우 → State의 개수가 늘어나면 위의 switch 구문과 같은 조건문들이 더욱 복잡해질 것이다.
위의 코드는 헤드퍼스트 디자인 패턴의 “스테이트 패턴” 단원에서 나온 부분이다. 현 상황에서는 Enum을 사용하는게 적절해 보이진 않는다.
- enum 에서 조건문을 이용한 방식은 코드를 복잡하게 만들어 유지 보수를 어렵게 할 가능성이 있다..
참조
effective-java-3e-source-code/Inverse.java at master · noogu/effective-java-3e-source-code
https://github.com/bethrobson/Head-First-Design-Patterns
'책 > effectiveJava' 카테고리의 다른 글
[Item 73] 추상화 수준에 맞는 예외를 던져라 (0) | 2022.12.31 |
---|---|
Item 53 : 가변인수는 신중히 사용하라 (0) | 2022.08.24 |