무엇인가
할 일을 값으로 만들어 저장하고, 옮기고, 나중에 실행한다. 호출하는 쪽은 무슨 일이 벌어지는지 모른 채 “실행해”라고만 한다.
이 갈래의 대표 사례다. 클래스판에서는 명령을 하나 늘릴 때마다 클래스가 하나 늘고, 람다판에서는 하나도 안 는다.
구조
graph TD INV[호출자: CommandList] -->|execute| Q[명령 목록] Q --> A[명령 1] Q --> B[명령 2] Q --> C[명령 3]
클래스판
추상 인터페이스 하나에 구현 클래스가 명령 수만큼 필요하다.
#include <iostream>
#include <memory>
#include <vector>
struct Command {
virtual ~Command() = default;
virtual void action() = 0;
};
struct AwakeCommand : Command {
void action() override { std::cout << "일어난다\n"; }
};
struct StudyCommand : Command {
void action() override { std::cout << "공부한다\n"; }
};
struct SleepCommand : Command {
void action() override { std::cout << "잔다\n"; }
};
class CommandList {
std::vector<std::unique_ptr<Command>> actions_;
public:
void add(std::unique_ptr<Command> c) { actions_.push_back(std::move(c)); }
void execute() const {
for (const auto& c : actions_) c->action();
}
};
int main() {
CommandList list;
list.add(std::make_unique<AwakeCommand>());
list.add(std::make_unique<StudyCommand>());
list.add(std::make_unique<SleepCommand>());
list.execute();
}클래스가 넷이다(인터페이스 1 + 구현 3). 명령을 하나 더 만들면 다섯이 된다.
람다판
Command의 메서드는 action() 하나뿐이다. 접힌다.
#include <functional>
#include <iostream>
#include <string>
#include <vector>
class CommandList {
std::vector<std::function<void()>> actions_;
public:
void add(std::function<void()> c) { actions_.push_back(std::move(c)); }
void execute() const {
for (const auto& c : actions_) c();
}
};
int main() {
CommandList list;
list.add([] { std::cout << "일어난다\n"; });
list.add([] { std::cout << "공부한다\n"; });
list.add([] { std::cout << "잔다\n"; });
// 상태가 필요하면 캡처한다 — 클래스판이라면 멤버와 생성자가 필요했다
std::string who = "수원";
int count = 3;
list.add([who, count] { std::cout << who << "이 " << count << "번 반복한다\n"; });
list.execute();
}클래스가 하나다(CommandList). 명령을 몇 개 더 만들어도 그대로 하나다.
마지막 명령이 요점을 하나 더 보여 준다. 클래스판에서 파라미터 있는 명령을 만들려면 멤버 변수와 생성자가 필요했는데, 여기서는 캡처 목록이 그 자리를 대신한다.
무엇이 달라졌나
| 클래스판 | 람다판 | |
|---|---|---|
| 명령 3개일 때 클래스 수 | 4 | 1 |
| 명령을 하나 더 | 클래스 +1 | 람다 한 줄 |
| 명령에 파라미터 | 멤버 + 생성자 | 캡처 |
| 명령이 적힌 곳 | 클래스 정의부 | 등록하는 자리 바로 그곳 |
마지막 줄이 실제로는 가장 크다. 클래스판은 “무엇을 하는 명령인지” 보려면 클래스 정의로 가야 하지만, 람다판은 등록하는 자리에 동작이 그대로 적혀 있다.
합성하면 컨테이너도 사라진다
명령이 전부 void()라면 둘을 이어 붙인 것도 void() 다. 그러면 목록조차 필요 없다.
#include <functional>
#include <iostream>
class CommandList {
std::function<void()> action_ = [] {};
public:
void add(std::function<void()> next) {
action_ = [prev = action_, next = std::move(next)] {
prev();
next();
};
}
void operator()() const { action_(); } // 자기 자신이 명령이 된다
};
int main() {
CommandList basic;
basic.add([] { std::cout << " 먹는다\n"; });
basic.add([] { std::cout << " 잔다\n"; });
CommandList combat;
combat.add([] { std::cout << " 쏜다\n"; });
combat.add([] { std::cout << " 엄폐한다\n"; });
CommandList all;
all.add([] { std::cout << "[기본]\n"; });
all.add(basic); // 목록을 목록에 넣는다
all.add([] { std::cout << "[전투]\n"; });
all.add(combat);
all();
}operator()를 두어 CommandList 자신을 호출 가능하게 만들면 목록이 곧 명령이 되어
목록 안에 목록을 넣을 수 있다. 이것이 사실상 Composite 패턴이고, 클래스 계층 없이 나온다.
C++14의 초기화 캡처([prev = action_, ...])가 이 구현의 핵심이다.
C++11만 쓴다면 이전 값을 지역 변수로 받아 복사 캡처해야 한다.
언제 접고 언제 접지 않나
접는다
- 명령이 단순하고 “실행” 하나만 필요할 때. 대부분이 여기 해당한다.
- 명령마다 클래스를 만드는 것이 과할 때.
접지 않는다
- 되돌리기(undo)가 필요할 때. GoF의 Command에는 보통
undo()가 같이 온다. 메서드가 둘이 되므로 접기 조건을 위반한다. - 명령을 직렬화하거나 로그로 남겨야 할 때. 람다는 이름도 타입도 없어 저장·복원이 안 된다.
- 명령을 런타임에 구분해야 할 때(이건 취소하고 저건 두고).
되돌리기가 필요하면
struct Action { std::function<void()> redo, undo; };처럼 값 타입 하나에 둘을 묶는 절충안이 있다. 클래스 계층은 안 생기고 쌍은 유지된다.