[모던 C++ 디자인 패턴] 책을 바탕으로 공부하는 내용을 정리한 내용이다.
Strategy Pattern
배열에 문자열 여러 개를 목록 정리해서 출력하려고 한다고 해보자. HTML이나 LaTeX의 경우 목록을 표현하기 위해서는 그 렌더링 언어만의 열림/닫힘 태그가 필요하다. 목록을 출력하는 일은 여러 경우마다 비슷한 부분이 있기도 하고 다른 부분도 있기도 하다. 이때 각 경우들 하나하나를 별개의 "전략"으로 취급할 수 있다.
목록을 출력하는 작업은 다음과 같은 전략으로 공식화할 수 있다. 이때 서로 다른 포맷마다 서로 다른 전략을 공식화하고 각 전략은 일반화되어 불변하는 상위 수준 텍스트 출력 알고리즘에 입력돼 세부 동작에 가변성을 부여할 수 있다.
- 목록의 열림 태그와 항목을 출력한다.
- 목록의 각 항목을 출력한다.
- 목록의 닫힘 태그와 항목을 출력한다.
이 디자인 패턴은 런타임에 변경 가능한 동적 형태와 탬플릿으로 컴파일 타임에 결정되는 정적 형태가 있다.
동적 전략
단순한 텍스트 목록을 두 포맷으로 렌더링하는 것을 해보자.
enum class OutputFormat {
markdown,
html
};
"전략"의 골격은 아래의 베이스 클래스로 정의할 수 있다.
struct ListStrategy {
virtual void start(ostringstream &oss) {};
virtual void add_list_item(ostringstream &oss, const string &item) {}
virtual void end(ostringstream &oss) {};
};
텍스트 처리 컴포넌트는 아래와 같다. 이 컴포넌트는 목록 처리를 위한 전용 멤버 함수 append_list()를 갖는다. oss는 결과가 출력될 버퍼이며, append_list()는 목록을 렌더링 하는 과정을 정의할 뿐만 아니라 렌더링에 적용할 전략도 갖는다.
struct TextProcessor {
public:
void append_list(const vector<string> itmes) {
list_strategy->start(oss);
for ( auto &item : items)
lits_strategy->add_list_item(oss, item);
list_strategy->end(oss);
}
private:
ostringstream oss;
unique_ptr<ListStrategy> list_strategy;
};
위 코드에서는 컴포지션이 사용되고 있따. 컴포지션은 골격 알고리즘을 실체화된 구현으로 만들어주는 방법 중 하나이다.
컴포지션 대신 add_list_item()과 같은 버추얼 멤버 함수를 두어, 하위 클래스에서 오버라이딩하게 할 수도 있다. 그러한 방법은 템플릿 메서드 패턴이다.
이제 목록 렌더링 전략을 어떻게 구현하는지 살펴보자.
struct HTmlListStrategy: public ListStrategy {
void start(ostringstream &oss) override {
oss << "<ul>\n";
}
void end(ostringstream & oss) override {
oss << "</ul>\n";
}
void add_list_item(ostringstream &oss, const string &item) override {
oss << "<li>" << item << "</li>\n";
}
};
struct MarkdownListStrategy : public ListStrategy {
void add_list_item(ostringstream &oss, const string &item) override {
oss << " * " << item << endl;
}
};
HTML과 달리 Markdown에서는 열림/닫힘 태그가 필요 없기 때문에 add_list_item() 멤버 함수만 오버라이딩하면 된다.
이제 TextProcessor를 이용해 서로 다른 전략에 목록을 입력해 서로 다른 렌더링 결과를 얻을 수 있다.
TextProcessor tp;
tp.set_output_format(OutputFormat::markdown);
tp.append_list({"foo", "bar", "baz"});
cout << tp.str() << endl;
// 출력 결과
// * foo
// * bar
// * baz
전략을 런타임 목적에 따라 선택할 수도 있다. 이 때문에 이 구현을 "동적 전략" 패턴이라고 부른다. set_output_format() 함수에서 전략의 선택이 이루어진다. 이 함수의 구현은 아래와 같이 단순하다.
void set_output_format(const OutputFormat format) {
switch ( format ) {
case OutputFormat::markdown:
list_strategy = make_unique<MarkdownListStrategy>();
break;
case OutputFormat::html:
list_strategy = make_unique<HtmlListStrategy>();
break;
}
}
정적 전략
템플릿 덕분에 어떤 전략이든 자동으로 타입에 맞추어 적용할 수 있다. 아래와 같이 TextProcessor 클래스에 약간에 수정만 하면 된다.
template <typename LS>
struct TextProcessor {
public:
void append_list(const vector<string> items) {
list_strategy.start(oss);
for ( auto &item : items )
list_strategy.add_list_item(oss, item);
list_strategy.end(oss);
}
// 다른 함수는 변경 없음
private:
ostringstream oss;
LS list_strategy;
};
요약
전략 디자인 패턴은 알고리즘의 골격만을 정의하고 세부 구현은 컴포지션으로서 특정 전략을 선택적으로 채워 넣을 수 있게 한다. 실현 방법은 두 가지가 있다.
- 동적 전략 : 사용될 전략을 단순 퐁니터 또는 참조로 갖는다. 전략을 바꾸고 싶을 때는 참조를 변경한다.
- 정적 전략 : 컴파일 시점에 전략이 선택되어 고정된다. 나중에 전략을 바꿀 수 없다.
위 두 사항은 개발자의 선택 사항이다.
'algorithm > 알고리즘' 카테고리의 다른 글
[알고리즘]Knuth's optimization 크누스 최적화 (0) | 2019.08.22 |
---|