이번 글에서는 지난번 글에서 살펴보았던 dynamic visitor를 개선해 보도록 하겠습니다. 먼저 이전 구현에서 불편한 점을 알아보겠습니다.
이전과 같은 B, D1, D2 클래스가 있습니다. 달라진 점은 print라는 함수가 D1과 D2 클래스에 대해 이미 존재한다는 점입니다.
이 클래스들을 가지고 dynamic_visitor를 사용하면 다음과 같습니다.
여기서 문제는 이미 D1, D2에 대해 print라는 함수가 구현되어 있는데 PrintVisitor에서 다시 구현해야 한다는 점입니다. 물론 이 예제에서는 코드가 짧아 별 문제 없어 보이지만 긴 코드라면 문제가 됩니다. DRY 규칙에 어긋나는 것이죠. ((Don't Repeat Yourself.))
그럼 이번 글에서 살펴볼 개선된 dynamic_visitor의 사용법을 보겠습니다.
만약 base 클래스인 B 클래스에 대한 visit함수가 아무 일도 안해도 된다면 다음과 같이 using을 사용하여 B*에 대한 visit 함수 대신 dynamic_visitor가 제공하는 default 구현을 사용할 수 있습니다.
여기서 super_t 타입은 dynamic_visitor에서 제공하는 typedef입니다. 만약 이 super_t 타입이 없다면 다음과 같이 코드가 복잡하고 길어지겠죠?
그럼 이제 구현에 대해 살펴보겠습니다.
각 항목 번호는 위의 코드에 있는 주석에 있는 번호에 해당합니다.
이외에 이전 글의 dynamic_visitor에 비해 개선된 점은 원하는 리턴 타입을 사용자가 지정할 수 있다는 점입니다. 예를 들어 추가하고자 하는 함수가 char const*를 리턴해야 한다면 다음과 같이 할 수 있습니다.
이번 글에서는 개선된 dynamic_visitor에 대해 알아봤습니다.
개인적으로는 남들이 사용할 수는 있지만 수정할 수 없는 라이브러리 코드에서 사용하는 것 외의 상황에서 Visitor 패턴을 만들어 사용하는 것을 권장하지 않습니다. 한번 프로젝트에서 사용했다가 크게 후회했던 적이 있거든요. 사실 제가 수정할 수도 있는 코드였는데 괜히 이 디자인 패턴을 사용해 봤던 것이죠. 덕분에 기능 추가할때마다 Visitor를 구현해야 했고 디버깅시 trace도 꽤나 복잡해지더군요. 물론 코드 읽기도 어려워지고요.
그리고 라이브러리 코드라고 해서 항상 코드를 수정할 수 없는 것은 아니라는 점도 고려해서 기능 확장을 위해 Visitor가 필요한지 그냥 코드를 수정하게 할지를 결정해야 할 것 같습니다.
dynamic_visitor.zip
struct B {}
struct D1 : B {}
struct D2 : B {}
void print(D1* ) { cout << "D1n"; }
void print(D2* ) { cout << "D2n"; }
이전과 같은 B, D1, D2 클래스가 있습니다. 달라진 점은 print라는 함수가 D1과 D2 클래스에 대해 이미 존재한다는 점입니다.
이 클래스들을 가지고 dynamic_visitor를 사용하면 다음과 같습니다.
typedef mpl::vector TL;
struct PrintVisitor : dynamic_visitor
{
void visit(B* b) { cout << “Bn”; }
void visit(D1* b) { cout << “D1n”; }
void visit(D2* b) { cout << “D2n”; }
};
여기서 문제는 이미 D1, D2에 대해 print라는 함수가 구현되어 있는데 PrintVisitor에서 다시 구현해야 한다는 점입니다. 물론 이 예제에서는 코드가 짧아 별 문제 없어 보이지만 긴 코드라면 문제가 됩니다. DRY 규칙에 어긋나는 것이죠. ((Don't Repeat Yourself.))
그럼 이번 글에서 살펴볼 개선된 dynamic_visitor의 사용법을 보겠습니다.
typedef mpl::vector TL;
struct PrintVisitor : dynamic_visitor
{
PrintVisitor() {
assign_impl(print);
assign_impl(print);
}
void visit(B* b) { cout << “Bn”; }
};
만약 base 클래스인 B 클래스에 대한 visit함수가 아무 일도 안해도 된다면 다음과 같이 using을 사용하여 B*에 대한 visit 함수 대신 dynamic_visitor가 제공하는 default 구현을 사용할 수 있습니다.
typedef mpl::vector TL;
struct PrintVisitor : dynamic_visitor
{
using super_t::visit;
PrintVisitor() {
assign_impl(print);
assign_impl(print);
}
};
여기서 super_t 타입은 dynamic_visitor에서 제공하는 typedef입니다. 만약 이 super_t 타입이 없다면 다음과 같이 코드가 복잡하고 길어지겠죠?
using dynamic_visitor::visit;
그럼 이제 구현에 대해 살펴보겠습니다.
template
class dynamic_visitor
{
typedef typename mpl::back::type base_type;
private:
template
struct visit_impl // (1)
{
typedef Ret (*impl_type)(T*);
visit_impl(impl_type it = 0) : impl(it) {}
impl_type impl;
};
typedef typename mpl::inherit_linearly<
TL,
mpl::inherit<_1, visit_impl<_2> >
>::type impl_map; // (2)
template
typename visit_impl::impl_type& get_impl(visit_impl& t) // (3)
{
return t.impl;
}
impl_map map_;
protected:
typedef dynamic_visitor super_t; // (4)
virtual Ret visit(base_type* ) {} // (5)
template
void assign_impl(typename visit_impl::impl_type impl) // (6)
{
get_impl(map_) = impl;
}
public:
Ret operator()(base_type* b) {
return do_visit(b, mpl::false_());
}
private:
template
Ret do_visit(base_type* b, mpl::false_) {
typedef typename mpl::front::type Head;
typedef typename mpl::pop_front::type Tail;
typedef typename mpl::empty::type IsEmpty;
Head* p = dynamic_cast(b);
if (p != 0) {
if (get_impl(map_)) { // (7)
return get_impl(map_)(p);
}
return static_cast(this)->visit(p);
}
return do_visit(b, IsEmpty());
}
template
Ret do_visit(base_type* b, mpl::true_) {
std::cout << "assertn"; // handle error.
return Ret();
}
};
각 항목 번호는 위의 코드에 있는 주석에 있는 번호에 해당합니다.
- function pointer를 저장하고 있는 클래스로 T 타입에 대해 Ret (T*) 함수 포인터를 저장한다.
- TypeList에 있는 타입들을 멤버 변수로 가지는 클래스를 선언한다. 여기서 inherit_linearly는 mpl 라이브러리에서 제공하는 meta-function으로 각 타입을 가지는 클래스들을 차례대로 상속받도록 fold하여 result 타입엔 각 타입에 해당하는 값을 가지고 있게 만들어 준다. ((예를 들어 만약 TL이 mpl::vector
이라면 다음과 같은 클래스 hierarchy가 만들어집니다.
struct visit_impl: empty_base { Ret (*impl)(char*); };
struct visit_impl: visit_impl { Ret (*impl)(int*); };
struct visit_impl: visit_impl { Ret (*impl)(double*); };
)) - 2번에서 만든 타입에서 특정 타입을 키로 하여 값을 얻을 수 있도록 해주는 helper function이다.
- 위에서 살펴 봤던 super_t 타입을 typedef한다.
- base 클래스에 대한 기본 visit 구현을 제공한다. 하위 클래스에서 using super_t::visit;를 하면 이 함수가 하위 클래스에서 보이게 된다.
- 하위 클래스에서 구현 함수를 셋할 수 있도록 해주는 protected 함수이다.
- 실제 operator() 호출시 사용자에 의해 셋된 구현이 있는지 확인하고 있다면 그 함수를 호출한다. 없다면 이전 버전과 동일하게 사용자가 구현한 visit 함수를 호출한다.
이외에 이전 글의 dynamic_visitor에 비해 개선된 점은 원하는 리턴 타입을 사용자가 지정할 수 있다는 점입니다. 예를 들어 추가하고자 하는 함수가 char const*를 리턴해야 한다면 다음과 같이 할 수 있습니다.
char const* get_name(D1* ) { return "D1"; }
typedef mpl::vector TL;
struct PrintVisitor : dynamic_visitor
{
char const* visit(B* ) { return "B"; }
char const* visit(D2* ) { return "D2"; }
};
// usage
PrintVisitor pv;
cout << pv(b1) << endl;
cout << pv(b2) << endl;
이번 글에서는 개선된 dynamic_visitor에 대해 알아봤습니다.
개인적으로는 남들이 사용할 수는 있지만 수정할 수 없는 라이브러리 코드에서 사용하는 것 외의 상황에서 Visitor 패턴을 만들어 사용하는 것을 권장하지 않습니다. 한번 프로젝트에서 사용했다가 크게 후회했던 적이 있거든요. 사실 제가 수정할 수도 있는 코드였는데 괜히 이 디자인 패턴을 사용해 봤던 것이죠. 덕분에 기능 추가할때마다 Visitor를 구현해야 했고 디버깅시 trace도 꽤나 복잡해지더군요. 물론 코드 읽기도 어려워지고요.
그리고 라이브러리 코드라고 해서 항상 코드를 수정할 수 없는 것은 아니라는 점도 고려해서 기능 확장을 위해 Visitor가 필요한지 그냥 코드를 수정하게 할지를 결정해야 할 것 같습니다.
Downloads
dynamic_visitor.zip
Comments
Post a Comment