Chain of Responsibility는 일련의 핸들러를 따라 요청을 전달할 수 있는 행동 디자인 패턴이다. 요청을 받으면 각 핸들러는 요청을 처리할지 아니면 체인의 다음 핸들러로 전달할지 결정한다.
온라인 주문 시스템에서 작업하고 있다고 상상해보자. 인증된 사용자만 주문을 생성할 수 있도록 시스템에 대ㄴ 액세스를 제한하려고 한다. 또한 관리 권한이 있는 사용자는 모든 주문에 대한 전체 액세스 권한이 있어야 한다.
약간의 계약 후에 이러한 검사를 순차적으로 수행해야 한다는 것을 깨달았다. 애플리케이션은 사용자의 자격 증명이 포함된 요청을 수신할 때마다 시스템에 대해 사용자 인증을 시도할 수 있다. 그러나 이러한 자격 증명이 올바르지 않고 인증에 실패하면 다른 검사를 진행할 이유가 없다.
다음 몇 달 동난 이러한 순차 검사 중 몇 가지를 더 구현했다.
동료 중 한 명이 원시 데이터를 주문 시스템에 직접 전달하는 것은 안전하지 않다고 제안했다. 따라서 요청의 데이터를 삭제하기 위해 추가 유효성 검사 단계를 추가했다.
나중에 누군가는 시스템이 무차별 암호 해독에 취약하다는 것을 알아차렸다. 이를 무효화하기 위해 동일한 IP 주소에서 오는 반복적인 실패한 요청을 필터링하는 검사를 즉시 추가했다.
다른 누군가는 동일한 데이터가 포함된 반복 요청에 대해 캐시된 결과를 반환하여 시스템 속도를 높일 수 있다고 제안했다. 따라서 적절한 캐시 응답이 없는 경우에만 요청이 시스템으로 전달되도록 하는 또 다른 검사를 추가했다.
이미 엉망인 것처럼 보였던 검사 코드는 새로운 기능을 추가할 때마다 점점 더 부풀려졌다. 하나의 검사를 변경하면 때때로 다른 검사에 영향을 준다. 무엇보다도 시스템의 다른 구성 요소를 보호하기 위해 검사를 재사용하려고 할 때 해당 구성 요소에 일부 검사가 필요하지만 전부는 아니기 때문에 일부 코드를 복제해야 했다.
시스템은 이해하기 매우 어렵고 유지 관리 비용이 많이 든다. 당신은 어느 날 전체를 리팩토링하기로 결정할 때까지 한동안 코드와 씨름했다.
다른 많은 Behavioral design pattern과 마찬가지로 Chain of Responsibility는 특정 행동을 핸들러라고 하는 독립 실행형 개체로 변환하는데 의존한다. 우리의 경우 각 검사는 검사를 수행하는 단일 메서드를 사용하여 자체 클래스로 추출하여야 한다. 요청은 데이터와 함께 이 메서드에 인수로 전달된다.
패턴은 이러한 핸들러를 체인으로 연결하도록 제안한다. 연결된 각 핸들러에는 체인의 다음 핸들러에 대한 참조를 저장하기 위한 필드가 있다. 요청을 처리하는 것 외에도 핸들러는 체인을 따라 요청을 더 전달한다. 요청은 모든 핸들러가 처리할 기회를 가질 때까지 체인을 따라 이동한다.
가장 좋은 부분은 다음과 같다. 핸들러는 요청을 더이상 체인 아래로 전달하지 않고 추가 처리를 효과적으로 중지할 수 있다.
순서 지정 시스템이 있는 우리의 예에서 핸들러는 처리를 수행한 다음 요청을 체인 아래로 더 전달할지 여부를 결정한다. 요청에 올바른 데이터가 포함되어 있다고 가정하면 모든 핸들러는 인증 확인이든 캐싱이든 동작을 실행할 수 있다.
그러나 요청을 수신하면 처리하기가 처리할 수 있는지 여부를 결정하는 약간 다른 접근 방식이 있다(and it's a bit more canonical).가능한 경우 더 이상 요청을 전달하지 않는다. 따라서 요청을 처리하는 핸들러는 하나뿐이거나 전혀 처리하지 않는다. 이 접근 방식은 그래픽 사용자 인터페이스내에서 요소 스택의 이벤트를 처리할 때 매우 일반적이다.
예를 들어, 사용자가 버튼을 클릭하면 이벤트는 버튼으로 시작하여 해당 컨테이너(예: 양식 또는 패널)를 따라 이동하고 메인 애플리메이션 창으로 끝나는 GUI 요소 체인을 통해 전파된다. 이벤트는 처리할 수 있는 체인의 첫 번째 요소에 의해 처리된다. 이 예는 체인이 항상 개체 트리에서 추출될 수 있음을 보여주기 때문에 주목할 만하다.
모든 핸들러 클래스가 동일한 인터페이스를 구현하는 것이 중요하다. 각 구체적인 핸들러는 execute
메서드가 있는 다음 핸들러에만 신경을 써야 한다. 이렇게 하면 코드를 구체적인 클래스에 연결하지 않고도 다양한 핸들러를 사용하여 런타임에 체인을 구성할 수 있다.
컴퓨터에 새 하드웨어를 구입하여 설치했다. 당신은 괴짜이기 때문에 컴퓨터에는 여러 운영 체제가 설치되어 있다. 하드웨어가 지원되는지 확인하기 위해 모두 부팅을 시도한다. Windows는 하드웨어를 자동으로 감지하고 활성화 한다. 그러나 사랑하는 Linux는 하드웨어로 작업하는 것을 거부한다. 작은 희망을 품고 상자에 적힌 기술 지원 전화번호로 전화하기로 결정한다.
가장 먼저 들리는 것은 자동 응답기의 로봇 음성이다. 다양한 문제에 대한 9가지 인기 있는 솔루션을 제안하지만 그 중 어느 것도 귀하의 사례와 관련이 없다. 잠시후 로봇이 당신을 라이브 교환원에게 연결한다.
Alas, 교환원도 구체적인 제안을 할 수 없다. 그는 매뉴얼에서 긴 발췌문을 계속 인용하며 당신의 의견을 듣지 않는다. "컴퓨터를 껐다가 다시 켜보셨습니까?"라는 문구를 듣고 10번째로, 단신은 적절한 엔지니어에게 연결을 요구한다.
결국 교환원은 사무실 건물의 어두운 지하실에 있는 외로운 서비실에 앉아 몇 시간 동안 실시간 채팅을 갈망했던 엔지니어 중 한 명에게 전화를 전달한다. 엔지니어는 새 하드웨어에 적합한 드라이버를 다운로드할 위치와 Linux에 설치하는 방법을 알려준다. 마지막으로 솔루션!기쁨에 넘쳐 통화를 종료한다.
1. Handler는 모든 구체적인 핸들러에 공통적인 인터페이스를 선언한다. 일반적으로 요청을 처리하는 단일 메서드만 포함하지만 때로는 체인에서 다음 핸들러를 설정하기 위한 다른 메서드가 있을 수도 있다.
2. Base Handler는 모든 핸들러 클래스에 공통적인 상용구 코드를 넣을 수 있는 선택적 클래스다.
일반적으로 이 클래스는 다음 핸들러에 대한 참조를 저장하기 위한 필드를 정의한다. 클라이언트는 핸들러를 이전 핸들러의 생성자 또는 설정자에 전달하여 체인을 구축할 수 있다. 클래스는 기본 처리 동작을 구현할 수도 있다. 즉, 존재 여부를 확인한 후 다음 처리기로 실행을 전달할 수 있다.
3. Concrete Handler에는 요청을 처리하기 위한 실제 코드가 포함되어 있다. 요청을 수신하면 각 핸들러는 요청을 처리할지 여부와 함께 체인을 따라 전달할지 여부를 결정해야 한다.
핸들러는 일반적으로 자체 포함되고 변경할 수 없으며 생성자를 통해 필요한 모든 데이터를 한 번만 수락한다.
4. Client는 응용 프로그램의 논리에 따라 체인을 한 번만 구성하거나 동적으로 구성할 수 있다. 요청은 체인의 모든 핸들러로 보낼 수 있다. 첫 번째 핸들러일 필요는 없다.
이 예에서 Chain of Responsibility 패턴은 활성 GUI 요소에 대한 상황별 도움말 정보를 표시하는 역할을 한다.
응용 프로그램의 GUI는 일반적으로 개체 트리로 구성된다. 예를 들어 앱의 기본 창을 렌더링하는 Dialog 클래스는 개체 트리의 루트가 된다. 대화 상자에는 다른 Panel
이나 Button
및 TextField
와 같은 간단한 하위 수준 요소가 포함될 수 있는 패널이 포함되어 있다.
간단한 구성 요소는 구성 요소에 일부 도움말 텍스트가 할당되어 있는 한 간단한 상황별 도구 설명을 표시할 수 있다. 그러나 더 복잡한 구성 요소는 설명서에서 발췌한 내용을 표시하거나 브라우저에서 페이지를 여는 것과 같이 상황에 맞는 도움말을 표시하는 고유한 방법을 정의한다.
사용자가 요소에 마우스 커서를 놓고 F1
키를 누르면 응용 프로그램이 포인터 아래의 구성 요소를 감지하고 도움말 요청을 보낸다. 요청은 도움말 정보를 표시할 수 있는 요소에 도달할 때까지 모든 요소의 컨테이너를 통해 버블링 된다.
// The handler interface declares a method for building a chain
// of handlers. It also declares a method for executing a
// request.
interface ComponentWithContextualHelp is
method showHelp()
// The base class for simple components.
abstract class Component implements ComponentWithContextualHelp is
field tooltipText: string
// The component's container acts as the next link in the
// chain of handlers.
protected field container: Container
// The component shows a tooltip if there's help text
// assigned to it. Otherwise it forwards the call to the
// container, if it exists.
method showHelp() is
if (tooltipText != null)
// Show tooltip.
else
container.showHelp()
// Containers can contain both simple components and other
// containers as children. The chain relationships are
// established here. The class inherits showHelp behavior from
// its parent.
abstract class Container extends Component is
protected field children: array of Component
method add(child) is
children.add(child)
child.container = this
// Primitive components may be fine with default help
// implementation...
class Button extends Component is
// ...
// But complex components may override the default
// implementation. If the help text can't be provided in a new
// way, the component can always call the base implementation
// (see Component class).
class Panel extends Container is
field modalHelpText: string
method showHelp() is
if (modalHelpText != null)
// Show a modal window with the help text.
else
super.showHelp()
// ...same as above...
class Dialog extends Container is
field wikiPageURL: string
method showHelp() is
if (wikiPageURL != null)
// Open the wiki help page.
else
super.showHelp()
// Client code.
class Application is
// Every application configures the chain differently.
method createUI() is
dialog = new Dialog("Budget Reports")
dialog.wikiPageURL = "http://..."
panel = new Panel(0, 0, 400, 800)
panel.modalHelpText = "This panel does..."
ok = new Button(250, 760, 50, 20, "OK")
ok.tooltipText = "This is an OK button that..."
cancel = new Button(320, 760, 50, 20, "Cancel")
// ...
panel.add(ok)
panel.add(cancel)
dialog.add(panel)
// Imagine what happens here.
method onF1KeyPress() is
component = this.getComponentAtMouseCoords()
component.showHelp()
프로그램이 다양한 방식으로 다양한 종류의 요청을 처리할 것으로 예상되지만 정확한 요청 유형과 순서를 미리 알 수 없는 경우 책임 사슬 패턴을 사용한다.
이 패턴을 사용하면 여러 핸들러를 하나의 체인으로 연결할 수 있으며 요청을 받으면 각 핸들러가 처리할 수 있는지 "요청"한다. 이런 식으로 모든 핸들러는 요청을 처리할 기회를 얻는다.
특정 순서로 여러 핸들러를 실행해야 하는 경우 패턴을 사용합니다.
어떤 순서로든 체인의 핸들러를 연결할 수 있으므로 모든 요청은 계획한 대로 정확히 체인을 통과한다.
핸들러 세트와 그 순서가 런타임에 변경되어야 하는 경우 CoR 패턴을 사용하십시오.
핸들러 클래스 내부의 참조 필드에 대한 세터를 제공하면 핸들러를 동적으로 삽입, 제거 또는 재정렬할 수 있다.
O. 요청 처리 순서를 제어할 수 있다.
O. 단일 책임 원칙. 작업을 수행하는 클래스에서 작업을 호출하는 클래스를 분리할 수 있다.
X. 개방/폐쇄 원칙. 기존 클라이언트 코드를 손상시키지 않고 앱에 새 핸들러를 도입할 수 있다.