Command[Design Pattern]

SnowCat·2023년 3월 17일
0

Design Pattern

목록 보기
15/23
post-thumbnail

의도

  • 커맨드 -> 요청을 하나의 큰 독립실행형 객체로 변환하는 행동 디자인 패턴
  • 다양한 요청들이 있는 메서드들을 인수화 할 수 있도록 하며, 요청의 실행을 기억하고 취소할 수 있는 작업을 지원할 수 있게 됨

문제

  • 텍스트 편집기 앱을 개발하고 있다 가정해보자. 현재 여러 버튼이 있는 툴바를 제작하고 있다.
  • 툴바 버튼에 사용할 버튼 클래스를 만들고, 각각의 버튼을 로직이 포함된 자식 클래스로 구현한다. 만약 툴바에서만 버튼을 사용한다면 이런 방식을 취해도 큰 문제가 없을 것이다.
  • 하지만 크기가 커져 버튼이 수십개가 되고 버튼 이외에서(ex: 복붙) 같은 비지니스 로직이 호출 가능하다면 코드가 중복되고, 객체간의 관계가 복잡하게 뒤엉키게 될 것이다.

해결책

  • 사용자 인터페이스가 직접 비즈니스 로직에 접근하는 것을 차단하고, 커맨드를 경유하도록 변경
  • 커맨드는 작업 단위로 만들게 되며, 동일한 작업은 같은 커맨드를 사용해 코드의 중복을 줄일 수 있음
  • 또한 커맨드를 중간에 둠으로써 사용자 인터페이스와 비즈니스 로직간의 결합을 줄일 수 있게 됨

구조

/* 
 * 커맨드 인터페이스
 * 커맨드를 실행하기 위한 메서드만을 선언하는게 일반적
 */
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 {
  	/*
     * 이렇게 객체에 비즈니스 로직이 포함되어 있는 경우를 수신자, receiver라고 부름
     * 대부분의 객체가 수신자 역할을 ㅅ할 수 있음
     */
    private receiver: Receiver;

    /**
     * receiver를 수행하기 위한 값
     */
    private a: string;

    private b: string;

    constructor(receiver: Receiver, a: string, b: string) {
        this.receiver = receiver;
        this.a = a;
        this.b = b;
    }

    /**
     * 커맨드는 작업을 처리하고 receiver에 위임하며, 실제 비즈니스 로직은 receiver에서 처맃
     */
    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);
    }
}

/**
 * The Receiver classes contain some important business logic. They know how to
 * perform all kinds of operations, associated with carrying out a request. In
 * fact, any class may serve as a Receiver.
 */
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}.)`);
    }
}

/**
 * invoker(발송자) 클래스는 요청들을 시작하는 역할 수행
 * 클라이언트는 요청을 invoker에 보내고, invoker가 커맨드를 대신 작동시킴
 */
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();
/*
Invoker: Does anybody want something done before I begin?
SimpleCommand: See, I can do simple things like printing (Say Hi!)
Invoker: ...doing something really important...
Invoker: Does anybody want something done after I finish?
ComplexCommand: Complex stuff should be done by a receiver object.
Receiver: Working on (Send email.)
Receiver: Also working on (Save report.)
*/

적용

  • 작업들로 객체를 매개변수화 하려는 경우에 사용
    특정 메서드 호출을 독립실행형 객체로 전환해 일관된 코드를 사용할 수 있게 됨
  • 작업들의 실행 순서를 기억하거나, 예약하고자 하는 경우에 사용
    커맨드는 객체이기 때문에 직렬화, 즉 파일이나 DB에 쓸 수 있는 문자열로 변환될 수 있음
    문자열은 초기 커맨드 객체로 복원할 수 있기 때문에 커맨드 실행 순서에 관한 로직을 설정해줄 수 있게 됨
  • 되돌릴 수 있는 작업을 구현하려 할 때 사용
    앱의 상태와 실행된 커맨드 객체를 스택으로 담아 구현

구현 방법

  1. 단일 실행 메서드로 커멘드 인터페이스 선언
  2. 요청들을 커맨드 인터페이스를 구현하는 구상 커맨드 클래스로 추출
    각 클래스에는 실제 수신자 객체에 대한 참조와 매개변수를 담을 필드를 가지고 생성자를 통해 초기화해야함
  3. 발송자 역할을 할 클래스들을 찾아 커맨드와 통신할 수 있도록 커맨드를 저장할 필드를 추가함
    커맨드 객체는 클라이언트 코드에서 가져옴
  4. 수신자에게 직접 요청을 보내는 대신 커맨드를 실행하도록 발송자들을 변경해야 함
  5. 크라이언트는 수신자 -> 커맨드 -> 발송자 순으로 객체를 초기화 시켜야 함

장단점

  • 작업을 호출하는 클래스를 수행하는 클래스와 분리해 단일 책임 원칙 준수
  • 새로운 커맨드 추가시 기존 코드를 건들 필요가 없기 때문에 개방, 폐쇄 원칙 준수
  • 실행 순서에 관한 조작을 구현 가능
  • 간단한 커맨드들을 복잡한 커맨드로 조합할 수 있음
  • 발송자와 수신자 사이에 완전히 새로운 레이어를 도입하기 떄문에 코드가 더 복잡해질 수 있음

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

profile
냐아아아아아아아아앙

0개의 댓글