Skip to content

Latest commit

 

History

History
182 lines (146 loc) · 5.09 KB

File metadata and controls

182 lines (146 loc) · 5.09 KB

옵저버 패턴

옵서버 패턴은 당신이 여러 객체에 자신이 관찰 중인 객체에 발생하는 모든 이벤트에 대하여 알리는 구독 메커니즘을 정의할 수 있도록 하는 행동 디자인 패턴입니다

Overview

  • 매장에서 신제품을 기다리는 손님이 있다고 가정해본다.
  • 이때 손님 입장에서는 매장에서 손님들에게 재고 현황을 주기적으로 전송해주길 바랄 수 있다.
  • 하지만 원하는 신제품이 있는지 없는지 손님은 항상 체크를 해야 하고 이는 불편함을 야기시킨다.
  • 원하는 제품이 나왔을 때에만 알림을 받을 방법이 있다면 모두가 행복할것이다!
  • 이것이 옵저버 패턴의 핵심

Terminology

  • Publisher: 다른 객체들에 관심 이벤트를 발행하는 객체
    • Subscriber들이 publish상태를 변경시킬 수 있는 메소드를 제공
  • Subscriber: 알림 인터페이스를 선언하는 인터페이스
    • 대부분의 경우 단일 update 메소드를 가짐
    • 파라미터에는 Publisher 가 전달하는 이벤트에 대한 정보를 함께 가질 수 있음
  • Concrete subscriber: Publisher가 발행한 알림에 대한 응답으로 몇가지 작업을 수행
    • Publisher와 결합되지 않도록 같은 인터페이스를 구현해야 함

Implementation

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();

Usecases

Store implementation

// 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