정의 : 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 가짐
이 패턴을 공부할 때 처음부터 스타그래프트라는 게임이 생각이 났다.
상태를 주로 관리하는 한 클래스가 의존하고 있는 클레스의 상태값을 한번에 관리해 주는것!
스타에서 우리가 건물에게 명령을 내리는 방식과 비슷하다
중앙에서 생산하라고 명령을 내리면
배럭이랑 팩토리에서 각자 생산할 수 있는것을
생산할 수 있는 만큼 생산한다.
게임상에서 배럭이랑 팩토리는 최대 5개까지만 생산을 할 수 있다.
아무리 커멘드가 탱크를 많이 생산하라고 해도 5개만 생산할 수 있다.
자세하게 코드를 보자
Subject 인터페이스로 registerObserver(건물이 지어졌을 때), removeObserver(건물이 파괴되었을 때), notifyObservers(명령)
Observer 인터페이스는 값을 변경하는 update
(지금 예시에서는 마린, 메딕, 탱크를 생산하는 예시를 구현하려고 한다)
MakeUnit은 건물이 자신이 할 수 있는 동작을 만드는 인터페이스 이다
interface Subject {
registerObserver(o: Observer): void;
removeObserver(o: Observer): void;
notifyObservers(): void;
}
interface Observer {
update({
marin,
medic,
tank,
}: {
marin: number;
medic: number;
tank: number;
}): void;
}
interface MakeUnit {
make(): void;
}
Subject의 구체 클레스를 만들자 이름은 commandCenter
class CommandCenter implements Subject {
private units: Observer[] = [];
private marin = 0;
private medic = 0;
private tank = 0;
registerObserver(o: Observer): void {
this.units.push(o);
}
removeObserver(o: Observer): void {
this.units = this.units.filter((unit) => unit !== o);
}
notifyObservers(): void {
this.units.forEach((unit) => {
unit.update({
marin: this.marin,
medic: this.medic,
tank: this.tank,
});
});
}
private commandChange() {
this.notifyObservers();
}
reset(){
this.marin = 0;
this.medic = 0;
this.tank = 0;
this.commandChange();
}
command({
marin,
medic,
tank,
}: {
marin: number;
medic: number;
tank: number;
}) {
this.marin = marin;
this.medic = medic;
this.tank = tank;
this.commandChange();
}
}
명령은 한번에 명령을 받는 주체들에게 전달!
명령을 받는 주체들의 구체 클레스 구현
자신이 수용할 수 있는 값들만 받아서 생산한다.
Barrack과 Factory는 자신의 생산능력이 5개뿐이여서 5개 이상 명령이 들어와도 5개만 생산!
terranStore는 react의 상태관리를 위해서 zustand로 구현한 훅으로 뒤에서 알아보자
export class Barrack implements Observer, MakeUnit {
private marin = 0;
private medic = 0;
constructor(
private readonly commandCenter: CommandCenter,
private readonly terranStore: useTerranStoreType
) {
this.commandCenter.registerObserver(this);
}
update({
marin,
medic,
tank,
}: {
marin: number;
medic: number;
tank: number;
}): void {
this.marin = marin;
this.medic = medic;
this.make();
}
make(): void {
this.terranStore().setMarin(this.marin > 5 ? 5 : this.marin);
this.terranStore().setMedic(this.medic > 5 ? 5 : this.medic);
}
}
class Factory implements Observer, MakeUnit {
private tank = 0;
constructor(
private readonly commandCenter: CommandCenter,
private readonly terranStore: useTerranStoreType
) {
this.commandCenter.registerObserver(this);
}
update({
marin,
medic,
tank,
}: {
marin: number;
medic: number;
tank: number;
}): void {
this.tank = tank;
this.make();
}
make(): void {
this.terranStore().setTank(this.tank > 5 ? 5 : this.tank);
}
}
이제 이렇게 만든 클레스를 react로 가져와 보자
예시를 복잡하게 만들지 않기 위해서
이미 건물을 짓거나 파괴하는 로직을 구현하지 않았다.
대신 상태가 바뀌는 것에 집중해보자
zustand는 getState라는 함수를 제공해서 상태를 react가 아닌 곳에서도 상태값을 쉽게 바꿀 수 있게 해준다.
그점을 이용해서 zustand로 리엑트 상태값과 setter를 만들어 준다
import { create } from "zustand";
import { combine } from "zustand/middleware";
interface IInitalState {
marin: number;
medic: number;
tank: number;
}
const initalState: IInitalState = {
marin: 0,
medic: 0,
tank: 0,
};
export const useTerranStore = create(
combine(initalState, (set, get) => ({
setMarin: (num: number) => {
set((state: IInitalState) => ({ marin: num }));
},
setMedic: (num: number) => {
set((state: IInitalState) => ({ medic: num }));
},
setTank: (num: number) => {
set((state: IInitalState) => ({ tank: num }));
},
}))
);
export type useTerranStoreType = typeof useTerranStore.getState
useTerranStoreType을 통해 컴포지션!(위의 옵져버들의 구체 클래스들을 다시 보자!)
이제 리액트 컴포넌트들을 만들어보자
import React from "react";
import "./App.css";
import { useTerranStore } from "./hooks/useTerranStore";
import { CommandCenter } from "./Observer/CommandCenter";
import { Barrack } from "./Observer/Barrack";
import { Factory } from "./Observer/Factory";
import Command from "./Command";
import BarrackComponent from "./Barrack";
import FactoryComponent from "./Factory";
//건물을 미리 지어놨다
const command = new CommandCenter();
const barrack = new Barrack(command, useTerranStore.getState);
const factory = new Factory(command, useTerranStore.getState);
//건물 커멘드에 등록
command.registerObserver(barrack);
command.registerObserver(factory);
function App() {
const { marinNum, medicNum, tankNum } = useTerranStore((state) => ({
marinNum: state.marin,
medicNum: state.medic,
tankNum: state.tank,
}));
const commandToBuilder = (marin: number, medic: number, tank: number) => {
command.command({
marin,
medic,
tank,
});
};
const reset = () => {
command.reset();
};
return (
<div className="flex gap-4 justify-center items-center h-[500px]">
<Command commandToBuilder={commandToBuilder} reset={reset} />
<BarrackComponent marinNum={marinNum} medicNum={medicNum} />
<FactoryComponent tankNum={tankNum} />
</div>
);
}
export default App;
자 이렇게 상태값만 받아서
커메드 센터에게 상태값을 바꾸는 함수만 전달하고 명령을 받는 주체는 상태값만 전달하면
import React from "react";
interface IBarrack {
marinNum: number;
medicNum: number;
}
const Barrack = ({ marinNum, medicNum }: IBarrack) => {
return (
<div className="w-[300px] h-[300px] border border-violet-500 flex flex-col items-center justify-center gap-7">
<h3>베럭</h3>
<div className="flex flex-col gap-5">
{marinNum > 0 ? (
<p className="rounded-md bg-cyan-400 p-2">
마린 {marinNum}명 양성합니다
</p>
) : null}
{medicNum > 0 ? (
<p className="rounded-md bg-cyan-400 p-2">
메딕 {medicNum}명 양성합니다
</p>
) : null}
</div>
</div>
);
};
export default Barrack;
import React from "react";
interface IFactory {
tankNum: number;
}
const Factory = ({ tankNum }: IFactory) => {
return (
<div className="w-[300px] h-[300px] border border-violet-500 flex flex-col items-center justify-center gap-7">
<h3>팩토리</h3>
<div>
{tankNum > 0 ? (
<p className="rounded-md bg-indigo-400 p-2">
탱크 {tankNum}대 생산합니다
</p>
) : null}
</div>
</div>
);
};
export default Factory;
커멘드의 명령 하나로 배럭과 팩토리의 상태값을 쉽게 바꿀 수 있었다.
만약 건물 짓는것 건물 파괴되는 로직까지 추가된다면 상태값 관리는 더더힘들어 질것이다. 이럴 때 이런 패턴을 이용해서 작업을 한다면 쉽게 상태관리를 할 수 있을 것 같습니다!!
상태관리를 디자인 패턴을 통해서 쉽게 할 수 있습니다!
zustand를 이용하면 이런 패턴을 응용하기 쉬워집니다!