요청을 요청에 대한 모든 정보를 가진 독립 실행형 객체로 만드는 행동 패턴
텍스트 에디터 앱 가정
에디터의 다양한 작업을 위한 많은 버튼들이 있는 툴바를 만들어야 함
Button
클래스를 생성했음모든 버튼이 비슷하게 생겼지만 모두 다른 작업을 하는 상황 - 버튼의 클릭 핸들러는 어디에 위치해야 할까?
잘못된 접근 방식
Button
클래스를 수정하면 서브클래스 코드 훼손복사/붙여넣기와 같은 작업들은 다양한 곳에서 호출됨
Ctrl+C
등등앱이 툴바만 가지고 있었을 때에는 다양한 작업의 구현을 버튼 서브클래스에 위치시켰어도 됐음
e.g. CopyButton
내에 텍스트 복사 코드가 있어도 무관
단축키, 콘텍스트 메뉴 등 다른 것들이 구현되면 한 작업의 코드를 많은 클래스에 붙여넣거나 메뉴들이 버튼에 의존하게 만들어야 함 → 더 안 좋은 상황
좋은 소프트웨어 디자인은 관심사 분리의 원칙을 기반으로 함 → 앱을 계층 구조로 분리
→ GUI 객체가 인수를 전달하며 비즈니스 로직 객체의 메서드를 호출 (”요청”을 보낸다고 표현)
커맨드 패턴 - GUI 객체들이 요청을 직접 보내면 안 되고, 요청의 세부 사항을 추출해 해당 요청을 작동시키는 단일 메서드를 가지는 별도의 커맨드 클래스로 분리
커맨드 객체 - GUI와 비즈니스 로직 객체들 사이 링크 역할 수행 → GUI는 무슨 비즈니스 로직 객체가 요청을 받고 요청이 어떻게 처리될 지 알 필요 없이 커맨드를 작동시키기만 하면 됨
커맨드들은 같은 인터페이스를 구현해야 함
위 예제 - 커맨드 패턴을 적용하면 버튼 서브클래스들이 필요 없음
1. 발신자 (invoker) - 요청을 시작하는 역할 수행
- 커맨드 객체에 대한 참조를 저장하는 필드 필요
- 발신자는 요청을 수신자에게 직접 보내지 않고 해당 커맨드를 작동시킴
- 발신자는 커맨드 객체를 생성할 책임이 없음
- 커맨드 객체는 일반적으로 생성자를 통해 클라이언트로부터 미리 생성됨
2. 커맨드 인터페이스 - 대개 커맨드를 실행하는 단일 메서드만을 선언
3. concrete 커맨드 - 다양한 종류의 요청 구현
- 작업을 직접 수행하지 않고 비즈니스 로직 객체에 호출을 전달해야지만 코드 단순화를 위해 통합될 수 있음
- 메서드를 실행하기 위해 필요한 매개변수는 concrete 커맨드의 필드로 선언될 수 있음
- 초기화를 생성자에게만 허용함으로써 커맨드 객체를 불변하게 만들 수 있음
4. 수신자 - 비즈니스 로직 보유
- 거의 모든 객체가 수신자가 될 수 있음
- 대부분 커맨드들은 요청이 수신자에게 전달되는 방식에 대한 세부 사항만 처리하고, 수신자가 실제 작업을 수행
5. 클라이언트 - concrete 커맨드 객체들을 생성하고 설정
- 수신자 인스턴스를 포함한 모든 요청 매개변수들을 커맨드 생성자에 전달해야 함
- 결과로 생성된 커맨드는 하나 이상의 수신자와 연결
- 커맨드 패턴은 특정 메서드 호출을 독립 실행형 객체로 만들 수 있음
→ 커맨드를 메서드의 인수로 전달하고, 다른 객체 내부에 저장하고, 연결된 커맨드를 전환하는 등 여러 작업이 가능해짐
- 다른 객체와 마찬가지로 커맨드 또한 직렬화될 수 있음
→ string으로 변환해 손쉽게 파일/DB에 넣고 커맨드 객체로 복원할 수 있음
- 커맨드 실행을 지연시키거나, 스케줄링하거나, 네트워크 상에서 대기열에 넣고, 로그하고, 전송할 수 있음
- undo/redo를 구현하는 많은 방법 중에서 커맨드 패턴이 가장 인기있음
- 커맨드 history(실행된 커맨드 객체들과 프로그램 상태 백업을 가지는 스택) 구현해야 함
- 두 가지 결점 보유
1. 프로그램의 상태 일부가 private이라 저장하기 어려울 수 있음 → 메멘토 패턴으로 완화 가능
2. 상태 백업이 RAM을 많이 소모할 수 있음
→ 이전 상태로 복원하는 대신 커맨드가 반대 작업을 실행하는 방법도 있음
→ 구현이 어렵거나 불가능할 수도 있음
1. 단일 실행 메서드로 커맨드 인터페이스 선언
2. 요청들을 커맨드 인터페이스를 구현하는 concrete 커맨드 클래스들로 추출
- 각 클래스는 요청 인수와 실제 요청 객체 참조를 저장하는 필드가 있어야 함
- 모든 값들은 커맨드의 생성자에 의해 초기화되어야 함
3. 발신자 역할을 하는 클래스들을 식별해 커맨드를 저장할 필드 추가
- 발신자는 커맨드 인터페이스를 통해서만 커맨드와 소통해야 함
- 발신자들은 커맨드 객체들을 스스로 생성하지 않고 주로 클라이언트 코드로부터 받음
4. 발신자들이 수신자들에게 직접 요청을 보내는 대신 커맨드를 작동하도록 변경
5. 클라이언트는 아래 순서대로 객체 초기화:
- 수신자 생성
- 커맨드 생성 후 필요하다면 수신자와 연결
- 발신자 생성 후 특정 커맨드들과 연결
- SRP - 작업을 유발하는 클래스들과 작업을 수행하는 클래스들을 분리할 수 있음
- OCP - 기존 클라이언트 코드를 훼손하지 않고 새 커맨드 도입 가능
- undo/redo 구현 가능
- 작업 지연 실행 구현 가능
- 단순한 커맨드들을 모아 복잡한 커맨드로 조립 가능
- 발신자와 수신자 사이에 새로운 계층이 도입돼 코드가 더 복잡해질 수 있음
- 책임 연쇄, 커맨드, 중재자, 옵저버 패턴은 요청의 발신자와 수신자를 연결하는 다양한 방법을 다룸
- 책임 연쇄 패턴 - 요청이 처리될 때까지 잠재적 수신자들로 구성된 동적 체인을 따라 전달함
- 커맨드 - 수신자와 발신자 단방향 커넥션 수립
- 중재자 - 수신자와 발신자 사이의 직접적인 연결을 제거하고 중재자 객체를 통해서만 소통하게 함
- 옵저버 - 수신자들이 동적으로 요청 수신을 구독/구독 취소할 수 있음
- 책임 연쇄 패턴의 핸들러들은 커맨드 패턴으로 구현 가능
→ 다양한 작업들을 같은 컨텍스트 객체에 대해 실행할 수 있고, 해당 객체는 요청으로 표현됨
- 요청 자체가 커맨드 객체인 경우 → 같은 연산을 체인으로 연결된 서로 다른 일련의 콘텍스트들에서 실행할 수 있음
- undo 구현 시 커맨드 패턴과 메멘토 패턴을 함께 사용할 수 있음
- 커맨드 - 대상 객체에 작업을 수행하는 역할
- 메멘토 - 커맨드가 실행되기 직전에 객체의 상태를 저장하는 역할
- 커맨드 패턴과 전략 패턴은 객체를 특정 작업으로 매개변수화할 수 있다는 점에서 비슷해 보이지만 다른 의도를 가짐
- 커맨드 - 어떤 작업이든 객체로 변환할 수 있음, 작업의 매개변수는 객체의 필드가 됨,
해당 변환은 작업 지연 실행, 작업을 대기열에 추가, 커맨드들의 history 저장,
커맨드들을 원격 서비스에 전달하는 등 기능을 가능하게 함
- 전략 - 주로 같은 일을 하는 여러 방법들을 설명해,
단일 컨텍스트 클래스 내부에서 해당 알고리즘들을 바꿔가며 사용할 수 있게 함
- 프로토타입 - 커맨드의 복제본을 history에 저장할 때 도움을 줄 수 있음
- 비지터 패턴 - 커맨드 패턴의 강력한 버전,
비지터 패턴 객체들은 작업들을 다양한 클래스들의 다양한 객체들에게 실행할 수 있음
/**
* The Command interface declares a method for executing a command.
*/
interface Command {
execute(): void;
}
/**
* Some commands can implement simple operations on their own.
*/
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})`);
}
}
/**
* However, some commands can delegate more complex operations to other objects,
* called "receivers."
*/
class ComplexCommand implements Command {
private receiver: Receiver;
/**
* Context data, required for launching the receiver's methods.
*/
private a: string;
private b: string;
/**
* Complex commands can accept one or several receiver objects along with
* any context data via the constructor.
*/
constructor(receiver: Receiver, a: string, b: string) {
this.receiver = receiver;
this.a = a;
this.b = b;
}
/**
* Commands can delegate to any methods of a 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}.)`);
}
}
/**
* The Invoker is associated with one or several commands. It sends a request to
* the command.
*/
class Invoker {
private onStart: Command;
private onFinish: Command;
/**
* Initialize commands.
*/
public setOnStart(command: Command): void {
this.onStart = command;
}
public setOnFinish(command: Command): void {
this.onFinish = command;
}
/**
* The Invoker does not depend on concrete command or receiver classes. The
* Invoker passes a request to a receiver indirectly, by executing a
* 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;
}
}
/**
* The client code can parameterize an invoker with any commands.
*/
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();
// Output.txt
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.)
참고 자료: Refactoring.guru