회사의 코드들이 상당히 계층적으로 묶여있는 상속관계… 즉, 결과와 관련된 모든 클래스는 ResultInterface라는 순수 가상 클래스(즉, 인터페이스)를 상속받고 있는 관계로 dynamic_cast를 if문으로 쭈욱 연결해서 해당 함수를 호출하는 코드가 일반적이었다.
예를 들자면,
class Interface { virtual void DoIt() = 0; }; class A : public Interface { virtual void DoIt(); }; class B : public Interface { virtual void DoIt(); }; class C : public Interface { virtual void DoIt(); };
위와 같이 선언이 되어 있고, 보통 하는 짓이..
void ProcessInterface(boost::shared_ptr<interface> obj) { boost::shared_ptr<a> a_obj = boost::dynamic_cast<a>(obj); if(a_obj.get() != 0) { ProcessInterface(a_obj); } boost::shared_ptr<b> b_obj = boost::dynamic_cast<b>(obj); if(b_obj.get() != 0) { ProcessInterface(b_obj); } boost::shared_ptr<c> c_obj = boost::dynamic_cast<c>(obj); if(c_obj.get() != 0) { ProcessInterface(c_obj); } } void ProcessInterface(boost::shared_ptr<a> obj){...} void ProcessInterface(boost::shared_ptr<b> obj){...} void ProcessInterface(boost::shared_ptr<c> obj){...}
이런 짓이었는데, Interface를 상속받는 클래스가 10개정도 되니 저 if문 남발이 너무나도 보기가 싫어졌다. -_-;
그리하야… boost::mpl::vector를 사용해서 나쁜(?) 짓을 해버렸는데…
template<class TypeList> void ProcessInterfaceImpl (boost::shared_ptr<interface> obj, boost::mpl::bool_<false>) { // TypeList 제일 앞에 있는 놈을 가져온다. (지금 dynamic_cast할꺼!) typedef typename boost::mpl::front<typeList>::type current_type; boost::shared_ptr<current_type> current_obj = boost::dynamic_cast<current_type>(obj); if(current_obj.get() != 0) { // 성공했으면 이놈으로 처리한다. ProcessInterface(current_obj); return; } else { // 실패했으면, 다음으로 넘어가야한다. 지금 확인했던놈을 빼자. (pop_front) typedef typename boost::mpl::pop_front<typeList>::type next_list; // next_list는 첫놈을 뺀 나머지의 리스트. next_list를 이용해 호출한다. ProcessInterfaceImpl<next_list>( obj, // empty체크를 하는 이유는?? boost::mpl::bool_<boost::mpl::empty<next_list>::value>() ); } } template<class TypeList> void ProcessInterfaceImpl (boost::shared_ptr<interface> obj, boost::mpl::bool_<true>){return true;} typedef boost::mpl::vector<a, B, C> type_list; void ProcessInterface(boost::shared_ptr<interface> obj) { ProcessIntefaceImpl<type_list>( obj, boost::mpl::bool_<boost::mpl::empty<type_list>::value>() ); }
boost::mpl::vector에 downcast해서 실행시킬 클래스들을 넣어놓고, ProcessInterfaceImpl을 통해 코드를 전개해서 해결하는 방식이다. 코드가 살짝 지저분한 것은, boost::mpl::bool_<> 템플릿을 이용하기 때문인데, 함수 템플릿은 부분 특화를 못하기 때문에 오버로딩을 사용했다. 물론 비어있는 boost::mpl::vector가 어떤 클래스인지를 알면 상관이 없긴 한데, 어차피 함수는 2개 짜야하는거고 (뒤가 있을때와 없을때.) 2개 짤거라면, 이렇게 하는 것도 나쁘지 않았다. 부분 특화를 막아놨다면, 오버로딩으로 풀면 되는거니깐.
물론, boost::mpl::fold같은 알고리즘을 사용하면 훨씬 쉽게 풀리겠지만(dispatcher패턴? 을 사용하면 되므로), 코드가 나만 읽는 것도 아니고.. 그렇게 쓰게 되면 여러모로 좀 복잡해진다.
Inteface를 쓰다 보면 종종 나오는 반복적인 dynamic_cast. 이렇게 코드를 줄여놓으면 상당히 편할 듯 싶다.
+2. 요즘 boost::mpl에 맛들렸음-
안녕하세요. 저도 놀러 왔습니다. 🙂 예전에 mpl책 나왔을때 사서 읽고 오랫만에 보는 mpl 코드네요. 재밌을것 같아 해보려고 했더니 컴파일 에러가 나네요. 컴파일 되는 코드로 올려주세요… (오랫만에 책도 꺼내서 보고 있는데 기억이 가물가물… 다시 한번 읽어야 겠어요. ㅋㅋ)
그리고 물론 진짜 코드에서야 이유가 있었겠지만 ProcessInterface 보다는 virtual Interface::process()와 같은 식으로 refactoring 해야 하는 코드가 아닌가 싶네요. shared_ptr를 쓰기 위해 다형성을 포기하는건 너무 비싼 듯.
컴파일 되는 코드는 한번 만들어 보겠습니다. 하하.
refactoring 해야합니다. ㅋㅋ
일단 저런 테크닉을 쓰게된 이유를 써보면.
virtual Interface::process()을 쓰는 방안도 고민을 했었지만, 이게 서로 다른 모듈(DLL)사이에서 주고받는 데이터 클래스라서 이런 식으로 처리되었습니다. 데이터 클래스의 경우에는 최대한 operation을 배제하기 때문에… 위 예제의 background에는 그런 스토리가 있는게지요. T_T
단점이 있다면, Interface를 상속받는 클래스들이 죄다 STL을 쓰고 있는지라, STL과 boost의 버젼이 모듈별로 잘 맞아야 한다는 점인데, 이건 메이져 릴리즈 내에서는 해결하고 있는 점이고, STL이나 boost가 변경되면 Interface 계열의 클래스들이 갖는 버젼 상수를 바꿔주기 때문에 런타임에서도 문제가 해결되지요. 후후.
후에 시간이 있다면, (그리고 정책이 된다면…) 제가 만드는 제품에서 다루고 있는 모듈화 테크닉에 대해 다뤄보는 것도 좋을 듯 싶네요.