옵서버 패턴은 당신이 여러 객체에 자신이 관찰 중인 객체에 발생하는 모든 이벤트에 대하여 알리는 구독 메커니즘을 정의할 수 있도록 하는 행동 디자인 패턴입니다
- 매장에서 신제품을 기다리는 손님이 있다고 가정해본다.
- 이때 손님 입장에서는 매장에서 손님들에게 재고 현황을 주기적으로 전송해주길 바랄 수 있다.
- 하지만 원하는 신제품이 있는지 없는지 손님은 항상 체크를 해야 하고 이는 불편함을 야기시킨다.
- 원하는 제품이 나왔을 때에만 알림을 받을 방법이 있다면 모두가 행복할것이다!
- 이것이 옵저버 패턴의 핵심
- Publisher: 다른 객체들에 관심 이벤트를 발행하는 객체
- Subscriber들이 publish상태를 변경시킬 수 있는 메소드를 제공
- Subscriber: 알림 인터페이스를 선언하는 인터페이스
- 대부분의 경우 단일 update 메소드를 가짐
- 파라미터에는 Publisher 가 전달하는 이벤트에 대한 정보를 함께 가질 수 있음
- Concrete subscriber: Publisher가 발행한 알림에 대한 응답으로 몇가지 작업을 수행
- Publisher와 결합되지 않도록 같은 인터페이스를 구현해야 함
interface Context<T> {
data: T;
name: string;
}
// Subscriber interface: 알림 인터페이스를 선언
interface Subscriber<T> {
update(ctx: Context<T>): void;
}
// Concrete subscriber: publisher의 이벤트 발행에 응답
class ConcreteSubscriber implements Subscriber<number> {
public update(ctx: Context<number>): void {
console.log("event name:", ctx.name, "with data:", ctx.data.toString());
}
}
// Publisher: 다른 객체들에 관심 이벤트를 발행
class Publisher {
public subscribers: Subscriber<number>[] = [];
public subscribe(subscriber: Subscriber<number>) {
this.subscribers.push(subscriber);
}
public notifySubscribers(ctx: Context<number>) {
this.subscribers.forEach((subscriber) => subscriber.update(ctx));
}
public unsubscribe(subscriber: Subscriber<number>) {
this.subscribers = this.subscribers.filter((sub) => sub !== subscriber);
}
public mainBusinessLogic(input: number) {
if (input > 10) return;
this.notifySubscribers({
name: "valid_input",
data: input,
});
}
}
class Client {
public static run() {
const publisher = new Publisher();
const s1 = new ConcreteSubscriber();
const s2 = new ConcreteSubscriber();
publisher.subscribe(s1);
publisher.subscribe(s2);
publisher.mainBusinessLogic(29); // nothing happens
publisher.mainBusinessLogic(8); // event name: valid_input with data: 8 (twice)
}
}
Client.run();
// src/Store.ts
export type Reducer<S, A> = (state: S, action: A) => S;
export type Listener<S> = (state: S) => void;
export interface IStore<S, A> {
state: S;
dispatch(action: A): void;
subscribe(listener: Listener<S>): void;
}
export class Store<S, A> implements IStore<S, A> {
private listeners: ((state: S) => void)[] = [];
constructor(public state: S, public reducer: Reducer<S, A>) {}
public dispatch(action: A): void {
this.state = this.reducer(this.state, action);
this.listeners.forEach((listener) => listener(this.state));
}
public subscribe(listener: Listener<S>): void {
this.listeners.push(listener);
}
public unsubscribe(listener: Listener<S>): void {
this.listeners = this.listeners.filter((l) => l !== listener);
}
}
// src/Calendar/application/store.ts
import { Store } from "../../Store";
export interface CalendarState {
year: number;
month: number;
showListView: boolean;
}
export type CalendarAction =
| { type: "toggleListView" }
| { type: "previousMonth" }
| { type: "nextMonth" }
| { type: "gotoToday" };
export const calendarStore = new Store<CalendarState, CalendarAction>(
{
year: new Date().getFullYear(),
month: new Date().getMonth() + 1,
showListView: false,
},
(state, action) => {
switch (action.type) {
case "toggleListView":
return { ...state, showListView: !state.showListView };
case "previousMonth":
return {
...state,
month: state.month === 1 ? 12 : state.month - 1,
year: state.month === 1 ? state.year - 1 : state.year,
};
case "nextMonth":
return {
...state,
month: state.month === 12 ? 1 : state.month + 1,
year: state.month === 12 ? state.year + 1 : state.year,
};
case "gotoToday":
return {
...state,
month: new Date().getMonth() + 1,
year: new Date().getFullYear(),
};
}
}
);
// src/Calendar/ui/Calendar.ts
export class Calendar extends Component {
private store = calendarStore;
// ...
public hydrate(): void {
this.store.subscribe(this.render);
}
public teardown(): void {
this.store.unsubscribe(this.render);
}
}
전체 코드 레퍼런스: https://github.com/rudy3091/app/blob/master/src/Calendar/ui/Calendar.ts