flagtags / OOP-study

0 stars 0 forks source link

10장. 상태패턴 #27

Open j03y14 opened 5 months ago

j03y14 commented 5 months ago

image

이렇게 작동하는 뽑기 기계 프로그램을 만들고 싶다.

class GumbalMachine {
    static SOLD_OUT = 0;
    static NO_QUARTER = 1;
    static HAS_QUARTER = 2;
    static SOLD = 3;

    state = GumbalMachine.SOLD_OUT;
    count = 0;

    constructor(count: number) {
        this.count = count;
        if (count > 0) {
            this.state = GumbalMachine.NO_QUARTER;
        }
    }

    insertQuarter() {
        if (this.state === GumbalMachine.HAS_QUARTER) {
            console.log('동전은 한 개만 넣어주세요');
        } else if (this.state === GumbalMachine.NO_QUARTER) {
            this.state = GumbalMachine.HAS_QUARTER;
            console.log('동전을 넣으셨습니다');
        } else if (this.state === GumbalMachine.SOLD_OUT) {
            console.log('매진되었습니다. 다음 기회에 이용해주세요');
        } else if (this.state === GumbalMachine.SOLD) {
            console.log('잠시만 기다려주세요. 알맹이가 나가고 있습니다');
        }
    }

    ejectQuarter() {
        if (this.state === GumbalMachine.HAS_QUARTER) {
            console.log('동전이 반환됩니다');
            this.state = GumbalMachine.NO_QUARTER;
        } else if (this.state === GumbalMachine.NO_QUARTER) {
            console.log('동전을 넣어주세요');
        } else if (this.state === GumbalMachine.SOLD) {
            console.log('이미 알맹이를 뽑으셨습니다');
        } else if (this.state === GumbalMachine.SOLD_OUT) {
            console.log('동전을 넣지 않으셨습니다. 동전이 반환되지 않습니다');
        }
    }

    turnCrank() {
        if (this.state === GumbalMachine.SOLD) {
            console.log('손잡이는 한 번만 돌려주세요');
        } else if (this.state === GumbalMachine.NO_QUARTER) {
            console.log('동전을 넣어주세요');
        } else if (this.state === GumbalMachine.SOLD_OUT) {
            console.log('매진되었습니다');
        } else if (this.state === GumbalMachine.HAS_QUARTER) {
            console.log('손잡이를 돌리셨습니다');
            this.state = GumbalMachine.SOLD;
            this.dispense();
        }
    }

    dispense() {
        if (this.state === GumbalMachine.SOLD) {
            console.log('알맹이가 나가고 있습니다');
            this.count--;
            if (this.count === 0) {
                console.log('더 이상 알맹이가 없습니다');
                this.state = GumbalMachine.SOLD_OUT;
            } else {
                this.state = GumbalMachine.NO_QUARTER;
            }
        } else if (this.state === GumbalMachine.NO_QUARTER) {
            console.log('동전을 넣어주세요');
        } else if (this.state === GumbalMachine.SOLD_OUT) {
            console.log('매진입니다');
        } else if (this.state === GumbalMachine.HAS_QUARTER) {
            console.log('알맹이가 나갈 수 없습니다');
        }
    }
}

10분의 1의 확률로 뽑기가 2개 나오도록 하고싶다. 당첨상태 추가 필요.

당첨 상태가 추가되면 모든 메서드에 당첨 상태를 추가해야한다.

이렇게 되면 생기는 문제점:

바뀌는 부분을 캡슐화 하려면?

상태별 행동을 별도의 클래스에 넣어두고 모든 상태에서 각각 자기가 할 일을 구현하도록 한다. 그리고, 뽑기 기계가 현재 상태를 나타내는 상태에게 작업을 넘기게 한다.

// 10분의 1의 확률로 뽑기가 2개 나오도록 하고싶다.
// 당첨 상태 추가.

// 상태별 행동을 별도의 클래스에 넣어두고 모든 상태에서 각각 자기가 할 일을 구현하도록 한다.
// 그리고, 뽑기 기계가 현재 상태를 나타내는 상태에게 작업을 넘기게 한다.

// 뽑기 기계와 관련된 모든 행동 메서드가 있는 State 인터페이스를 만든다.
// State 인터페이스를 구현하는 클래스를 만든다.
// 조건문 코드를 없애고 상태 객체에게 작업을 위임한다.

abstract class State {
    abstract insertQuarter(): void;
    abstract ejectQuarter(): void;
    abstract turnCrank(): void;
    abstract dispense(): void;
}

class SoldOutState extends State {
    constructor(private gumballMachine: GumbalMachine) {
        super();
    }

    insertQuarter() {
        console.log('매진되었습니다');
    }

    ejectQuarter() {
        console.log('동전을 넣지 않으셨습니다. 동전이 반환되지 않습니다');
    }

    turnCrank() {
        console.log('매진되었습니다');
    }

    dispense() {
        console.log('매진되었습니다');
    }
}

class NoQuarterState extends State {
    constructor(private gumballMachine: GumbalMachine) {
        super();
    }

    insertQuarter() {
        console.log('동전을 넣으셨습니다');
        this.gumballMachine.setState(this.gumballMachine.getHasQuarterState());
    }

    ejectQuarter() {
        console.log('동전을 넣어주세요');
    }

    turnCrank() {
        console.log('동전을 넣어주세요');
    }

    dispense() {
        console.log('동전을 넣어주세요');
    }
}

class HasQuarterState extends State {
    constructor(private gumballMachine: GumbalMachine) {
        super();
    }

    insertQuarter() {
        console.log('동전은 한 개만 넣어주세요');
    }

    ejectQuarter() {
        console.log('동전이 반환됩니다');
        this.gumballMachine.setState(this.gumballMachine.getNoQuarterState());
    }

    turnCrank() {
        console.log('손잡이를 돌리셨습니다');
        this.gumballMachine.setState(this.gumballMachine.getSoldState());
    }

    dispense() {
        console.log('동전을 넣어주세요');
    }
}

class SoldState extends State {
    constructor(private gumballMachine: GumbalMachine) {
        super();
    }

    insertQuarter() {
        console.log('잠시만 기다려주세요. 알맹이가 나가고 있습니다');
    }

    ejectQuarter() {
        console.log('이미 알맹이를 뽑으셨습니다');
    }

    turnCrank() {
        console.log('손잡이는 한 번만 돌려주세요');
    }

    dispense() {
        this.gumballMachine.releaseBall();
        if (this.gumballMachine.getCount() > 0) {
            this.gumballMachine.setState(this.gumballMachine.getNoQuarterState());
        } else {
            console.log('더 이상 알맹이가 없습니다');
            this.gumballMachine.setState(this.gumballMachine.getSoldOutState());
        }
    }
}

// State 인터페이스에서 모든 행동을 구현해야 한다.
// 그런데 GumbalMachine에서도 모든 행동을 구현해야 하는데 State를 상속하지 않았을까?

// dispense 메서드는 상태에는 필요하지만 사용자가 기계에 뽑기를 내놓으라고 할 수는 없다.
// 그래서 상속을 안 한 것 같다.
class GumbalMachine {
    private soldOutState: State;
    private noQuarterState: State;
    private hasQuarterState: State;
    private soldState: State;

    private state: State;

    private count = 0;

    constructor(count: number) {
        this.count = count;
        this.soldOutState = new SoldOutState(this);
        this.noQuarterState = new NoQuarterState(this);
        this.hasQuarterState = new HasQuarterState(this);
        this.soldState = new SoldState(this);

        if (count > 0) {
            this.state = this.noQuarterState;
        } else {
            this.state = this.soldOutState;
        }
    }

    insertQuarter() {
        this.state.insertQuarter();
    }

    ejectQuarter() {
        this.state.ejectQuarter();
    }

    trunCrank() {
        this.state.turnCrank();
        this.state.dispense();
    }

    setState(state: State) {
        this.state = state;
    }

    getSoldOutState() {
        return this.soldOutState;
    }

    getNoQuarterState() {
        return this.noQuarterState;
    }

    getHasQuarterState() {
        return this.hasQuarterState;
    }

    getSoldState() {
        return this.soldState;
    }

    releaseBall() {
        console.log('알맹이가 나가고 있습니다');
        this.count--;
    }

    getCount() {
        return this.count;
    }

}

상태 패턴

상태패턴을 사용하면 객체의 내부 상태가 바뀜에 따라서 객체의 행동을 바꿀 수 있다.

상태를 별도의 클래스로 캡슐화 한 다음 현재 상태를 나타내는 객체에게 행동을 위임한다.

Q&A

  1. 다음 상태로의 상태 전환을 Context에서 해도 되나?
    • 상태 전환이 고정되어 있으면 상태 전환 흐름을 결정하는 코드를 context에 넣어도 된다.
    • 하지만 상태 전환이 동적으로 결정된다면 상태 클래스에서 결정하는 것이 좋다.

정상성 점검하기

논의해볼만한 점

kkirico commented 5 months ago

알뽑기 예시

동전 없음 → (동전넣기) → 동전 있음 → (동전반환) → 동전 없음

동전 없음 → (동전넣기) → 동전 있음 → (손잡이 돌림) → 알맹이 반환 → 알맹이 = 0 → 알맹이 없음

4가지 상태와 4가지 행동이 정의되었다 + 전환의 종류는 5가지

// 상태 정의
const SOLD_OUT = 0;
const NO_QUARTER = 1;
const HAS_QUARTER = 2;
const SOLD = 3;

const state = SOLD_OUT;

상태 기계 역할 클래스

class insertQuarter() {
    if (state === HAS-QUARTER){
        console.log('동전은 한개만 넣어주세요');   
    } else if(state === NO_QUARTER){
        state = HAS_QUARTER;
        console.log('동전이 투입되었습니다');
    } else if(state === SOLD_OUT){
        console.log('매진되었습니다.');
    } else if(state === SOLD){
        console.log('알맹이를 내보냅니다');
    }
}
class GumBallMachine(){
    SOLD_OUT = 0;
    NO_QUARTER = 1;
    HAS_QUARTER = 2;
    SOLD = 3;

    state = SOLD_OUT;
    count = 0;

    constructor(count){
        this.count = count;
        if(count > 0){
            state = NO_QUARTER;
        }
    }

    insertQuarter() {
        if (state === HAS-QUARTER){
            console.log('동전은 한개만 넣어주세요');   
        } else if(state === NO_QUARTER){
            state = HAS_QUARTER;
            console.log('동전이 투입되었습니다');
        } else if(state === SOLD_OUT){
            console.log('매진되었습니다.');
        } else if(state === SOLD){
            console.log('알맹이를 내보냅니다');
        }
    }

    ejectQuarter(){
        //...
    }
    turnCrank(){
        //...
    }
    dispense(){
        //...
    }
}

const gumballMachine = new GumballMachine(5);

gumballMachine.insertQuarter();
gumballMachine.turnCrank();

gumballMachine.insertQuarter();
gumballMachine.ejectQuarter();

10분의 1 확률로 하나 더 드립니다

너무 많은 메소드가 변경됨

바뀌는 부분이 캡슐화 되지 않았습니다

상태 전환이 조건문 속에 숨어있어서, 분명하게 드러나지 않습니다.

계획 변경

상태 객체들을 별도의 코드에 넣고, 어떤 행동이 일어날 때 상태 객체에게 필요한 작업을 처리하게 한다.

각 상태별로 행동을 정의해서 클래스에 넣는다

interface State{
    insertQuarter();
    ejectQuarter();
    turnCrank();
    dispense();
}

class SoldState: State(){
    //...
}

class SoldOutState extends State
class NoQuarterState extends State() {
    gumballMachine

    constructor(gumballMachine: GumballMachine) {
        this.humballMachine = gumballMachine;
    }

    // state update here
    insertQuarter() {
        gumballMachine.setState(gumballMachine.getHasQuarterState());
    }

    ejectQuarter(){
        console.log('');
    }
    turnCrank(){
        console.log('');
    }
    dispense(){
        console.log('');
    }
}

class HasQuarterState
// new state
class WinnerState

class gumballMachine(){
    soldOutState: State;
    noQuarterState: State;
    hasQuarterState: State;
    soldState: State;

    state: State
    gumballs: number;

    constructor(gumballs: number){
        soldOutState = new SoldoutState(this);
        noQuarterState = new NoQuarterState(this);
        hasQuarterState = new HasQuarterState(this);
        soldState = new SoldState(this);

        this.count = gumballs;

        if(gumballs > 0){
            state = noQuarterState;
        } else {
            state = soldOutState;
        }
    }

    insertQuarter() {
        state.insertQuarter();
    }
    ejectQurater() {
        state.ejectQuarter();
    }
    // ...

    setState(state) {
        this.state = state;
    }
}

정의

객체 내부 상태가 바뀜에 따라서 객체의 행동을 바꿀 수 있습니다.

Context객체(gumballMachine) 을 사용합니다.

Context 객체에 수많은 조건문을 넣지 않고 상태 패턴을 쓴다.

이를 통해 상태에 따라 서로 다른 행동을 실행하도록 한다.

비교

상태 패턴은 상태 객체에 행동이 캡슐화 됩니다.

전략 패턴은 클라이언트가 Context 객체에게 어떤 전략을 사용할지 지정합니다.

당첨 기능 추가

새로운 state 정의하고, 기존 state에서 해당 state로 변경되는 경우를 찾아 수정해주면 됩니다.

SoldState와 WinnerState를 구분하는 이유는 단일 책임 원칙을 위해서 이다.