핸들러의 체인을 따라 요청을 보낼 수 있게 해주는 행동 패턴
각 핸들러는 요청을 받으면 요청을 처리할 지 체인의 다음 핸들러에게 넘길지 결정
온라인 주문 시스템 가정
검사가 순차적으로 진행되어야 함 → 애플리케이션은 유저의 자격 증명을 포함한 요청을 받으면 인증 시도를 할 수 있지만, 자격 증명이 올바르지 않아 인증이 실패하면 다른 검사를 할 필요가 없어짐
순차적 검사들을 몇 가지 더 구현:
검사 코드들은 새로운 기능이 추가될 때마다 더 비대해짐
한 검사를 변경하면 다른 검사에도 영향을 미침
다른 컴포넌트에서 검사를 재사용하려 할 때, 일부분의 검사만 필요하기 때문에 코드의 일부분만을 복사해서 사용해야만 함
→ 시스템이 복잡해지고 유지보수가 어려워져서 리팩토링을 하기로 결심
책임 연쇄 패턴은 특정 행동들을 핸들러라는 독립 객체로 변경
위 예제에서 각 검사는 검사를 수행하는 단일 메서드를 가지는 독립적인 클래스로 추출될 수 있음
책임 연쇄 패턴은 핸들러들을 체인으로 연결해 각 핸들러가 다음 핸들러에 대한 참조를 저장하는 필드를 가짐
더 정석적인 방법 - 핸들러가 요청을 받으면 요청을 처리할 수 있는지 결정하고, 할 수 있다면 요청을 더 멀리 보내지 않음 → 단 하나의 핸들러만 요청을 처리하거나, 아무 핸들러도 처리하지 않음
모든 핸들러 클래스들이 같은 인터페이스를 구현하는 게 중요
execute
메서드를 가진 다음 핸들러만 신경 씀 → 코드를 concrete 클래스들에 결합하지 않고 다양한 핸들러를 사용해 런타임에 체인을 구성할 수 있게 됨1. 핸들러 - 모든 concrete 핸들러들에게 공통되는 인터페이스 선언
- 주로 요청을 처리하는 단일 메서드만을 가지지만, 가끔 체인의 다음 핸들러를 설정하는 메서드를 가질 수도 있음
2. (optional) 기초 핸들러 - 모든 핸들러 클래스들에게 공통된 boilerplate 코드를 넣을 수 있음
- 주로 다음 핸들러에 대한 참조를 저장하는 필드를 정의
- 클라이언트는 핸들러를 이전 핸들러의 생성자 또는 setter에게 전달해 체인을 만들 수 있음
- 다음 핸들러의 존재 여부를 확인한 후 다음 핸들러로 실행을 넘기는 디폴트 핸들링 행동을 구현할 수도 있음
3. concrete 핸들러 - 요청을 처리하는 실제 코드를 포함
- 각 핸들러는 요청을 받으면 처리할 것인지, 그리고 체인을 따라 요청을 전달할 것인지 결정해야 함
4. 클라이언트 - 애플리케이션의 로직에 따라 체인을 단 한 번 구성할 수도 있고 동적으로 구성할 수도 있음
- 요청은 체인의 첫번째 핸들러 뿐만 아니라 어떤 핸들러로도 전송될 수 있음
- 책임 연쇄 패턴은 여러 핸들러를 체인으로 연결해, 요청을 받으면 각 핸들러에게 요청을 처리할 수 있는지 물음
→ 모든 핸들러들에게 요청을 처리할 기회가 주어짐
- 핸들러를 어떤 순서로든 연결할 수 있기 때문에, 모든 요청은 계획된 대로 체인을 지남
- 핸들러 클래스들 내부 참조 필드에 setters를 제공하면 핸들러들을 동적으로 삽입, 삭제, 재배열할 수 있음
1. 핸들러 인터페이스를 선언하고 요청 처리 메서드의 시그니처 설명
- 클라이언트가 어떻게 메서드에 데이터를 전달할 것인지 결정
→ 가장 유연한 방법은 요청을 객체로 변환해 핸들링 메서드에 매개변수로 전달하는 것
2. concrete 핸들러들의 중복된 boilerplate 코드를 없애기 위해,
핸들러 인터페이스로부터 파생된 추상 기초 핸들러 클래스 생성 고려
- 해당 클래스는 체인의 다음 핸들러에 대한 참조를 저장하는 필드 필요 (불변 클래스 고려)
- 체인을 런타임에 변경하려면 참조 필드의 값을 변경하는 setter를 정의해야 함
- 다음 객체가 있으면 요청을 다음으로 보내는 디폴트 핸들링 메서드를 구현할 수도 있음
→ concrete 핸들러들이 부모 메서드를 호출함으로써 사용할 수 있게 됨
3. 핸들러 서브클래스들을 만들고 핸들링 메서드를 구현, 각 핸들러는 요청을 받으면 두 가지 결정을 해야 함
- 요청을 처리할 것인지
- 체인을 따라 요청을 전달할 것인지
4. 클라이언트는 직접 체인을 조립할 수도, 다른 객체로부터 미리 만들어진 체인을 받을 수도 있음
- 후자라면 설정된 환경에 따라 체인을 만드는 팩토리 클래스들을 구현해야 함
5. 클라이언트는 첫번째 핸들러 뿐만 아니라 체인에 있는 어떤 핸들러든 사용 가능
- 요청은 체인의 끝에 도달하거나 핸들러가 더 이상 요청을 전달하지 않을 때까지 체인을 따라 전달됨
6. 체인의 동적 특성으로 인해 클라이언트는 다음 시나리오를 대비해야 함:
- 체인이 단일 링크로 구성될 수 있음
- 요청이 체인 끝에 도달하지 않을 수 있음
- 요청이 처리되지 않은 상태로 체인 끝에 도달할 수 있음
- 요청 처리 순서를 제어할 수 있음
- SRP - 행동을 실행하는 클래스들과 행동을 유발하는 클래스들을 분리할 수 있음
- OCP - 기존 클라이언트 코드를 훼손하지 않고 새 핸들러를 도입할 수 있음
- 일부 요청이 처리되지 않을 수 있음
- 책임 연쇄, 커맨드, 중재자, 옵저버 패턴은 요청의 발신자와 수신자를 연결하는 다양한 방법을 다룸
- 책임 연쇄 패턴 - 요청이 처리될 때까지 잠재적 수신자들로 구성된 동적 체인을 따라 전달함
- 커맨드 - 수신자와 발신자 단방향 커넥션 수립
- 중재자 - 수신자와 발신자 사이의 직접적인 연결을 제거하고 중재자 객체를 통해서만 소통하게 함
- 옵저버 - 수신자들이 동적으로 요청 수신을 구독/구독 취소할 수 있음
- 책임 연쇄 패턴 - 종종 컴포지트 패턴과 함께 사용
- 리프 요소가 요청을 받으면, 객체 트리의 루트까지 연쇄적으로 전달
- 책임 연쇄 패턴의 핸들러들은 커맨드 패턴으로 구현 가능
→ 다양한 작업들을 같은 컨텍스트 객체에 대해 실행할 수 있고, 해당 객체는 요청으로 표현됨
- 요청 자체가 커맨드 객체인 경우 → 같은 연산을 체인으로 연결된 서로 다른 일련의 콘텍스트들에서 실행할 수 있음
- 책임 연쇄 패턴과 데코레이터 패턴은 비슷한 클래스 구조를 가지지만 차이점들 존재
- 둘 다 재귀적 합성에 의존해 일련의 객체에 실행 전달
- 책임 연쇄 핸들러 - 독립적으로 임의의 작업을 실행할 수 있고, 아무 때나 요청 전달을 멈출 수 있음
- 데코레이터 - 기본 인터페이스를 유지하며 객체의 행위를 확장할 수 있고, 요청의 흐름을 끊을 수 없음
/**
* The Handler interface declares a method for building the chain of handlers.
* It also declares a method for executing a request.
*/
interface Handler {
setNext(handler: Handler): Handler;
handle(request: string): string;
}
/**
* The default chaining behavior can be implemented inside a base handler class.
*/
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;
}
}
/**
* All Concrete Handlers either handle a request or pass it to the next handler
* in the chain.
*/
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);
}
}
/**
* The client code is usually suited to work with a single handler. In most
* cases, it is not even aware that the handler is part of a chain.
*/
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.`);
}
}
}
/**
* The other part of the client code constructs the actual chain.
*/
const monkey = new MonkeyHandler();
const squirrel = new SquirrelHandler();
const dog = new DogHandler();
monkey.setNext(squirrel).setNext(dog);
/**
* The client should be able to send a request to any handler, not just the
* first one in the chain.
*/
console.log('Chain: Monkey > Squirrel > Dog\n');
clientCode(monkey);
console.log('');
console.log('Subchain: Squirrel > Dog\n');
clientCode(squirrel);
// Output.txt
Chain: Monkey > Squirrel > Dog
Client: Who wants a Nut?
Squirrel: I'll eat the Nut.
Client: Who wants a Banana?
Monkey: I'll eat the Banana.
Client: Who wants a Cup of coffee?
Cup of coffee was left untouched.
Subchain: Squirrel > Dog
Client: Who wants a Nut?
Squirrel: I'll eat the Nut.
Client: Who wants a Banana?
Banana was left untouched.
Client: Who wants a Cup of coffee?
Cup of coffee was left untouched.
참고 자료: Refactoring.guru