Design Pattern(3) - 행동 패턴

황승우·2023년 1월 23일
0

design-pattern

목록 보기
3/3

행동 패턴

  • 객체 간의 효과적인 의사소통과 책임 할당을 처리

**책임 연쇄 패턴**(Chain of Responsibility **Pattern)**

  • 자세히보기
    • 핸들러들의 체인(사슬)을 따라 요청을 전달할 수 있게 해주는 행동 디자인 패턴

      
      interface Handler {
          setNext(handler: Handler): Handler;
      
          handle(request: string): string;
      }
      
      abstract class AbstractHandler implements Handler
      {
          private nextHandler: Handler;
      
          public setNext(handler: Handler): Handler {
              this.nextHandler = handler;
              // Returning a handler from here will let us link handlers in a
              // convenient way like this:
              // monkey.setNext(squirrel).setNext(dog);
              return handler;
          }
      
          public handle(request: string): string {
              if (this.nextHandler) {
                  return this.nextHandler.handle(request);
              }
      
              return null;
          }
      }
      
      class MonkeyHandler extends AbstractHandler {
          public handle(request: string): string {
              if (request === 'Banana') {
                  return `Monkey: I'll eat the ${request}.`;
              }
              return super.handle(request);
      
          }
      }
      
      class SquirrelHandler extends AbstractHandler {
          public handle(request: string): string {
              if (request === 'Nut') {
                  return `Squirrel: I'll eat the ${request}.`;
              }
              return super.handle(request);
          }
      }
      
      class DogHandler extends AbstractHandler {
          public handle(request: string): string {
              if (request === 'MeatBall') {
                  return `Dog: I'll eat the ${request}.`;
              }
              return super.handle(request);
          }
      }
      
      function clientCode(handler: Handler) {
          const foods = ['Nut', 'Banana', 'Cup of coffee'];
      
          for (const food of foods) {
              console.log(`Client: Who wants a ${food}?`);
      
              const result = handler.handle(food);
              if (result) {
                  console.log(`  ${result}`);
              } else {
                  console.log(`  ${food} was left untouched.`);
              }
          }
      }
      
      const monkey = new MonkeyHandler();
      const squirrel = new SquirrelHandler();
      const dog = new DogHandler();
      
      monkey.setNext(squirrel).setNext(dog);
    • 적용 할 곳

      • 프로그램이 다양한 방식으로 다양한 종류의 요청들을 처리할 것으로 예상되지만 정확한 요청 유형들과 순서들을 미리 알 수 없는 경우
      • 특정 순서로 여러 핸들러를 실행해야 할 때 사용
      • 핸들러들의 집합과 그들의 순서가 런타임에 변경되어야 할 때 사용
    • 장점

      • 요청의 처리 순서를 제어할 수 있음.
      • 작업을 호출하는 클래스들을 작업을 수행하는 클래스들과 분리할 수 있음.
      • 기존 클라이언트 코드를 손상하지 않고 앱에 새 핸들러들을 도입할 수 있음.
    • 단점

      • 일부 요청들은 처리되지 않을 수 있음.

**커맨드 패턴**(Command **Pattern)**

  • 자세히보기
    • 요청을 요청에 대한 모든 정보가 포함된 독립실행형 객체로 변환하는 행동 디자인 패턴

    • 이 변환은 다양한 요청들이 있는 메서드들을 인수화 할 수 있도록 하며, 요청의 실행을 지연 또는 대기열에 넣을 수 있도록 하고, 또 실행 취소할 수 있는 작업을 지원할 수 있도록 함.

      
      interface Command {
          execute(): void;
      }
      
      class SimpleCommand implements Command {
          private payload: string;
      
          constructor(payload: string) {
              this.payload = payload;
          }
      
          public execute(): void {
              console.log(`SimpleCommand: See, I can do simple things like printing (${this.payload})`);
          }
      }
      
      class ComplexCommand implements Command {
          private receiver: Receiver;
          private a: string;
          private b: string;
      
          constructor(receiver: Receiver, a: string, b: string) {
              this.receiver = receiver;
              this.a = a;
              this.b = b;
          }
      
          public execute(): void {
              console.log('ComplexCommand: Complex stuff should be done by a receiver object.');
              this.receiver.doSomething(this.a);
              this.receiver.doSomethingElse(this.b);
          }
      }
      
      class Receiver {
          public doSomething(a: string): void {
              console.log(`Receiver: Working on (${a}.)`);
          }
      
          public doSomethingElse(b: string): void {
              console.log(`Receiver: Also working on (${b}.)`);
          }
      }
      
      class Invoker {
          private onStart: Command;
      
          private onFinish: Command;
      
          public setOnStart(command: Command): void {
              this.onStart = command;
          }
      
          public setOnFinish(command: Command): void {
              this.onFinish = command;
          }
      
          public doSomethingImportant(): void {
              console.log('Invoker: Does anybody want something done before I begin?');
              if (this.isCommand(this.onStart)) {
                  this.onStart.execute();
              }
      
              console.log('Invoker: ...doing something really important...');
      
              console.log('Invoker: Does anybody want something done after I finish?');
              if (this.isCommand(this.onFinish)) {
                  this.onFinish.execute();
              }
          }
      
          private isCommand(object): object is Command {
              return object.execute !== undefined;
          }
      }
      
      const invoker = new Invoker();
      invoker.setOnStart(new SimpleCommand('Say Hi!'));
      const receiver = new Receiver();
      invoker.setOnFinish(new ComplexCommand(receiver, 'Send email', 'Save report'));
      
      invoker.doSomethingImportant()
    • 적용 할 곳

      • 작업들로 객체를 매개변수화하려는 경우
      • 되돌릴 수 있는 작업을 구현하려고 할 때
      • 작업들의 실행을 예약하거나, 작업들을 대기열에 넣거나 작업들을 원격으로 실행하려는 경우
    • 장점

      • 작업을 호출하는 클래스들을 이러한 작업을 수행하는 클래스들로부터 분리할 수 있음.
      • 기존 클라이언트 코드를 손상하지 않고 앱에 새 커맨드들을 도입할 수 있음.
      • 실행 취소/다시 실행을 구현할 수 있음.
      • 작업들의 지연된 실행을 구현할 수 있음.
      • 간단한 커맨드들의 집합을 복잡한 커맨드로 조합할 수 있음.
    • 단점

      • 발송자와 수신자 사이에 완전히 새로운 레이어를 도입하기 때문에 코드가 더 복잡해질 수 있음.

**반복자 패턴 패턴**(Iterator **Pattern)**

  • 자세히보기
    • 컬렉션의 요소들의 기본 표현(리스트, 스택, 트리 등)을 노출하지 않고 그들을 하나씩 순회할 수 있도록 하는 행동 디자인 패턴
    • 적용 할 곳
      interface Iterator<T> {
          current(): T;
      
          next(): T;
      
          key(): number;
      
          valid(): boolean;
      
          rewind(): void;
      }
      
      interface Aggregator {
          getIterator(): Iterator<string>;
      }
      
      class AlphabeticalOrderIterator implements Iterator<string> {
          private collection: WordsCollection;
          private position: number = 0;
          private reverse: boolean = false;
      
          constructor(collection: WordsCollection, reverse: boolean = false) {
              this.collection = collection;
              this.reverse = reverse;
      
              if (reverse) {
                  this.position = collection.getCount() - 1;
              }
          }
      
          public rewind() {
              this.position = this.reverse ?
                  this.collection.getCount() - 1 :
                  0;
          }
      
          public current(): string {
              return this.collection.getItems()[this.position];
          }
      
          public key(): number {
              return this.position;
          }
      
          public next(): string {
              const item = this.collection.getItems()[this.position];
              this.position += this.reverse ? -1 : 1;
              return item;
          }
      
          public valid(): boolean {
              if (this.reverse) {
                  return this.position >= 0;
              }
      
              return this.position < this.collection.getCount();
          }
      }
      
      class WordsCollection implements Aggregator {
          private items: string[] = [];
      
          public getItems(): string[] {
              return this.items;
          }
      
          public getCount(): number {
              return this.items.length;
          }
      
          public addItem(item: string): void {
              this.items.push(item);
          }
      
          public getIterator(): Iterator<string> {
              return new AlphabeticalOrderIterator(this);
          }
      
          public getReverseIterator(): Iterator<string> {
              return new AlphabeticalOrderIterator(this, true);
          }
      }
      
      const collection = new WordsCollection();
      collection.addItem('First');
      collection.addItem('Second');
      collection.addItem('Third');
      
      const iterator = collection.getIterator();
      
      console.log('Straight traversal:');
      while (iterator.valid()) {
          console.log(iterator.next());
      }
      
      console.log('');
      console.log('Reverse traversal:');
      const reverseIterator = collection.getReverseIterator();
      while (reverseIterator.valid()) {
          console.log(reverseIterator.next());
      }
    • 적용 할 곳
      • 당신의 컬렉션이 내부에 복잡한 데이터 구조가 있지만 이 구조의 복잡성을 보안이나 편의상의 이유로 클라이언트들로부터 숨기고 싶을 때
      • 당신의 앱 전체에서 순회 코드의 중복을 줄이고 싶은 경우
      • 코드가 다른 데이터 구조들을 순회할 수 있기를 원할 때 또는 이러한 구조들의 유형을 미리 알 수 없을 때
    • 장점
      • 부피가 큰 순회 알고리즘들을 별도의 클래스들로 추출하여 클라이언트 코드와 컬렉션들을 정돈할 수 있음.
      • 새로운 유형의 컬렉션들과 반복자들을 구현할 수 있으며 이들을 아무것도 훼손하지 않은 체 기존의 코드에 전달할 수 있음.
      • 같은 컬렉션을 병렬로 순회할 수 있음. 각 반복자 객체에는 자신의 고유한 순회 상태가 포함되어 있기 때문.
      • 순회를 지연하고 필요할 때 계속할 수 있음.
    • 단점
      • 단순한 컬렉션들과만 작동하는 경우 반복자 패턴을 적용하는 것은 과도할 수 있음.
      • 일부 특수 컬렉션들의 요소들을 직접 탐색하는 것보다 덜 효율적일 수 있음.

**중재자 패턴**(Mediator **Pattern)**

  • 자세히보기
    • 객체 간의 혼란스러운 의존 관계들을 줄일 수 있는 행동 디자인 패턴

    • 이 패턴은 객체 간의 직접 통신을 제한하고 중재자 객체를 통해서만 협력하도록 함.

      
      interface Mediator {
          notify(sender: object, event: string): void;
      }
      
      class ConcreteMediator implements Mediator {
          private component1: Component1;
      
          private component2: Component2;
      
          constructor(c1: Component1, c2: Component2) {
              this.component1 = c1;
              this.component1.setMediator(this);
              this.component2 = c2;
              this.component2.setMediator(this);
          }
      
          public notify(sender: object, event: string): void {
              if (event === 'A') {
                  console.log('Mediator reacts on A and triggers following operations:');
                  this.component2.doC();
              }
      
              if (event === 'D') {
                  console.log('Mediator reacts on D and triggers following operations:');
                  this.component1.doB();
                  this.component2.doC();
              }
          }
      }
      
      class BaseComponent {
          protected mediator: Mediator;
      
          constructor(mediator?: Mediator) {
              this.mediator = mediator!;
          }
      
          public setMediator(mediator: Mediator): void {
              this.mediator = mediator;
          }
      }
      
      class Component1 extends BaseComponent {
          public doA(): void {
              console.log('Component 1 does A.');
              this.mediator.notify(this, 'A');
          }
      
          public doB(): void {
              console.log('Component 1 does B.');
              this.mediator.notify(this, 'B');
          }
      }
      
      class Component2 extends BaseComponent {
          public doC(): void {
              console.log('Component 2 does C.');
              this.mediator.notify(this, 'C');
          }
      
          public doD(): void {
              console.log('Component 2 does D.');
              this.mediator.notify(this, 'D');
          }
      }
      
      const c1 = new Component1();
      const c2 = new Component2();
      const mediator = new ConcreteMediator(c1, c2);
      
      console.log('Client triggers operation A.');
      c1.doA();
      
      console.log('');
      console.log('Client triggers operation D.');
      c2.doD();
    • 적용 할 곳

      • 일부 클래스들이 다른 클래스들과 단단하게 결합하여 변경하기 어려울 때 사용
      • 타 컴포넌트들에 너무 의존하기 때문에 다른 프로그램에서 컴포넌트를 재사용할 수 없는 경우
      • 몇 가지 기본 행동을 다양한 콘텍스트들에서 재사용하기 위해 수많은 컴포넌트 자식 클래스들을 만들고 있는 스스로를 발견했을 때
    • 장점

      • 다양한 컴포넌트 간의 통신을 한곳으로 추출하여 코드를 이해하고 유지 관리하기 쉽게 만들 수 있음.
      • 실제 컴포넌트들을 변경하지 않고도 새로운 중재자들을 도입할 수 있음.
      • 프로그램의 다양한 컴포넌트 간의 결합도를 줄일 수 있음.
      • 개별 컴포넌트들을 더 쉽게 재사용할 수 있음.
    • 단점

      • 중재자는 전지전능한 객체로 발전할지도 모름.

**메멘토 패턴**(Memento **Pattern)**

  • 자세히보기
    • 객체의 구현 세부 사항을 공개하지 않으면서 해당 객체의 이전 상태를 저장하고 복원할 수 있게 해주는 행동 디자인 패턴

      
      class Originator {
          private state: string;
      
          constructor(state: string) {
              this.state = state;
              console.log(`Originator: My initial state is: ${state}`);
          }
      
          public doSomething(): void {
              console.log('Originator: I\'m doing something important.');
              this.state = this.generateRandomString(30);
              console.log(`Originator: and my state has changed to: ${this.state}`);
          }
      
          private generateRandomString(length: number = 10): string {
              const charSet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
      
              return Array
                  .apply(null, { length })
                  .map(() => charSet.charAt(Math.floor(Math.random() * charSet.length)))
                  .join('');
          }
      
          public save(): Memento {
              return new ConcreteMemento(this.state);
          }
      
          public restore(memento: Memento): void {
              this.state = memento.getState();
              console.log(`Originator: My state has changed to: ${this.state}`);
          }
      }
      
      interface Memento {
          getState(): string;
      
          getName(): string;
      
          getDate(): string;
      }
      
      class ConcreteMemento implements Memento {
          private state: string;
      
          private date: string;
      
          constructor(state: string) {
              this.state = state;
              this.date = new Date().toISOString().slice(0, 19).replace('T', ' ');
          }
      
          public getState(): string {
              return this.state;
          }
      
          public getName(): string {
              return `${this.date} / (${this.state.substr(0, 9)}...)`;
          }
      
          public getDate(): string {
              return this.date;
          }
      }
      
      class Caretaker {
          private mementos: Memento[] = [];
      
          private originator: Originator;
      
          constructor(originator: Originator) {
              this.originator = originator;
          }
      
          public backup(): void {
              console.log('\nCaretaker: Saving Originator\'s state...');
              this.mementos.push(this.originator.save());
          }
      
          public undo(): void {
              if (!this.mementos.length) {
                  return;
              }
              const memento = this.mementos.pop();
      
              console.log(`Caretaker: Restoring state to: ${memento.getName()}`);
              this.originator.restore(memento);
          }
      
          public showHistory(): void {
              console.log('Caretaker: Here\'s the list of mementos:');
              for (const memento of this.mementos) {
                  console.log(memento.getName());
              }
          }
      }
      
      const originator = new Originator('Super-duper-super-puper-super.');
      const caretaker = new Caretaker(originator);
      
      caretaker.backup();
      originator.doSomething();
      
      caretaker.backup();
      originator.doSomething();
      
      caretaker.backup();
      originator.doSomething();
      
      console.log('');
      caretaker.showHistory();
      
      console.log('\nClient: Now, let\'s rollback!\n');
      caretaker.undo();
      
      console.log('\nClient: Once more!\n');
      caretaker.undo();
    • 적용 할 곳

      • 객체의 이전 상태를 복원할 수 있도록 객체의 상태의 스냅샷들을 생성하려는 경우
      • 객체의 필드들/게터들/세터들을 직접 접근하는 것이 해당 객체의 캡슐화를 위반할 때
    • 장점

      • 캡슐화를 위반하지 않고 객체의 상태의 스냅샷들을 생성할 수 있음
    • 단점

      • 클라이언트들이 메멘토들을 너무 자주 생성하면 앱이 많은 RAM을 소모할 수 있음.
      • PHP, 파이썬 및 JavaScript와 같은 대부분의 동적 프로그래밍 언어에서는 메멘토 내의 상태가 그대로 유지된다고 보장할 수 없음.

**옵서버 패턴**(Observer **Pattern)**

  • 자세히보기
    • 당신이 여러 객체에 자신이 관찰 중인 객체에 발생하는 모든 이벤트에 대하여 알리는 구독 메커니즘을 정의할 수 있도록 하는 행동 디자인 패턴

      
      interface Subject {
          attach(observer: Observer): void;
      
          detach(observer: Observer): void;
      
          notify(): void;
      }
      
      class ConcreteSubject implements Subject {
          public state: number;
      
          private observers: Observer[] = [];
      
          public attach(observer: Observer): void {
              const isExist = this.observers.includes(observer);
              if (isExist) {
                  return console.log('Subject: Observer has been attached already.');
              }
      
              console.log('Subject: Attached an observer.');
              this.observers.push(observer);
          }
      
          public detach(observer: Observer): void {
              const observerIndex = this.observers.indexOf(observer);
              if (observerIndex === -1) {
                  return console.log('Subject: Nonexistent observer.');
              }
      
              this.observers.splice(observerIndex, 1);
              console.log('Subject: Detached an observer.');
          }
      
          public notify(): void {
              console.log('Subject: Notifying observers...');
              for (const observer of this.observers) {
                  observer.update(this);
              }
          }
      
          public someBusinessLogic(): void {
              console.log('\nSubject: I\'m doing something important.');
              this.state = Math.floor(Math.random() * (10 + 1));
      
              console.log(`Subject: My state has just changed to: ${this.state}`);
              this.notify();
          }
      }
      
      interface Observer {
          // Receive update from subject.
          update(subject: Subject): void;
      }
      
      class ConcreteObserverA implements Observer {
          public update(subject: Subject): void {
              if (subject instanceof ConcreteSubject && subject.state < 3) {
                  console.log('ConcreteObserverA: Reacted to the event.');
              }
          }
      }
      
      class ConcreteObserverB implements Observer {
          public update(subject: Subject): void {
              if (subject instanceof ConcreteSubject && (subject.state === 0 || subject.state >= 2)) {
                  console.log('ConcreteObserverB: Reacted to the event.');
              }
          }
      }
      
      const subject = new ConcreteSubject();
      
      const observer1 = new ConcreteObserverA();
      subject.attach(observer1);
      
      const observer2 = new ConcreteObserverB();
      subject.attach(observer2);
      
      subject.someBusinessLogic();
      subject.someBusinessLogic();
      
      subject.detach(observer2);
      
      subject.someBusinessLogic();
    • 적용 할 곳

      • 한 객체의 상태가 변경되어 다른 객체들을 변경해야 할 필요성이 생겼을 때, 그리고 실제 객체 집합들을 미리 알 수 없거나 이러한 집합들이 동적으로 변경될 때
      • 앱의 일부 객체들이 제한된 시간 동안 또는 특정 경우에만 다른 객체들을 관찰해야 할 때
    • 장점

      • 출판사의 코드를 변경하지 않고도 새 구독자 클래스들을 도입할 수 있음. 반대로 구독자의 클래스들을 변경하지 않고 새 출판사 클래스들을 도입하는 것 역시 가능.
      • 런타임에 객체 간의 관계들을 형성할 수 있음.
    • 단점

      • 구독자들은 무작위로 알림을 받음

**상태 패턴**(State **Pattern)**

  • 자세히보기
    • 객체의 내부 상태가 변경될 때 해당 객체가 그의 행동을 변경할 수 있도록 하는 행동 디자인 패턴

    • 객체가 행동을 변경할 때 객체가 클래스를 변경한 것처럼 보일 수 있음.

      class Context {
          private state: State;
      
          constructor(state: State) {
              this.transitionTo(state);
          }
      
          public transitionTo(state: State): void {
              console.log(`Context: Transition to ${(<any>state).constructor.name}.`);
              this.state = state;
              this.state.setContext(this);
          }
      
          public request1(): void {
              this.state.handle1();
          }
      
          public request2(): void {
              this.state.handle2();
          }
      }
      
      abstract class State {
          protected context: Context;
      
          public setContext(context: Context) {
              this.context = context;
          }
      
          public abstract handle1(): void;
      
          public abstract handle2(): void;
      }
      
      class ConcreteStateA extends State {
          public handle1(): void {
              console.log('ConcreteStateA handles request1.');
              console.log('ConcreteStateA wants to change the state of the context.');
              this.context.transitionTo(new ConcreteStateB());
          }
      
          public handle2(): void {
              console.log('ConcreteStateA handles request2.');
          }
      }
      
      class ConcreteStateB extends State {
          public handle1(): void {
              console.log('ConcreteStateB handles request1.');
          }
      
          public handle2(): void {
              console.log('ConcreteStateB handles request2.');
              console.log('ConcreteStateB wants to change the state of the context.');
              this.context.transitionTo(new ConcreteStateA());
          }
      }
      
      const context = new Context(new ConcreteStateA());
      context.request1();
      context.request2();
    • 적용 할 곳

      • 현재 상태에 따라 다르게 행동하는 객체가 있을 때, 상태들의 수가 많을 때, 그리고 상태별 코드가 자주 변경될 때
      • 당신이 클래스 필드들의 현재 값들에 따라 클래스가 행동하는 방식을 변경하는 거대한 조건문들로 오염된 클래스가 있을 때
      • 유사한 상태들에 중복 코드와 조건문-기반 상태 머신의 천이가 많을 때
    • 장점

      • 특정 상태들과 관련된 코드를 별도의 클래스들로 구성할 수 있음.
      • 존 상태 클래스들 또는 콘텍스트를 변경하지 않고 새로운 상태들을 도입할 수 있음.
      • 거대한 상태 머신 조건문들을 제거하여 콘텍스트의 코드를 단순화할 수 있음.
    • 단점

      • 상태 머신에 몇 가지 상태만 있거나 머신이 거의 변경되지 않을 때 상태 패턴을 적용하는 것은 과도할 수 있음.

**전략 패턴**(Strategy **Pattern)**

  • 자세히보기
    • 알고리즘들의 패밀리를 정의하고, 각 패밀리를 별도의 클래스에 넣은 후 그들의 객체들을 상호교환할 수 있도록 하는 행동 디자인 패턴

      class Context {
          private strategy: Strategy;
          constructor(strategy: Strategy) {
              this.strategy = strategy;
          }
      
           public setStrategy(strategy: Strategy) {
              this.strategy = strategy;
          }
      
          public doSomeBusinessLogic(): void {
              // ...
      
              console.log('Context: Sorting data using the strategy (not sure how it\'ll do it)');
              const result = this.strategy.doAlgorithm(['a', 'b', 'c', 'd', 'e']);
              console.log(result.join(','));
      
              // ...
          }
      }
      
      interface Strategy {
          doAlgorithm(data: string[]): string[];
      }
      class ConcreteStrategyA implements Strategy {
          public doAlgorithm(data: string[]): string[] {
              return data.sort();
          }
      }
      
      class ConcreteStrategyB implements Strategy {
          public doAlgorithm(data: string[]): string[] {
              return data.reverse();
          }
      }
      
      const context = new Context(new ConcreteStrategyA());
      console.log('Client: Strategy is set to normal sorting.');
      context.doSomeBusinessLogic();
      
      console.log('');
      
      console.log('Client: Strategy is set to reverse sorting.');
      context.setStrategy(new ConcreteStrategyB());
      context.doSomeBusinessLogic();
    • 적용 할 곳

      • 객체 내에서 한 알고리즘의 다양한 변형들을 사용하고 싶을 때, 그리고 런타임 중에 한 알고리즘에서 다른 알고리즘으로 전환하고 싶을 때
      • 일부 행동을 실행하는 방식에서만 차이가 있는 유사한 클래스들이 많은 경우에 사용
      • 전략 패턴을 사용하여 클래스의 비즈니스 로직을 해당 로직의 콘텍스트에서 그리 중요하지 않을지도 모르는 알고리즘들의 구현 세부 사항들로부터 고립
      • 같은 알고리즘의 다른 변형들 사이를 전환하는 거대한 조건문이 당신의 클래스에 있는 경우
    • 장점

      • 런타임에 한 객체 내부에서 사용되는 알고리즘들을 교환할 수 있음.
      • 알고리즘을 사용하는 코드에서 알고리즘의 구현 세부 정보들을 고립할 수 있음.
      • 상속을 합성으로 대체할 수 있음.
      • 콘텍스트를 변경하지 않고도 새로운 전략들을 도입할 수 있음.
    • 단점

      • 알고리즘이 몇 개밖에 되지 않고 거의 변하지 않는다면, 패턴과 함께 사용되는 새로운 클래스들과 인터페이스들로 프로그램을 지나치게 복잡하게 만들 이유가 없음.
      • 클라이언트가 적절한 전략을 선택할 수 있도록 전략 간의 차이점들을 알고 있어야 함.
      • 현대의 많은 프로그래밍 언어에는 익명 함수들의 집합 내에서 알고리즘의 다양한 버전들을 구현할 수 있는 함수형 지원이 있으며, 클래스들과 인터페이스들을 추가하여 코드의 부피를 늘리지 않으면서도 전략 객체를 사용했을 때와 똑같이 이러한 함수들을 사용할 수 있음.

**템플릿 메서드 패턴**(Template Method **Pattern)**

  • 자세히보기
    • 부모 클래스에서 알고리즘의 골격을 정의하지만, 해당 알고리즘의 구조를 변경하지 않고 자식 클래스들이 알고리즘의 특정 단계들을 오버라이드(재정의)할 수 있도록 하는 행동 디자인 패턴

      abstract class AbstractClass {
          public templateMethod(): void {
              this.baseOperation1();
              this.requiredOperations1();
              this.baseOperation2();
              this.hook1();
              this.requiredOperation2();
              this.baseOperation3();
              this.hook2();
          }
      
          protected baseOperation1(): void {
              console.log('AbstractClass says: I am doing the bulk of the work');
          }
      
          protected baseOperation2(): void {
              console.log('AbstractClass says: But I let subclasses override some operations');
          }
      
          protected baseOperation3(): void {
              console.log('AbstractClass says: But I am doing the bulk of the work anyway');
          }
      
          protected abstract requiredOperations1(): void;
      
          protected abstract requiredOperation2(): void;
      
          protected hook1(): void { }
      
          protected hook2(): void { }
      }
      
      class ConcreteClass1 extends AbstractClass {
          protected requiredOperations1(): void {
              console.log('ConcreteClass1 says: Implemented Operation1');
          }
      
          protected requiredOperation2(): void {
              console.log('ConcreteClass1 says: Implemented Operation2');
          }
      }
      
      class ConcreteClass2 extends AbstractClass {
          protected requiredOperations1(): void {
              console.log('ConcreteClass2 says: Implemented Operation1');
          }
      
          protected requiredOperation2(): void {
              console.log('ConcreteClass2 says: Implemented Operation2');
          }
      
          protected hook1(): void {
              console.log('ConcreteClass2 says: Overridden Hook1');
          }
      }
      
      function clientCode(abstractClass: AbstractClass) {
          // ...
          abstractClass.templateMethod();
          // ...
      }
      
      console.log('Same client code can work with different subclasses:');
      clientCode(new ConcreteClass1());
      console.log('');
      
      console.log('Same client code can work with different subclasses:');
      clientCode(new ConcreteClass2());
    • 적용 할 곳

      • 클라이언트들이 알고리즘의 특정 단계들만 확장할 수 있도록 하고 싶을 때, 그러나 전체 알고리즘이나 알고리즘 구조는 확장하지 못하도록 하려고 할 때
      • 약간의 차이가 있지만 거의 같은 알고리즘들을 포함하는 여러 클래스가 있는 경우
    • 장점

      • 클라이언트들이 대규모 알고리즘의 특정 부분만 오버라이드하도록 하여 그들이 알고리즘의 다른 부분에 발생하는 변경에 영향을 덜 받도록 할 수 있음.
      • 중복 코드를 부모 클래스로 가져올 수 있음.
    • 단점

      • 일부 클라이언트들은 알고리즘의 제공된 골격에 의해 제한될 수 있음.
      • 자식 클래스를 통해 디폴트 단계 구현을 억제하여 리스코프 치환 원칙(부모 객체와 이를 상속한 자식 객체가 있을 때 부모 객체를 호출하는 동작에서 자식 객체가 부모 객체를 완전히 대채할 수 있다는 원칙)을 위반할 수 있음.
      • 단계들이 더 많을수록 유지가 더 어려운 경향이 있음.

**비지터 패턴**(Visitor **Pattern)**

  • 자세히보기
    • 알고리즘들을 그들이 작동하는 객체들로부터 분리할 수 있도록 하는 행동 디자인 패턴

      interface Component {
          accept(visitor: Visitor): void;
      }
      
      class ConcreteComponentA implements Component {
          public accept(visitor: Visitor): void {
              visitor.visitConcreteComponentA(this);
          }
      
          public exclusiveMethodOfConcreteComponentA(): string {
              return 'A';
          }
      }
      
      class ConcreteComponentB implements Component {
          public accept(visitor: Visitor): void {
              visitor.visitConcreteComponentB(this);
          }
      
          public specialMethodOfConcreteComponentB(): string {
              return 'B';
          }
      }
      
      interface Visitor {
          visitConcreteComponentA(element: ConcreteComponentA): void;
      
          visitConcreteComponentB(element: ConcreteComponentB): void;
      }
      
      class ConcreteVisitor1 implements Visitor {
          public visitConcreteComponentA(element: ConcreteComponentA): void {
              console.log(`${element.exclusiveMethodOfConcreteComponentA()} + ConcreteVisitor1`);
          }
      
          public visitConcreteComponentB(element: ConcreteComponentB): void {
              console.log(`${element.specialMethodOfConcreteComponentB()} + ConcreteVisitor1`);
          }
      }
      
      class ConcreteVisitor2 implements Visitor {
          public visitConcreteComponentA(element: ConcreteComponentA): void {
              console.log(`${element.exclusiveMethodOfConcreteComponentA()} + ConcreteVisitor2`);
          }
      
          public visitConcreteComponentB(element: ConcreteComponentB): void {
              console.log(`${element.specialMethodOfConcreteComponentB()} + ConcreteVisitor2`);
          }
      }
      
      function clientCode(components: Component[], visitor: Visitor) {
          // ...
          for (const component of components) {
              component.accept(visitor);
          }
          // ...
      }
      
      const components = [
          new ConcreteComponentA(),
          new ConcreteComponentB(),
      ];
      
      console.log('The client code works with all visitors via the base Visitor interface:');
      const visitor1 = new ConcreteVisitor1();
      clientCode(components, visitor1);
      console.log('');
      
      console.log('It allows the same client code to work with different types of visitors:');
      const visitor2 = new ConcreteVisitor2();
      clientCode(components, visitor2);
    • 적용 할 곳

      • 복잡한 객체 구조(예: 객체 트리)의 모든 요소에 대해 작업을 수행해야 할 때
      • 비지터 패턴을 사용하여 보조 행동들의 비즈니스 로직을 정리
      • 행동이 클래스 계층구조의 일부 클래스들에서만 의미가 있고 다른 클래스들에서는 의미가 없을 때
    • 장점

      • 다른 클래스를 변경하지 않으면서 해당 클래스의 객체와 작동할 수 있는 새로운 행동을 도입할 수 있음.
      • 같은 행동의 여러 버전을 같은 클래스로 이동할 수 있음.
      • 비지터 객체는 다양한 객체들과 작업하면서 유용한 정보를 축적할 수 있음. 이것은 객체 트리와 같은 복잡한 객체 구조를 순회하여 이 구조의 각 객체에 비지터 패턴을 적용하려는 경우에 유용할 수 있음.
    • 단점

      • 클래스가 요소 계층구조에 추가되거나 제거될 때마다 모든 비지터를 업데이트해야함.
      • 비지터들은 함께 작업해야 하는 요소들의 비공개 필드들 및 메서드들에 접근하기 위해 필요한 권한이 부족할 수 있음.

참고

https://refactoring.guru/ko/design-patterns/chain-of-responsibility

https://refactoring.guru/ko/design-patterns/command

https://refactoring.guru/ko/design-patterns/iterator

https://refactoring.guru/ko/design-patterns/mediator

https://refactoring.guru/ko/design-patterns/memento

https://refactoring.guru/ko/design-patterns/observer

https://refactoring.guru/ko/design-patterns/state

https://refactoring.guru/ko/design-patterns/strategy

https://refactoring.guru/ko/design-patterns/template-method

https://refactoring.guru/ko/design-patterns/visitor

profile
백엔드 개발자

0개의 댓글