디자인 패턴 - 3. 행동패턴

Chanyoung Park·2024년 6월 8일
0

행동(Hebavior) 패턴

객체간의 책임할당과 관련
어떻게 동작을 실행할 것인가?

1. 🔗 책임 연쇄(Chain of Responsibility)

  • 요청이 들어왔을 때, 객체->객체 간의 이동을 반복하며 적절한 핸들러를 찾을 때까지 계속 이동
  • 예시)
    • 결제수단이 1. 카드 2. 현금 3. 네이버페이 가 있을 경우,
    • 100,000원을 결제하려 한다.
    • 카드에서 100,000원을 결제한다 -> 하지만, 50,000원밖에 결제가 되지 않았다.
    • 2,3순위로 이동하여 결제를 계속한다.

2. 명령(Command)

  • 명령할 인터페이스를 분리하여, 클라이언트에서 사용할 때, 명령을 전달한다.
// 전구를 리모컨으로 조작할 때의 경우

interface Command {
  exe(): void;
  undo(): void;
  redo(): void;
}

class Bulb {
  turnOn(): void {
    console.log("전구 켜짐");
  }

  turnOff(): void {
    console.log("전구 꺼짐");
  }
}

class TurnOn implements Command {
  bulb: Bulb;

  constructor(bulb: Bulb) {
    this.bulb = bulb;
  }

  exe(): void {
    this.bulb.turnOn();
  }

  undo(): void {
    this.bulb.turnOff();
  }

  redo(): void {
    this.exe();
  }
}

class RemoteControl {
  submit(command: Command) {
    command.exe();
  }
}

const bulb = new Bulb();

const turnOn = new TurnOn(bulb);

const remote = new RemoteControl();

remote.submit(turnOn);

3. 반복자(Iterator)

  • 객체의 요소에 접근하는 방법을 제시하고, 내부 구조를 드러내지 않는다.
/**
 * @desc 라디오 채널을 예시로 한 코드
 */
class RadioStation {
  frequency: number;

  constructor(frequency: number) {
    this.frequency = frequency;
  }

  getFrequency(): number {
    return this.frequency;
  }
}

class StationList implements Iterable<RadioStation> {
  stations: RadioStation[] = [];
  counter = 0;

  addStation(station: RadioStation) {
    this.stations.push(station);
  }

  removeStation(station: RadioStation) {
    this.stations = this.stations.filter(
      (s) => s.frequency != station.frequency
    );
  }

  [Symbol.iterator](): Iterator<RadioStation> {
    return {
      next: () => {
        if (this.counter < this.stations.length) {
          return { done: false, value: this.stations[this.counter++] };
        } else {
          this.counter = 0;
          return { done: true, value: null };
        }
      },
    };
  }
}

const stationList = new StationList();
stationList.addStation(new RadioStation(100));
stationList.addStation(new RadioStation(200));
stationList.addStation(new RadioStation(300));

for (const station of stationList) {
  console.log(station.getFrequency());
}

stationList.removeStation(new RadioStation(200));

for (const station of stationList) {
  console.log(station.getFrequency());
}

4. 중재자(mediator)

  • 두 객체 간의 상호작용을 제어하기 위해 중재자를 추가한다.
  • 중재자패턴은 상호작용에 필요한 구현체를 알 필요가 없어, 객체간의 결합도를 줄일 수 있다.
/**
 * @desc 채팅방을 예시로 한 중재자 패턴
 */

interface ChatRoomMediator {
  showMessage(user: User, message: string): void;
}

class User {
  name: string;
  chatMediator: ChatRoomMediator;

  constructor(name: string, chatMediator: ChatRoomMediator) {
    this.name = name;
    this.chatMediator = chatMediator;
  }

  getName() {
    return this.name;
  }

  send(message: string): void {
    this.chatMediator.showMessage(this, message);
  }
}

class ChatRoom implements ChatRoomMediator {
  showMessage(user: User, message: string): void {
    const time = new Date().toLocaleDateString();
    const sender = user.getName();

    console.log(`${time} [${sender}]: ${message}`);
  }
}

const chatRoom = new ChatRoom();
const johnUser = new User("John", chatRoom);
const janeUser = new User("Jane", chatRoom);

johnUser.send("hi");
janeUser.send("hey there");

5. 메멘토(Memento)

  • 객체의 상태를 어딘가에 저장해두고, 나중에 복원할 수 있도록 저장해두는 패턴이다.
/**
 * @desc 텍스트편집기의 예시로 메멘토 패턴
 */
class EditorMemento {
  protected content: string;

  constructor(content: string) {
    this.content = content;
  }

  getContent(): string {
    return this.content;
  }
}

class Editor {
  protected content = "";

  type(words: string) {
    this.content = this.content + " " + words;
  }

  save(): EditorMemento {
    return new EditorMemento(this.content);
  }

  restore(editorMemento: EditorMemento) {
    this.content = editorMemento.getContent();
  }

  print(): void {
    console.log(this.content);
  }
}

const editor = new Editor();

editor.type("hi");
editor.type("hello");

const editorMemento = editor.save();

editor.type("whatup");
editor.print();

editor.restore(editorMemento);
editor.print();

6. 옵저버(Observer)

  • 객체 간의 의존성을 연결하여, 객체의 상태가 변경될 때마다 구독객체에 알림을 보낸다.
  • (react에서는 흔히 알고 있는 전역상태관리, state가 변경되면 UI렌더링)
/**
 * @desc 구인구직을 예시로 한 옵저버 패턴
 */
class JobPost {
  protected title: string;

  constructor(title: string) {
    this.title = title;
  }

  getTitle(): string {
    return this.title;
  }
}

interface Observer {
  onJobPosted(jobPost: JobPost): void;
}

class JobSeeker implements Observer {
  protected name: string;

  constructor(name: string) {
    this.name = name;
  }

  onJobPosted(jobPost: JobPost): void {
    console.log(`Hi ${this.name}! New Job Posted: ${jobPost.getTitle()}`);
  }
}

interface Observable {
  notify(jobPost: JobPost): void;
  attach(observer: Observer): void;
  addJob(jobPost: JobPost): void;
}

class EmployeeAgency implements Observable {
  protected observers: Observer[] = [];

  // 옵저버 등록
  attach(observer: Observer): void {
    this.observers.push(observer);
  }

  // 새로운 직업이 등록된다면, 알림 발송
  addJob(jobPost: JobPost): void {
    this.notify(jobPost);
  }

  // 등록된 옵저버들에게 새로운 직업이 등록되었다고 알리기
  notify(jobPost: JobPost): void {
    for (const observer of this.observers) {
      observer.onJobPosted(jobPost);
    }
  }
}

const john = new JobSeeker("john");
const jane = new JobSeeker("jane");

const jobPoster = new EmployeeAgency();
jobPoster.attach(john);
jobPoster.attach(jane);

jobPoster.addJob(new JobPost("FE Engineer"));

7. 방문자(Visitor)

  • 기존 객체를 수정하지 않고 새로운 작업을 추가하기 위한 패턴
/**
 * @desc 동물원을 예시로 한 방문자 패턴
 * 방문자들에게 여러동물들의 묘기를 보여줘야 함
 */
interface Animal {
  accept(operation: AnimalOperation): void;
}

interface AnimalOperation {
  visitMonkey(monkey: Monkey): void;
  visitLion(lion: Lion): void;
  visitDolphin(dolphin: Dolphin): void;
}

class Monkey implements Animal {
  accept(operation: AnimalOperation): void {
    operation.visitMonkey(this);
  }

  shout(): void {
    console.log("우끼끼");
  }
}

class Lion implements Animal {
  accept(operation: AnimalOperation): void {
    operation.visitLion(this);
  }

  roar(): void {
    console.log("ROAR");
  }
}

class Dolphin implements Animal {
  accept(operation: AnimalOperation): void {
    operation.visitDolphin(this);
  }

  speak(): void {
    console.log("돌고래 울음소리");
  }
}

class Speak implements AnimalOperation {
  visitMonkey(monkey: Monkey): void {
    monkey.shout();
  }

  visitDolphin(dolphin: Dolphin): void {
    dolphin.speak();
  }

  visitLion(lion: Lion): void {
    lion.roar();
  }
}

const monkey = new Monkey();
const lion = new Lion();
const dolphin = new Dolphin();

const speak = new Speak();

monkey.accept(speak);
lion.accept(speak);
dolphin.accept(speak);

// 여기서, Jump라는 묘기를 추가한다면 어떡할까?
// 아마 각 동물들을 수정해야 할 것이다.
// 그러지말고, Jump라는 새 방문자를 만들어 보자
class Jump implements AnimalOperation {
  visitMonkey(monkey: Monkey): void {
    console.log("monkey jump");
  }

  visitDolphin(dolphin: Dolphin): void {
    console.log("dolphin jump");
  }

  visitLion(lion: Lion): void {
    console.log("lion jump");
  }
}

const jump = new Jump();

monkey.accept(jump);
lion.accept(jump);
dolphin.accept(jump);

8. 전략(Strategy)

  • 상황에 따라 객체가 수행할 행동을 달리하는 것이다.
  • 예를 들어, 엄청난 알고리즘을 적용했다고 하자.
    • 하지만, 이는 데이터의 양이 방대할 때만 유효하고, 데이터 양이 적을 때는 오히려 역효과가 발생한다고 가정하자.

전략 패턴은 상황에 따라 전략을 전환하는 패턴이다.

interface ISort {
  sort(dataset: number[]): void;
}

class BubbleSort implements ISort {
  sort(dataset: number[]): void {
    console.log("데이터의 수가 적을 때는 Bubble이 유용해요!");
  }
}

class QuickSort implements ISort {
  sort(dataset: number[]): void {
    console.log("데이터의 수가 많은 때는 QuickSort가 유용해요!");
  }
}

class Sorter {
  protected sorterSmall: ISort;
  protected sorterBig: ISort;

  constructor(sorterSmall: ISort, sorterBig: ISort) {
    this.sorterSmall = sorterSmall;
    this.sorterBig = sorterBig;
  }

  sort(dataset: number[]): void {
    if (dataset.length > 5) {
      return this.sorterBig.sort(dataset);
    }
    return this.sorterSmall.sort(dataset);
  }
}

const dataSet = [1, 2, 3, 4, 5];

const sorter = new Sorter(new BubbleSort(), new QuickSort());

sorter.sort(dataSet);

dataSet.push(6);
sorter.sort(dataSet);

9. 상태(State)

상태가 변경될 때, 클래스의 동작을 변경
객체가 특정상태에 따라 행위를 달리하는 상황에서, 조건문으로 행위를 달리하는 것이 아닌 상태를 객체화 하여 상태가 행동할 수 있도록 위임하는 패턴

  • 상태는 state라는 변수로 관리하면서,
  • 메소드(행동)에 따라, state에 update될 내용을 클래스로 만들어 낸다.

10. 템플릿 메소드(Template Method)

객체의 생성단계가 절대 변경되지 못할 때, 이 단계를 템플릿화하여 생성하는 패턴이다.

/**
 * @desc 프로그램 개발을 예시로 템플릿메소드 패턴
 */
abstract class ProgramBuilder {
  public build(): void {
    this.test();
    this.lint();
    this.assemble();
    this.deploy();
  }

  abstract test(): void;
  abstract lint(): void;
  abstract assemble(): void;
  abstract deploy(): void;
}

class AProgramBuilder extends ProgramBuilder {
  test(): void {
    console.log("AProgramBuilder test");
  }

  lint(): void {
    console.log("AProgramBuilder lint");
  }

  assemble(): void {
    console.log("AProgramBuilder assemble");
  }

  deploy(): void {
    console.log("AProgramBuilder deploy");
  }
}

const AProgram = new AProgramBuilder();

AProgram.build();

마무리하며...

  • 생성, 구조, 행동 패턴에서의 다양한 디자인 패턴을 보며, 친근하게 느꼈던 패턴들이 많았다.
  • 그럴만한게, 코드를 작성하면서 내가 고민했던 방식들이 이 디자인패턴들의 특징과 맞닿아있었기 때문이다.
    • 퍼사드 패턴 builder패턴 템플릿 패턴과 같이 코드의 복잡성을 낮추려 할 때 많이 사용했던 것 같다.
  • 디자인 패턴을 알아보며, 코드를 작성하는 개발자에게는 디자인 패턴이라는 것은 고민의 흔적이라는 것이라고 느끼며, 디자인 패턴을 활용하는 일은 많지 않지만, 특별한 상황에서의 디자인패턴 활용은 해답이 되어주는 것 같다.
  • 이처럼 디자인 패턴은 많이 활용되지는 않지만, 특별한 문제에 맞닥뜨렸을 때 아 ~~디자인 패턴 사용하면 될 것 같은데?!라고 떠올릴 정도로 개념만 익히고 가는 것이 좋다고 생각한다.
profile
더 나은 개발경험을 생각하는, 프론트엔드 개발자입니다.

0개의 댓글