의도
- 커맨드 -> 요청을 하나의 큰 독립실행형 객체로 변환하는 행동 디자인 패턴
- 다양한 요청들이 있는 메서드들을 인수화 할 수 있도록 하며, 요청의 실행을 기억하고 취소할 수 있는 작업을 지원할 수 있게 됨
문제
- 텍스트 편집기 앱을 개발하고 있다 가정해보자. 현재 여러 버튼이 있는 툴바를 제작하고 있다.
- 툴바 버튼에 사용할 버튼 클래스를 만들고, 각각의 버튼을 로직이 포함된 자식 클래스로 구현한다. 만약 툴바에서만 버튼을 사용한다면 이런 방식을 취해도 큰 문제가 없을 것이다.
- 하지만 크기가 커져 버튼이 수십개가 되고 버튼 이외에서(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 {
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();
적용
- 작업들로 객체를 매개변수화 하려는 경우에 사용
특정 메서드 호출을 독립실행형 객체로 전환해 일관된 코드를 사용할 수 있게 됨
- 작업들의 실행 순서를 기억하거나, 예약하고자 하는 경우에 사용
커맨드는 객체이기 때문에 직렬화, 즉 파일이나 DB에 쓸 수 있는 문자열로 변환될 수 있음
문자열은 초기 커맨드 객체로 복원할 수 있기 때문에 커맨드 실행 순서에 관한 로직을 설정해줄 수 있게 됨
- 되돌릴 수 있는 작업을 구현하려 할 때 사용
앱의 상태와 실행된 커맨드 객체를 스택으로 담아 구현
구현 방법
- 단일 실행 메서드로 커멘드 인터페이스 선언
- 요청들을 커맨드 인터페이스를 구현하는 구상 커맨드 클래스로 추출
각 클래스에는 실제 수신자 객체에 대한 참조와 매개변수를 담을 필드를 가지고 생성자를 통해 초기화해야함
- 발송자 역할을 할 클래스들을 찾아 커맨드와 통신할 수 있도록 커맨드를 저장할 필드를 추가함
커맨드 객체는 클라이언트 코드에서 가져옴
- 수신자에게 직접 요청을 보내는 대신 커맨드를 실행하도록 발송자들을 변경해야 함
- 크라이언트는 수신자 -> 커맨드 -> 발송자 순으로 객체를 초기화 시켜야 함
장단점
- 작업을 호출하는 클래스를 수행하는 클래스와 분리해 단일 책임 원칙 준수
- 새로운 커맨드 추가시 기존 코드를 건들 필요가 없기 때문에 개방, 폐쇄 원칙 준수
- 실행 순서에 관한 조작을 구현 가능
- 간단한 커맨드들을 복잡한 커맨드로 조합할 수 있음
- 발송자와 수신자 사이에 완전히 새로운 레이어를 도입하기 떄문에 코드가 더 복잡해질 수 있음
출처:
https://refactoring.guru/ko/design-patterns/command