디자인 패턴/GoF

[디자인 패턴] 행동 패턴 (1) 책임 연쇄 Chain of Responsiblity

로파이 2021. 2. 1. 15:40

책임 연쇄 (Chain of Responsibility)

 

1. 의도

메시지를 보내는 객체와 이를 받아 처리하는 객체들 간의 결합도를 없애기 위한 패턴

요청에 대해 객체는 자신의 요청이면 처리하고 아닐시 다른 객체에게 요청을 전달한다.

 

2. 활용

 

그래픽 사용자 인터페이스(GUI)에 있는 문맥 감지 도움말 기능의 예

 

- 다이얼로그와 버튼

Visual Studio 종료시 나타나는 다이얼로그

  • 버튼은 클릭 가능한 인터페이스를 제공하고 다이얼로그는 버튼 인터페이스를 포함하고 사용자에게 메세지나 기능의 내용을 전달하는 인터페이스이다.
  • Microsoft Visual Studio 프로그램 종료 시 하면 위 예시와 같은 다이얼로그를 표시한다.
  • "저장", "저장안함", "취소" 버튼 인터페이스와 변경 내용을 띄우는 창 그리고 기본적인 텍스트가 포함된 다이얼로그라고 할 수 있다.
  • "?"의 기능을 이용해 버튼의 도움말을 알아보고자 할 때 (실제로는 도움말 사이트가 연결되지만 그런 기능이라고 가정한다.) 저장 버튼을 클릭했을 때 작은 도움말 텍스트가 나오도록 한다.
  • ex) 현재까지 진행한 작업을 저장합니다.

 

만약 "저장" 버튼에 도움말 기능이 없고 종료 프로그램이 "저장" 버튼에 해당하는 도움말 기능을 제공하고 있다면, 이 도움말 텍스트를 띄우는 작업은 "저장" 버튼이 하지 않는다. 따라서 사용자가 도움말 요청을 했다는 사실을 버튼을 통해 프로그램으로 최종 전달해야한다.

 

책임 연쇄 패턴의 아이디어는 이러한 요청 메세지를 송신과 수신으로 분리하는 것이다.  버튼은 송신자로 실제 도움말 기능을 수행하는 종료 프로그램, 수신자로 메세지를 전달하게된다.

 

송신자가 수신자를 특정하여 보내는 것이 아니라 송신자에서 연결된 다른 객체를 통해 연결을 전달하고 수신자가 받을 때까지 이 연결 고리는 계속된다.

 

- 연결 고리 관계

  • 버튼-다이얼로그-프로그램은 계층적 구조를 가진다고 말할 수 있고 다음과 같이 handler라는 상위 부모 참조자를 통해 연결 고리를 정의할 수 있다.

- 연쇄적 요청 전달

 

상호작용 다이어그램 - HandleHelp()

  • 요청을 자신이 처리 가능한지 혹은 아닐 경우 handler 객체에 요청을 다시 전달하는 인터페이스를 정의하는데, HandleHelp() 연산이 연결 인터페이스를 제공한다.
  • 사용자는 저장 버튼을 도움말 기능을 위해 "?"와 함께 선택하고 HandleHelp()를 호출하게 된다. HandleHelp()는 자신이 요청을 처리하거나 handler->HandleHelp() 연산을 재귀적으로 호출하게 되어 있다.
  • 모든 도움말 요청은 종료 프로그램에서 처리되므로 종료 프로그램까지 HandleHelp()가 전달된 것을 볼 수 있다.

- HelpHandler 클래스

  • 책임 연쇄 패턴을 구현하기 위해 다음 연결 객체의 인스턴스를 합성하고 요청을 전달하는 HandleHelp() 연산을 제공한다.
  • 이러한 연쇄 패턴을 사용하는 모든 클래스는 HelpHandler를 상속하여 구현한다.
  • 실제로 버튼 자체가 처리할 수 있는 경우 이는 버튼 서브 클래스에서 ShowHelp()로 구현함으로 처리 가능하다.

3. 참여자

  • Handler: 요청을 처리하는 인터페이스를 정의하고 후속 처리자(successor)와 연결을 구현한다.
  • ConcreteHandler: 책임져야 할 행동이 있다면 스스로 요청을 처리하고 그렇지 않으면 후속 처리자에게 처리를 요청한다.
  • Client: ConcreteHandler 객체에 필요한 요청을 보낸다.

4. 결과

  • 서로 연결된 객체는 어떤 요청을 처리하는 지 모르기 때문에 결합도가 적어진다.
  • 객체 책임을 분산시킬 수 있으므로 런타임에 연결 고리를 변경하거나 추가하여 책임을 변경, 확장 가능하다.
  • 요청에 대한 메세지 처리가 보장이 되지 않을 수 있다.

5. 구현

 

1) 연결 고리 구현

  • 생성 당시 연결할 객체를 전달하거나 동적으로 연결 객체를 할당, 변경 가능하다. 또한 기존 연결고리가 있는 객체를 후속 처리자로 만들 수 도 있다.

2) 후속 처리자 연결

  • HelpHandler 클래스는 요청 처리 인터페이스와 연결 객체의 인스턴스(후속 처리자)를 가지고 있어야한다.

- HelpHandler의 기본 인터페이스 예시

class HelpHandler {
public:
	HelpHandler(HelpHandler* s) : _successor(s){}
	virtual void HandleHelp();
private:
	HelpHandler* _successor;
};
void HelpHandler::HandleHelp()
{
	if (_successor)
	{
		_successor->HandleHelp();
	}
}

3) 처리 요청의 표현을 정의

  • 메세지가 자신이 처리 가능한 지 판별하기 위해 반드시 메세지 식별자를 매개변수로 전달받아 요청을 처리하도록 한다.
  • 메세지 식별자는 모든 송신자와 수신자에게 약속된 규약이 있어야한다.

ExtendedHandler는 _successor를 Handler 인스턴스로 저장하고 자신의 요청이 아닐시 요청을 전달한다.

class Handler
{
public:
	virtual void HandleRequest(Request*);
};

void Handler::HandleRequest(Request* theRequest)
{
	switch (theRequest->GetKind())
	{
	case Help:
		HandleHelp((HelpRequest)*theRequest);
		break;
	case Print:
		HandlePrint((PrintRequest*)theRequest);
		break;
	default:
		break;
	}
}

class ExtendedHandler : public Handler
{
	virtual void HandleRequset(Request* theRequest);
};

void ExtendedHandler::HandleRequest(Request* theRequest)
{
	switch (theRequest->GetKind())
	{
	// 자신의 요청
	case Preview:
		// 요청 처리
		HandlePreview((PreviewRequest*) theRequest);
		break;
	default:
    	// ExtendedHandler의 후속 처리자에 요청 전달
		// _successor->HandleRequest(theRequest);
		Handler::HandleRequest(theRequest);
	}
}
  • 기본 Handler 클래스와 이를 상속하여 다른 요청을 처리하는 ExtendedHandler 클래스를 정의할 수 있다.
  • 예시로서 매개 변수로 메세지 식별을 한다.

 

예제 코드

 

HelpHandler.h

HelpHandler 클래스는 도움말 요청을 처리하는 인터페이스를 정의한다.

- 생성자: 후속 처리자에 대한 참조자와 자신이 관리하는 요청 번호를 매개변수로 받는다.

- HasHelp(): 자신이 처리하는 요청인지 확인한다.

- SetHandler(): 후속 처리자를 동적으로 변경하거나 추가하고, 요청 번호를 할당한다.

- HandleHelp(): 가장 중요한 연산으로 실제 처리 가능 여부를 판별하고 다음 후속자에게 전달하는 인터페이스를 제공한다. 서브 클래스는 이 연산을 재정의하여 사용해야한다.

typedef int Topic;
const Topic NO_HELP_TOPIC = -1;

class HelpHandler
{
public:
	HelpHandler(HelpHandler* = 0, Topic = NO_HELP_TOPIC);
	virtual bool HasHelp();
	virtual void SetHandler(HelpHandler*, Topic);
	virtual void HandleHelp();
private:
	HelpHandler* _successor;
	Topic _topic;
};

HelpHandler::HelpHandler(
	HelpHandler* h, Topic t)
	: _successor(h), _topic(t) {}

bool HelpHandler::HasHelp()
{
	return _topic != NO_HELP_TOPIC;
}

void HelpHandler::SetHandler(HelpHandler* h, Topic t)
{
	if (_successor) return;
	_successor = h;
	_topic = t;
}

void HelpHandler::HandleHelp()
{
	if (_successor != 0)
		_successor->HandleHelp();
}

 

Widget.h

Widget은 HelpHandler를 상속하고 Widget을 대표하는 추상 클래스이다.

거의 똑같은 인터페이스이지만 Widget 참조자를 관리하여 후속 처리자가 Widget인 것을 명시할 수 있다.

 

Widget을 상속하여, Button과 Dialog를 만들 수 있고

Button 클래스는 후속 처리자가 Widget만 온다고 가정하여 부모 Widget 생성자를 이용하고

Dialog 클래스는 후속 처리자가 임의의 HelpHandler가 오기 때문에 Widget 생성자를 이용하지 않고 SetHandler 메서드를 이용한다. (부모의 부모 클래스 HelpHandler 생성자는 비가상 메서드 취급이 되기 때문에 호출할 수 없음) 

class Widget : public HelpHandler
{
protected:
	Widget(Widget* parent, Topic t = NO_HELP_TOPIC);
private:
	Widget* _parent;
};

Widget::Widget(Widget* w, Topic t) : HelpHandler(w, t)
{
	_parent = w;
}

class Button : public Widget {
public:
	// 자신을 포함하는 Widget 인스턴스
	Button(Widget* d, Topic t = NO_HELP_TOPIC);

	virtual void HandleHelp();
};

Button::Button (Widget* h, Topic t) : Widget(h, t){}

// Button에 대한 요청 처리
void Button::HandleHelp()
{
	// 자신의 요청인지 확인
	if (HasHelp()){
		// my answer to request
	}
	else {
		// _successor->HandleHelp();
		HelpHandler::HandleHelp();
	}
}

class Dialog : public Widget {
public:
	Dialog(HelpHandler* h, Topic t = NO_HELP_TOPIC);
	virtual void HandleHelp();
};

Dialog::Dialog(HelpHandler* h, Topic t) : Widget(0)
{
	SetHandler(h, t);
}

void Dialog::HandleHelp()
{
	if (HasHelp()){

	}
	else {
		HelpHandler::HandleHelp();
	}
}

 

Application.h

후속 처리자가 없는 연결 끝 단계에서 나머지 모든 요청을 처리한다.

class Application : public HelpHandler
{
public:
	Application(Topic t) : HelpHandler(0, t) {}
	virtual void HandleHelp();
};

void Application::HandleHelp()
{
	// 기본 도움말 항목 리스트를 표시
}

 

실제 사용자 코드

위 구현에서는 Button이 직접 도움말을 제공하게 된다.

const Topic PRINT_TOPIC = 1;
const Topic PAPER_ORIENTATION_TOPIC = 2;
const Topic APPLICATION_TOPIC = 3;

Application* application = new Application(APPLICATION_TOPIC);
Dialog* dialog = new Dialog(application, PRINT_TOPIC);
Button* button = new Button(dialog, PAPER_ORIENTATION_TOPIC);

// 버튼에서 도움말 요청
button->HandleHelp();