[Behavioral Patterns] - Command

Lee Jeong Min·2022년 1월 16일
0

디자인 패턴

목록 보기
15/23
post-thumbnail

의도

명령은 요청을 요청에 대한 모든 정보를 포함하는 독립 실행형 객체로 변환하는 동작 설계 패턴이다. 이 변환을 통해 요청을 메서드 인수로 전달하고, 요청 실행을 지연 또는 대기열에 놓으며, 실행할 수 없는 작업을 지원할 수 있다.

변환은 명령의 지연 또는 원격 실행, 명령 기록 저장 등을 허용한다.

문제

새로운 텍스트 편집 앱을 만드록 있다고 상상해보자. 편집기의 다양한 작업을 위한 여러 버튼이 있는 도구 모음을 만들고 이를위한 깔끔한 Button 클래스를 만들었다.

앱의 모든 버튼은 동일한 클래스에서 파생된다.

모든 버튼들이 비슷하게 보이지만 이것들은 모두 다른일을 한다. 이러한 상황에서 클릭 핸들러의 코드를 어디에 두어야 할까? 가장 간단한 방법은 버튼이 사용되는 각 위치에 여러개의 하위 클래스를 만들어 클릭 핸들러를 각각 정의하는 것이다.

엄청나게 많은 버튼 클래스들... 문제가 발생할 것 같다.

머지않아 이러한 접근법이 잘못되었다는 것을 깨달을 것이다. 그 이유로 수많은 하위 클래스가 있고 기본 Button 클래스를 수정할 때마다 비즈니스 로직의 휘발성 코드(변하는 부분)에 어색하게 의존하고 있어 하위 클래스 버튼의 코드를 수정하지 않아도 된다.

여러 클래스가 동일한 기능을 구현한다.

그리고 가장 안좋은 부분이 있는데 텍스트 복붙 같은 기능은 여러 위치에서 호출되어야 한다. 예를들어 사용자는 도구 모음에서 작은 '복사' 버튼을 클릭하거나 상황에 맞는 메뉴를 통해 무언가를 복사하거나 키보드에서 Ctrl + C를 누를 수 있다.

처음에는 버튼 서브클래스에 다양한 연산을 구현해도 괜찮았다. 다시 말해, CopyButton 하위 클래스 안에 텍스트를 복사하는 코드가 있어도 괜찮았다. 그러나 컨텍스트 메뉴, 바로가기 등을 구현할 때 많은 클래스에서 연산 코드를 복제하거나 메뉴에 버튼을 의존하도록 만들어야하는데 이는 더 상황을 악화시킨다.

해결책

훌륭한 소프트웨어 설계는 관심사의 분리 원칙에 기반을 두는데 보통 앱을 계층으로 분리하게 된다. 가장 일반적인 예로 그래픽 사용자 인터페이스를 위한 계층과 비즈니스 논리를 위한 또 다른 계층이다. GUI 계층은 화면에 그림을 렌더링하고, 입력을 캡처하고, 사용자와 앱이 하는 일의 결과를 보여주는 역할을 한다. 이외에 다른 작업은 비즈니스 로직의 기초 계층으로 위임한다.

GUI 객체는 비즈니스 논리 객체에 직접 액세스할 수 있다.

명령 패턴은 GUI 객체가 이러한 요청을 직접 전송하지 않고 이 요청과 관련하여 모든 세부 정보를 트리거하는 단일 메서드를 사용하여 별도의 명령 클래스에 추출한다.

명령 객체는 GUI와 비즈니스 논리간 링크 역할을 하면서 GUI 객체는 모든 세부 정보를 처리하는 명령만 트리거하면 된다.

비즈니스 로직 계층에 대한 접근을 명령을 통해 한다.

다음 단계는 명령이 동일한 인터페이스를 구현하도록 하는 것이다. 일반적으로 매개 변수를 사용하지 않는 단일 실행 메서드를 사용하며 특정 클래스의 명령에 결합하지 않고 동일한 요청 송신자와 함께 다양한 명령을 사용할 수 있다. 또한 런타임에 송신자의 동작을 효과적으로 변환할 수도 있다.

만약 GUI 객체가 비즈니스 계층 객체에 일부 매개 변수를 전달할 경우는 그럼 어떻게 해야할까? 그런 경우에는 이 매개변수 데이터를 사용하여 명령을 미리 구성했거나 직접 가져올 수 있어야 한다.

GUI 객체는 명령을 사용하여 작업을 위임한다.

다시 텍스트 편집기로 돌아와서 보면 버튼의 하위 클래스는 이제 필요없고 명령 객체에 대한 참조를 저장하는 기본 클래스에 필드 하나를 넣고 클릭 한번으로 해당 명령을 실행시키도록 만들자.

가능한 모든 작업에 대해 명령 클래스 모음을 구현하고 버튼의 의도된 동작에 따라 특정 버튼과 연결한다.

이를 통해 동일한 연산과 관련된 요소들은 동일한 명령어에 연결되어 어떤 코드 중복도 방지하게 되고 GUI와 비즈니스 로직 계층 간 결합을 줄이는 편리한 중간 계층이 된다.

현실 유사성

레스토랑에서 주문하는 과정이 명령 패턴과 유사하다.

레스토랑에서 주문을 하게 되면 웨이터가 주문을 종이에 적고 셰프는 그것을 읽고 그에 따라 요리를 하게 된다. 이후 요리가 완료되면 웨이터가 요리를 다시 손님에게 갖다주게 된다.

주문을 적은 종이는 명령의 역할을 하게 되고 셰프가 요리를 완료할때까지 남아있게 된다.

구조

  1. 호출자 클래스는 요청을 시작하는 역할을 한다. 이 클래스에는 명령 객체에 대한 참조를 저장하는 필드가 있어야 한다. 수신자에게 직접 요청을 보내는 대신 송신자가 해당 명령을 트리거한다. 송신자는 명령 객체를 작성할 책임이 없고 일반적으로 생성자를 통해 클라이언트로부터 미리 생성된 명령을 받는다.

  2. 명령 인터페이스는 명령 실행을 위한 단일 메서드만 선언한다.

  3. 구체적인 명령은 다양한 종류의 요청을 구현한다. 스스로 작업을 수행하는 것이 아닌 비즈니스 논리 객체 중 하나에 호출을 전달하는 것이며 코드를 단순화하기 위해 이 클래스들은 병합될 수 있다.
    수신 객체에서 메서드를 실행하는 데 필요한 매개 변수는 구체적인 명령의 필드로 선언할 수 있다. 생성자를 통해서만 이러한 필드의 초기화를 허용함으로써 명령 객체를 불변으로 만들 수 있다.

  4. 수신자 클래스에는 몇 가지 비즈니스 논리가 포함되어 있다. 거의 모든 객체는 수신자의 역할을 할 수 있고 대부분의 명령어는 수신자가 실제 작업을 수행하는 동안 요청이 수신자로 전달되는 방법에 대한 세부 사항만 처리한다.

  5. 클라이언트는 구체적인 명령 객체를 만들고 구성한다. 클라이언트는 수신자 인스턴스를 포함한 모든 요청 매개변수를 명령의 생성자로 전달해야 한다. 그런 다음 결과 명령을 호출자와 연결할 수 있다.

적용가능성

  • 작업을 통해 객체를 매개 변수화하려면 명령 패턴을 사용하라.

  • 작업을 대기열에 넣거나, 실행을 예약하거나, 원격으로 실행하려는 경우 명령 패턴을 사용하라.

  • 되돌릴 수 있는 연산을 구현하길 원한다면 명령 패턴을 사용하라.

장단점

장점

  • SRP

  • OCP

  • 실행 취소/재실행할 수 있다.

  • 지연된 작업 실행을 구현할 수 있다.

  • 간단한 명령 집합을 복잡한 명령으로 조합할 수 있다.

단점

  • 송신자와 수신자 사이에 완전히 새로운 계층이 도입되어 코드가 더 복잡해질 수 있다.

Command in TypeScript

TypeScript의 패턴 사용

복잡도: ★☆☆

인기: ★★★

사용 예: 명령 패턴은 TS 코드에서 매우 일반적이다. 대부분의 경우 UI 요소를 동작으로 매개 변수화하는 콜백의 대안으로 사용된다. 또한 작업 대기 행렬, 작업 내역 추적등에 사용된다.

식별: 명령 패턴은 다른 추상/인터페이스 유형(수신자)의 구현에서 메서드를 호출하는 추상/인터페이스 유형(발신자)의 동작 메서드에 의해 인식되며, 이는 생성 중에 명령 구현에 의해 캡슐화된다.

index.ts

// 명령 실행을 위한 메서드를 명령 인터페이스에 선언한다.
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})`);
  }
}

// 그러나 몇몇 명령어는 recevier라고 불리는 다른 객체에게 복잡한 작업을 위임한다.
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();

결과

요약

명령 패턴은 요청에 대한 정보를 포함하는 객체로 변환하여 인수로 전달하여 사용하는 방법이다.

이를 위해 관심사의 분리 원칙에 따라 새로운 계층을 만들어 양 계층간에서 링크 역할을 하는 객체로 명령을 트리거하여 사용한다.

간단한 명령이 아닌 구체적인 명령은 비즈니스 논리 객체 중 하나에 호출을 전달하고 생성자를 통해 필드를 초기화하여 사용한다.

참고 사이트

profile
It is possible for ordinary people to choose to be extraordinary.

0개의 댓글