의도

  • 요청 자체를 캡슐화하는 것. 이를 통해 요청이 서로 다른 사용자를 매개변수로 만들고, 요청을 대기시키거나 로깅하며, 되돌릴 수 있는 연산을 지원함.

 

다른 이름

  • 작동(Action), 트랜잭션(Transaction)

 

언제 쓰는가 ?

  • 수행할 동작을 객체로 매개변수화하고자 할 때.
    • 절차지향 프로그램에서는 이를 콜백 함수, 즉 어딘가 등록되었다가 나중에 호출되는 함수를 사용해서 이러한 매개변수화를 표현할 수 있음. 명령 패턴은 콜백을 객체지향 방식으로 나타낸 것.
  • 서로 다른 시간에 요청을 명시하고, 저장하며, 실행하고 싶을 때.
    • Command 객체는 원래의 요청과 다른 생명주기가 있음. 요청을 받아 처리하는 객체가 주소 지정 방식과는 독립적으로 표현될 수 있다면, Command 객체를 다른 프로세스에게 넘겨주고 거기서 해당 처리를 진행하게 할 수 있음.
  • 실행 취소 기능을 지원하고 싶을 때.
    • Command의 Execute() 연산은 상태를 저장할 수 있는데, 이를 이용해서 지금까지 얻은 결과를 바꿀 수 있음. 이를 위해 Unexecute()연산을 Command 클래스의 인터페이스에 추가함. 실행된 명령어를 모두 기록해 두었다가 이 리스트를 역으로 탐색해서 Unexecute 가능.
  • 시스템이 고장 났을 때 재적용이 가능하도록 변경 과정에 대한 로깅을 지원하고 싶을 때.
    • Command 인터페이스를 확장해서 load()와 store() 연산을 정의하면 상태의 변화를 지속적으로 저장소에 저장해 둘 수 있음. 시스템 장애가 발생했을 때 해당 저장소에서 저장된 명령어를 읽어 다시 Execute() 연산을 통해 재실행하면 됨.
  • 기본적인 연산의 조합으로 만든 상위 수준 연산을 써서 시스템을 구조화하고 싶을 때.
    • 정보 시스템의 일반적인 특성을 트랜잭션을 처리해야 한다는 것. 트랜재션은 일련의 과정을 통해 데이터를 변경하는 것인데, Command 패턴은 이런 트랜잭션의 모델링을 가능하게 함. Command 클래스는 일관된 인터페이스를 정의하는데, 이로써 모든 트랜잭션이 동일한 방식으로 호출됨. 새로운 트랜잭션을 만들면 상속으로 command 클래스를 확장하면 되므로 시스템 확장도 어렵지 않음.

 

구조

GoF의 디자인 패턴 중 P.315

  • Command : 연산 수행에 필요한 인터페이스를 선언함.
  • ConcreteCommand : Receiver 객체와 액션 간의 연결성을 정의함. 또한 처리 객체에 정의된 연산을 호출하도록 Execute를 구현함.
  • Client : ConcreteCommand 객체를 생성하고 처리 객체로 정의함.
  • Invoker : 명령어에 처리를 수행할 것을 요청함.
  • Receiver : 요청에 관련된 연산 수행 방법을 알고 있음. 어떤 클래스도 요청 수신자로서 동작할 수 있음.

 

협렵 방법

  • 사용자는 ConcreteCommand 객체를 생성하고 이를 수신자로 지정함
  • Invoker 클래스는 ConcreteCommand 객체를 저장함
  • Invoker 클래스는 command에 정의된 Execute()를 호출하여 요청을 발생시킴. 명령어가 취소 가능한 것이라면 ConcreteCommand는 이전에 Execute() 호출 전 상태의 취소 처리를 위해 저장함.
  • ConcreCommand 객체는 요청을 실제 처리할 객체에 정의된 연산을 호출함.

 

결과

  1. Command는 연산을 호출하는 객체와 연산 수행 방법을 구현하는 객체를 분리
  2. Command는 일급 클래스. 다른 객체와 같은 방식으로 조작되고 확장될 수 있음
  3. 명령을 여러 개를 복합해서 복합 명령을 만들 수 있음
  4. 새로운 Command 객체를 추가하기 쉬움. 기존 클래스를 변경할 필요 없이 새로운 명령어에 대응하는 클래스만 정의하면 됨

 

구현 / 고려 사항

  1. 명령이 얼마나 지능적이어야 할까?
    1. 명령 패턴을 수신 객체를 동적으로 발견할 수 있는 능력 정도는 지녀야 함
  2. 취소 및 반복 연산 지원하기
    1. 명령어가 지금까기 수행해 온 것을 뒤집는 방법을 제공한다면 가능한 일 ( 수신 객체, 매개 변수, 연산 )
  3. 취소를 진행하는 도중 오류가 누적되는 것 피하기
    1. 취소 시 원래 상태로 복귀했는지 확인하는 작업이 필요함
    2. 명령어가 다른 객체의 내부를 노출하지 않은 채 객체의 수행 결과에 대한 정보에 접근하여 확인할 수 있도록 메멘토 패턴 적용 가능
  4. C++ 템플릿 사용하기

 

예제 코드 (1)

  • fstream 객체를 이용해서 파일 입출력에 대해  1) 파일 열기 2) 파일에 쓰기 3) 파일 닫기 의 명령 3가지에 대해 Command 클래스로 작성해봤습니다.
#include "Command1.h"
#include <fstream>

int main()
{
	std::fstream file_stream;

	OpenCommand open_command(&file_stream, "HelloWorld");
	WriteCommand write_command(&file_stream);
	CloseCommand close_command(&file_stream);

	open_command.Execute();

	write_command.SetComment("HelloWorld!!");
	write_command.Execute();

	close_command.Execute();

	return 0;
}

  • 사실 fstream 객체 자체가 파일 열기, 쓰기, 닫기에 대해 쓰기 쉽게 제공하는 메서드가 있습니다. 하지만 Command 클래스를 이용해서 얻을 수 있는 이득은 redo, undo 기능을 추가하기 쉽고 명령이라는 하나의 공통 행동으로 묶을 수 있다는 점입니다.
#pragma once
#include <string>
#include <fstream>

__interface Command
{
	void Execute();
};

class OpenCommand : public Command
{
public:
	OpenCommand(std::fstream* file_stream, std::string file_name) : file_stream(file_stream), file_name(file_name) {}

	virtual void Execute() override;

private:
	std::string file_name;
	std::fstream* file_stream;
};

class WriteCommand : public Command
{
public:
	WriteCommand(std::fstream* file_stream) : file_stream(file_stream) {}

	virtual void Execute() override;
	void SetComment(std::string comment);

private:
	std::string comment;
	std::fstream* file_stream;
};

class CloseCommand : public Command
{
public:
	CloseCommand(std::fstream* file_stream) : file_stream(file_stream) {}

	virtual void Execute() override;

private:
	std::fstream* file_stream;
};
  • 각각의 클래스는 Command 인터페이스를 상속받아 Execute() 연산을 재정의해야 하며, 이 때 Command의 성격에 맞게 추가적인 변수나 메서드를 가질 수 있습니다.
    • OpenCommand의 경우 Open할 파일의 이름을 받게 되고
    • WriteCommand의 경우 Write할 Comment를 받게 됩니다.
  • 기본적으로 fstream의 경우 외부에서 제공해주는 형태로 사용하려고 포인터 형태로 받았습니다.
#include "Command1.h"
#include <cassert>
#include <filesystem>
#include <iostream>

void OpenCommand::Execute()
{
	file_stream->open(file_name, std::ios::out | std::ios::app);
	
	if (file_stream->fail())
	{
		assert(false && "Fail File Openning!!");
	}
}

void WriteCommand::Execute()
{
	if (comment.empty() == false && file_stream->is_open())
	{
		file_stream->write(comment.c_str(), comment.size());
		comment.clear();
	}
}

void WriteCommand::SetComment(std::string comment)
{
	this->comment = comment;
}

void CloseCommand::Execute()
{
	file_stream->close();
}
  • OpenCommand의 Execute의 경우 쓰기 모드(std::ios::out)로 열게 되며, 기존 파일에 이어 쓰는 옵션(std::ios::app)을 주었습니다. 열기에 실패했을 경우 프로그램을 중단할 수 있게끔 assert 구문 사용했습니다.
  • WriteCommand의 경우 Comment가 유효하고 파일이 열려있는 경우에만 명령을 수행하게 해주었습니다. 명령을 수행한 뒤에는 다시 Comment를 채워주어야 합니다.

 

예제 코드 (2)

  • 책에 있는 예제 중에 void() 시그니처를 가지는 멤버함수에 대해 Command에 적용하여 사용할 수 있는 방법이 있기에 예제 작성해봅니다.
  • 책에선 raw한 함수 포인터를 typedef으로 별명 지어서 사용했는데 std::function 사용과 using으로 함수 포인터 별명 짓는 방법도 소개합니다.
#include "Command2.h"

int main()
{
	CommandTest command_test;
	SimpleCommand<CommandTest> simple_command(&command_test, &CommandTest::HelloWorld);

	simple_command.Execute();

	return 0;
}

  • SimpleCommand 클래스가 생성자에서 1) 누구 2) 어떤 멤버 함수를 부를지 인자로 받습니다.
  • simple_command를 Execute하면 1)누구의 2) 어떤 멤버 함수가 호출됩니다.
//Command2.h

#pragma once
#include <functional>
#include <iostream>

__interface Command
{
	void Execute();
};

class CommandTest
{
public:
	void HelloWorld()
	{
		std::cout << "Hello World!" << std::endl;
	}
};

#define STD_FUNCTION

template<typename Receiver>
class SimpleCommand : public Command
{
	//How Difference with using, typedef, std::function
#ifdef STD_FUNCTION
	using Action = std::function<void(Receiver*)>;
#elif USING
	using Action = void (Receiver::*)();
#else TYPEDEF
	typedef void(Receiver::*Action)();
#endif

public:
	SimpleCommand(Receiver* receiver, Action action);

	virtual void Execute() override;

private:
	Action action;
	Receiver* receiver;
};

template<typename Receiver>
SimpleCommand<Receiver>::SimpleCommand(Receiver* receiver, Action action)
	: receiver(receiver), action(action)
{
}

template<typename Receiver>
inline void SimpleCommand<Receiver>::Execute()
{
#ifdef STD_FUNCTION
	action(receiver);
#else
	(receiver->*action)();
#endif
}
  • 함수 포인터를 받는 3가지 방법에 대해서 #ifdef, #elif, #else로 구분해서 작성해봤고 어떤 방법으로 받는지에 따라 Execute에서 호출하는 방법도 조금 달라집니다. 적어도 멤버함수를 호출하기 위한 Command이니깐 누구의 멤버함수인지를 알게 해주기 위해 receiver가 필요한 모습입니다.

'디자인 패턴 > 행위' 카테고리의 다른 글

중재자(Mediator)  (2) 2024.01.11
반복자(Iterator)  (1) 2024.01.04
해석자(Interpreter)  (1) 2024.01.04
책임 연쇄(Chain Of Responsibility)  (2) 2024.01.03
행동 패턴  (2) 2024.01.03

+ Recent posts