CS/디자인 패턴
State Pattern
Ynghan
2023. 12. 12. 15:03
목차
- 상태 패턴의 발견
- 상태 패턴의 구조와 실행시간 동작원리
- 전략 패턴과의 유사성
State Pattern
- 목적
- 객체의 상황을 동작과 연결하여 개체가 내부 상태에 따라 다양한 방식으로 동작할 수 있도록 한다.
- 언제 사용하나요?
- 객체의 동작이 상태에 영향을 받아야 한다.
- 복잡한 상태를 객체의 상태에 따라 동작과 연결한다.
- 상태 사이의 전환은 명시적이어야 한다.
상태 & 동작 구현
상태 Coding
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;
Code 쓰기
public class GumbleMachine {
final static int SOLD_OUT = 0;
final static int NO_QUARTER = 1;
final static int HAS_QUARTER = 2;
final static int SOLD = 3;
//현재 상태를 나타내는 변수 state
int state = SOLD_OUT;
//껌의 개수를 나타내는 변수 count
int count = 0;
public GumbleMachineStart(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 insert a quarter");
} else if ...
}
public void ejectQuarter() {
if(state == HAS_QUARTER) {
System.out.println("Quarter returned");
} else if (state == NO_QUARTER) {
System.out.println("You haven't inserted a quarter");
} else if (state == SOLD) {
...
}
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) {
...
}
public void dispense() {
count -= 1;
if(count > 0) {
state = NO_QUARTER;
} else {
state = SOLD_OUT;
}
}
}
이 코드는 상태 기계를 모델링하는 고전적인 방법을 사용하고 있다.
각 상태에 따른 동작을 메소드 내에서 분기 처리하고 있다( 각각의 메소드가 state의 값에 따른 모든 경우에 대한 조건 처리를 해주고 있다).
이 경우의 문제점은 다음과 같다.
1. 확장성 : 새로운 상태가 추가될 경우, 각 메소드에 새로운 조건을 추가해야 한다. 이는 코드의 유지 보수를 어렵게 만든다.
2. 가독성 : 각 메소드가 여러 상태를 처리하기 때문에, 코드의 가독성이 저하된다. 한 메소드 내에서 여러 가지 동작을 처리하므로, 코드를 이해하고 디버깅하는 데 시간이 오래 걸린다.
3. 코드 중복 : 상태마다 동일한 로직을 반복해서 작성해야 할 수 있다. 이로 인해 코드 중복이 발생하고, 이는 버그를 유발할 수 있다.
4. 상태 불일치 : dispense 메소드에서는 상태를 변경하고 있지만, 다른 메소드에서는 상태 변경이 없다. 이로 인해 상태 불일치가 발생할 수 있다.
이러한 문제를 해결하기 위해 "State Pattern"을 고려해보자. 상태 패턴은 객체의 내부 상태가 변경될 때마다 객체의 행동을 변경하는 디자인 패턴이다. 이를 통해 객체의 상태(인터페이스 구현 클래스)를 객체의 행동(메소드)과 연결시키고, 상태 변화에 따른 행동 변화를 클래스로 캡슐화함으로써 코드를 더 깔끔하게 만들 수 있다.
새로운 프로모션과 새로운 요구사항
- 위너를 만들자!
- 10개의 검볼 중 1개의 검볼을 무료로 한다.
새로운 아이디어
- 변하는 것을 캡슐화하라!
- 자신의 클래스에 각 상태의 동작을 넣어라. 그때, 모든 상태는 단지 자신의 동작을 구현한다.
- 검볼 기계는 현재 상태를 나타내는 상태 객체에 위임할 수 있다.
- 상속(Inheritance)보다 합성(Composition)을 선호한다.
상속과 합성은 객체 지향 프로그래밍에서 가장 중요한 개념 중 두 가지입니다. 각각의 특징과 장단점을 비교해 보겠습니다.
상속(Inheritance):
- 클래스 간의 관계를 정의하는 방법 중 하나로, 기반 클래스의 속성과 메소드를 파생 클래스가 물려받는 구조입니다.
- 장점 : 코드 재사용을 통해 중복을 줄이고, 코드의 일관성을 유지할 수 있다. 또한, 다형성을 통해 코드의 유연성을 증가시킬 수 있다.
- 단점 : 클래스 간의 강한 결합이 발생하여, 기반 클래스의 변경이 파생 클래스에 영향을 미친다. 또한, 상속 계층이 복잡해질수록 코드의 복잡성이 증가한다.
합성(Composition):
- 합성은 한 클래스가 다른 클래스의 인스턴스를 포함하는 방법입니다. 이를 통해 클래스 간의 관계를 구축하고, 필요한 기능을 위임받아 사용할 수 있습니다.
- 장점 : 느슨한 결합을 통해 클래스 간의 독립성을 유지할 수 있다. 또한, 런타임에 동적으로 행동을 변경할 수 있으며, 모듈화를 통해 코드의 재사용성을 높일 수 있다.
- 단점 : 설계가 복잡해질 수 있으며, 너무 많은 위임으로 인해 코드의 흐름을 따라가기 어려울 수 있다.
따라서, 상황에 따라 적절한 방법을 선택하는 것이 중요하다. 일반적으로 "is-a" 관계를 나타낼 때 상속을, "has-a" 관계를 나타낼 때 합성을 사용한다. 그러나 상속이 과도하게 사용되면 복잡성과 유지보수의 어려움을 초래할 수 있으므로, 합성을 통해 더 유연하고 재사용 가능한 설계를 고려하는 것이 좋다.
새로운 설계
- 검볼 머신의 모든 동작에 대한 메소드를 포함하는 상태 인터페이스를 정의한다.
- 머신의 모든 상태에 대한 상태 클래스를 구현한다.
- 해당 상태에 있을 때, 기계의 동작을 담당한다.
- 모든 조건부 코드를 제거하고 대신 작업을 수행하도록 상태 개체에 위임한다.
State 인터페이스와 클래스 정의하기
- 검볼 머신의 모든 상태에 대한 상태 클래스 구현
상태 클래스들 구현하기
Gumball Machine 수정
public class GumbleMachine {
// final static int SOLD_OUT = 0;
// final static int NO_QUARTER = 1;
// final static int HAS_QUARTER = 2;
// final static int SOLD = 3;
State soldOutState;
State noQuarterState;
State hasQuarterState;
State soldState;
// int state = SOLD_OUT;
// int count = 0;
State state = soldOutState;
int count = 0;
public GumbleMachineStart(int numberGumballs) {
soldOutState = new SoldOutState(this);
noQuarterState = new NoQuarterState(this);
hasQuarterState = new HasQuarterState(this);
soldState = new SoldState(this);
this.count = numberGumballs;
if (numberGumballs > 0) {
state = noQuarterState;
}
}
// 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 insert a quarter");
// } else if ...
// }
public void insertQuarter() {
state.insertQuarter();
}
// public void ejectQuarter() {
// if (state == HAS_QUARTER) {
// System.out.println("Quarter returned");
// } else if (state == NO_QUARTER) {
// System.out.println("You haven't inserted a quarter");
// } else if (state == SOLD) {
// ...
// }
// }
public void ejectQuarter() {
state.ejectQuarter();
}
// 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) {
// ...
// }
// }
// public void dispense() {
// count -= 1;
// if(count > 0) {
// state = NO_QUARTER;
// } else {
// state = SOLD_OUT;
// }
// }
void releaseBall() {
System.out.println("A gumball comes rolling out the slot...");
if (count != 0)
count = count - 1;
}
void setState(State state) {
this.state = state;
}
}
상태 패턴
- 상태 패턴은 내부 상태가 변경될 때, 객체가 동작을 변경할 수 있다.
- 객체의 클래스가 변경된다.
상태 패턴의 적용 가능성
- 사용하는 상황
- 객체의 동작은 해당 상태에 따라 달라지며, 해당 상태에 따라 런타임 시 동작을 변경해야 한다.
- 동작에는 객체의 상태에 따라 달라지는 크고 여러 부분으로 구성된 조건문이 있다.
- 상태 패턴은 조건문의 각 분기를 별도의 클래스에 배치한다.
클래스 내부에 객체의 인터페이스를 가져와 클래스 안에서 조건문에 따라 인터페이스 구현 객체를 변경한다.
상태 패턴의 결과
- 이점
- 상태와 관련된 모든 동작을 하나의 객체에 넣습니다.(여기서는 GumballMachine.class)
- 상태 전환 로직을 거대한 if나 switch 문장에 통합하는 대신, 상태 객체에 포함시키도록 허용한다.
- 여러 객체나 속성이 아닌 하나의 상태 객체(State)만 사용하여 상태 변경이 발생하므로, 일관성 없는 상태를 방지하는 데 도움이 됩니다.
- 부채(단점)
- 객체 수 증가
State 패턴 vs Strategy 패턴
- State 패턴과 Strategy 패턴 사이에 공통점에 주목하자.
- 강도의 차이다.
- State 객체는 상태에 의존하는 동작을 캡슐화한다. ( + 상태 변환 )
- 컨텍스트의 동작은 시간이 지남에 따라 변경될 때
- 컨텍스트에 많은 조건을 넣어야 할 때,
- Strategy 객체는 알고리즘을 캡슐화한다.
- 종종 context 객체에 가장 적합하다.
- 서브 클래싱에 대한 유연한 대안이 된다.
- 둘다 위임을 통한 구성의 예시이다.
구현 문제
- 상태 변환은 누가 정의하나요?
- 선택 1 : context 클래스 - 간단한 상황
- 선택 2 : 구체 상태 클래스 - 일반적으로 더 유연하다. 그러나 구체 상태 클래스 사이에 구현 종속성을 야기한다.
- 언제 구체 상태 객체를 만드나요?
- 선택 1 : 필요에 따라 구체 상태 객체를 생성한다.
- 선택 2 : 모든 구체 상태 객체를 한번 생성하고 context 객체가 예제에 대한 참조를 유지하도록 한다.
Gumball 10개 중 하나 게임 끝내기