프로그래밍 일반/디자인 패턴

방문자 패턴(Visitor Pattern)

지노윈 2020. 3. 4. 22:57
반응형

방문자 패턴은 어떤 객체의 계층 각각마다 서로 다른 작업을 수행해야 할때 사용하면 좋습니다.

기존 코드의 수정 없이 새로운 방문자를 추가하는 것만으로도 기능을 확장 할 수 있습니다.

기능 확장면에서 OCP 충실히 따를 수 있습니다.

다음과 같이 DoubleExpression과 AdditionExpression 수식이 있고 각 수식마다 새로운 동작을 추가해야 할때를 생각해봅시다.

struct Expression
{
};
 
struct DoubleExpression : Expression
{
  double value;
  explicit DoubleExpression(const double value)
    : value{value} {}
};
 
struct AdditionExpression : Expression
{
  Expression *left, *right;
 
  AdditionExpression(Expression* const left, Expression* const right)
    : left{left}, right{right} {}
 
  ~AdditionExpression()
  {
    delete left;
    delete right;
  }

침습적 방문자

원래의 방문자는 기존 코드에 영향을 주지 않고 확장하는 것이지만 직접 수정을 하여 print 동작을 추가합니다.

struct Expression
{
  virtual void print(ostringstream& oss) = 0;       // print 동작 추가
};
 
struct DoubleExpression : Expression
{
  double value;
  explicit DoubleExpression(const double value)
    : value{value} {}
 
  void print(ostringstream& oss) override       // print 동작 추가
  {
    oss << value;
  }
};
 
struct AdditionExpression : Expression
{
  Expression *left, *right;
 
  AdditionExpression(Expression* const left, Expression* const right)
    : left{left}, right{right} {}
 
  ~AdditionExpression()
  {
    delete left;
    delete right;
  }
 
  void print(ostringstream& oss) override        // print 동작 추가
  {
    oss << "(";
    left->print(oss);
    oss << "+";
    right->print(oss);
    oss << ")";
  }
};

이 방식으로 수식이나 동작들이 더 추가되려면 어떤 작업들을 해주어야 하는가?

OCP는 문제도 아닙니다. 실질적인 문제는 SRP입니다.

아래와 같이 테스트 해봅니다.

auto e = new AdditionExpression{
  new DoubleExpression{1},
  new AdditionExpression{
    new DoubleExpression{2},
    new DoubleExpression{3}
  }
};
ostringstream oss;
e->print(oss);
cout << oss.str() << endl;    // 출력 결과 : (1+(2+3))

반추적(reflective) 출력

이제 별도의 출력 컴포넌트를 만드는 접근 방법을 사용해봅시다.

print 메서드를 모두 제거하고 ExpressionPrinter 클래스를 도입합니다. print 기능만을 클래스를 구현하므로 SRP 위배하지 않게 됩니다.

struct Expression
{
  virtual ~Expression() = default;
};
 
struct DoubleExpression : Expression
{
  double value;
  explicit DoubleExpression(const double value)
    : value{ value } {}
};
 
struct AdditionExpression : Expression
{
  Expression *left, *right;
 
  AdditionExpression(Expression* const left, Expression* const right)
    : left{ left }, right{ right } {}
 
  ~AdditionExpression()
  {
    delete left;
    delete right;
  }
};
 
struct ExpressionPrinter
{
  void print(DoubleExpression *de, ostringstream& oss) const
  {
    oss << de->value;
  }
  void print(AdditionExpression *ae, ostringstream& oss) const
  {
    oss << "(";
    print(ae->left, oss);    // ERROR : left의 타입이 Expression* 이므로 두 print 함수 중 어느 것이 선택되어야 할지 모릅니다.
    oss << "+";
    print(ae->right, oss);
    oss << ")";
  }
};

C++은 런타임에 타입을 체크하여 오버로딩하는 방식이 아니라 컴파일 시점에 오버로딩이 결정되기 때문에 두 개의 print() 중에 어느 것이 선택되어 하는지 알지 못합니다.

이를 해결하는 방법이 바로 오버로딩을 버리고 런타임에 타입 체크를 명시적으로 구현해 넣는 것입니다. 타입이 무엇인지 되돌아보기 때문에 반추적 방법이라 합니다.

 

struct ExpressionPrinter
{
  ostringstream oss;
 
  void print(Expression *e)
  {
    if (auto de = dynamic_cast<DoubleExpression*>(e)) // 런타임에 타입 체크를 하여 구현, reflective
    {
      oss << de->value;
    }
    else if (auto ae = dynamic_cast<AdditionExpression*>(e))
    {
      oss << "(";
      print(ae->left);
      oss << "+";
      print(ae->right);
      oss << ")";
    }
  }
 
  string str() const { return oss.str(); }
};

코드 품질이 좋지 않지만 꽤 실용적입니다.

auto e = new AdditionExpression{
	new DoubleExpression{ 1 },
	new AdditionExpression{
		new DoubleExpression{ 2 },
		new DoubleExpression{ 3 }
	}
};
ExpressionPrinter ep;
ep.print(e);
cout << ep.str() << endl;

디스패치(Dispatch)?

디스패치의 무슨 의미인가요?

일을 처리할 담당자를 찾아서 전달하는 작업입니다.

 

디스패치의 예를 살펴봅시다.

struct Stuff {};
struct Foo : Stuff {};
struct Bar : Stuff {};
 
void func(Foo* foo) {}
void func(Bar* bar) {}

위와 같은 코드 하에서, 다음의 코드는 컴파일러가 오버로딩을 해줍니다.

Foo* foo = new Foo;
func(foo);  // ok

그렇지만 Stuff로 업캐스팅 하는 경우에는 컴파일러가 오버로딩할 함수를 찾지 못합니다.

Stuff* foo = new Foo;
func(foo);  // error

런타임에 명시적으로 타입 체크를 하지 않고서도(dynamic_cast 등을 이용) 올바르게 오버로딩할 방법이 있을까?

다음의 코드가 그 방법입니다. "이중 디스패치"라고 부릅니다.

struct Foo;
struct Bar;
void func(Foo* foo) {               // 두번째 dispatch, this가 Foo이므로 func(Foo* foo) 호출
 cout << "foo" << endl;
}
void func(Bar* bar) {
 cout << "bar" << endl;
}
 
struct Stuff {
 virtual void call() = 0;
};
struct Foo : Stuff {
 void call() override { func(this); }  // 첫번째 dispatch, vtable에 의해 Foo의 call() 호출
};
struct Bar : Stuff {
 void call() override { func(this); }
};
 
int main()
{
 Stuff* foo = new Foo;
 foo->call();
 
 return 0;
}

전통적인 방문자

방문자 패턴의 전통적인 구현은 이중 디스패치를 이용합니다. 방문자 구현시 다음과 같은 네이밍 관례가 있습니다.

  • 방문자의 멤버 함수는 visit()라는 이름을 가집니다.
  • 클래스 계층마다 구현될 멤버 함수는 accept()라는 이름을 가집니다.

이제 Expression에 accept 멤버 함수를 둡니다.

struct DoubleExpression : Expression
{
  double value;
  explicit DoubleExpression(const double value)
    : value{ value } {}
 
  void accept(ExpressionVisitor* visitor) override
  {
    visitor->visit(this);    // 이중 디스패치
  }
};
 
struct AdditionExpression : Expression
{
  Expression *left, *right;
 
  AdditionExpression(Expression* const left, Expression* const right)
    : left{ left }, right{ right } {}
 
  ~AdditionExpression()
  {
    delete left;
    delete right;
  }
 
  void accept(ExpressionVisitor* visitor) override
  {
    visitor->visit(this);
  }
};

accept()의 구현을 보면 ExpressionVisitor 포인터를 인자로 받습니다. ExpressionVisitor는 여러가지 방문자(ExpressionPrinter, ExpressionEvaluator)의 베이스 클래스입니다.

ExpressionVisitor 관련 클래스들 정의는 다음과 같습니다.

struct ExpressionVisitor
{
  virtual void visit(DoubleExpression* de) = 0;
  virtual void visit(AdditionExpression* ae) = 0;
};
 
struct ExpressionPrinter : ExpressionVisitor
{
  ostringstream oss;
  string str() const { return oss.str(); }
  void visit(DoubleExpression* de) override;
  void visit(AdditionExpression* ae) override;
};

visit() 멤버 함수들의 구현입니다.

void ExpressionPrinter::visit(DoubleExpression* de)
{
  oss << de->value;
}
 
void ExpressionPrinter::visit(AdditionExpression* e)
{
  oss << "(";
  e->left->accept(this);  // 이중 디스패치
  oss << "+";
  e->right->accept(this);
  oss << ")";
}

이중 디스패치를 채용한 Visitor는 다음과 같이 이용 될 수 있습니다.

auto e = new AdditionExpression{
  // 동일
};
ostringstream oss;
ExpressionPrinter ep;
ep.visit(e);
cout << printer.str() << endl;

방문자 추가하기

다음과 같이 OCP를 준수하면서 새로운 방문자 ExpressionEvaluator를 추가 할 수 있습니다.

struct ExpressionEvaluator : ExpressionVisitor
{
  double result;
  void visit(DoubleExpression* de) override;
  void visit(AdditionExpression* ae) override;
};
 
void ExpressionEvaluator::visit(DoubleExpression* de)
{
  result = de->value;
}
 
void ExpressionEvaluator::visit(AdditionExpression* ae)
{
  ae->left->accept(this);
  auto temp = result;
  ae->right->accept(this);
  result += temp;
}

방문자를 추가하면서 accept 관련 코드는 전혀 손댈 필요가 없었습니다.

비순환 방문자

방문자 패턴은 다음과 같이 두 가지 유형으로 나눌 수 있습니다.

  • 순환 방문자
    함수 오버로딩에 기반하는 방문자입니다. 클래스 계층과 방문자간에 상호 참조하는 순화적인 종속성이 발생합니다.
    24.4에서의 구현이 순환 방문자 입니다.  DoubleExpression(클래스 계층)과 ExpressionPrinter(방문자)간의 상호 참조가 있는 것을 코드를 보면 바로 알 수 있습니다.
  • 비순환 방문자
    런타임 타입 정보(RTTI)에 의존하는 방문자입니다. 이 방법은 방문 될 클래스 계층에 제한이 없다는 점입니다. 하지만 성능적인 부분의 약간의 손해가 았습니다.

비순환 방문자는 아래와 같이 최대한 범용적인 형태로 방문자 인터페이스를 정의합니다.

template <typename Visitable>
struct Visitor
{
  virtual void visit(Visitable& obj) = 0;
};

앞서 보았던 Expression 클래스들은 다음과 같이 정의합니다.

struct Expression
{
  virtual void accept(VisitorBase& obj)
  {
    using EV = Visitor<Expression>;
    if (auto ev = dynamic_cast<EV*>(&obj))
      ev->visit(*this);
  }
};
 
struct DoubleExpression : Expression{
  double value;
 
  DoubleExpression(double value) : value(value) {}
 
  virtual void accept(VisitorBase& obj)
  {
    using DEV = Visitor<DoubleExpression>;
    if (auto ev = dynamic_cast<DEV*>(&obj))   // 타입 캐스팅 하여 성공하면 호출 합니다. 특정 방문자를 종속 하는 것이 아니라 VisitorBase를 참조하므로 비순환이라 합니다.
      ev->visit(*this);
  }
};
 
struct AdditionExpression : Expression
{
  Expression *left, *right;
 
  AdditionExpression(Expression *left, Expression *right) : left(left), right(right) {}
 
  ~AdditionExpression()
  {
    delete left;
    delete right;
  }
 
  virtual void accept(VisitorBase& obj)
  {
    using AEV = Visitor<AdditionExpression>;
    if (auto ev = dynamic_cast<AEV*>(&obj))
      ev->visit(*this);
  }
};

dynamic_cast<EV*>(&obj) 캐스팅이 성공하면 방문자가 해당 타입을 어떻게 방문해야하는지 알 수 있게 되고 캐스팅 된 타입의 visit() 멤버 함수를 호출합니다.

만약 캐스팅에 실패하면 아무것도 하지 않습니다.

ExpressionPrinter 클래스를 다음과 같이 구현하여 전체 표현을 방문 할 수 있도록 합니다.

struct ExpressionPrinter : VisitorBase,
                           Visitor<DoubleExpression>,
                           Visitor<AdditionExpression>
{
  void visit(DoubleExpression &obj) override
  {
    oss << obj.value;
  }
  void visit(AdditionExpression &obj) override
  {
    oss << "(";
    obj.left->accept(*this);
    oss << "+";
    obj.right->accept(*this);
    oss << ")";
  }
 
  string str() const { return oss.str(); }
private:
  ostringstream oss;

 std::variant와 std::visit

std::any가 안전한 void*라면 std::variant는 안전한 union이라 할 수 있습니다.

다음과 같이 사용할 수 있습니다.

variant<string, int> house;
house = 221;
 
cout << std::get<int>(house) << endl;;
cout << std::get<1>(house) << endl;

std::visit를 이용해 자동으로 가변 타입에서 실제 저장된 타입에 맞추어 오버로딩 되도록 함수 호출 연산자를 호출 할 수 있습니다.

struct AddressPrinter
{
 void operator() (const string& house_name) const {
  cout << "A house called " << house_name << endl;
 }
 
 void operator() (const int house_number) const {
  cout << "House number " << house_number << endl;
 }
};
 
int main()
{
 variant<string, int> house;
 house = 221;
 
 AddressPrinter ap;
 std::visit(ap, house);
 
 house = "jino";
 std::visit(ap, house);
 
 return 0;
}
 
// 출력 결과 :
// House number 221
// A house called jino

 

 

[독서 리뷰] - 모던 C++ 디자인 패턴

 

모던 C++ 디자인 패턴

[객체 지향 프로그래밍/디자인 패턴] - 빌더 패턴(Builder Pattern) [분류 전체보기] - 디자인 패턴 [객체 지향 프로그래밍/디자인 패턴] - 팩토리 패턴(Factory pattern) [객체 지향 프로그래밍/디자인 패턴] -..

devjino.tistory.com

코드 참고 : https://cpp-design-patterns.readthedocs.io/en/latest/
 

Welcome to C++ Design Patterns’s documentation! — C++ Design Patterns 0.0.1 documentation

© Copyright 2018, Hans-J. Schmid Revision 786c83f8.

cpp-design-patterns.readthedocs.io

 

'프로그래밍 일반 > 디자인 패턴' 카테고리의 다른 글

템플릿 메서드 패턴(Template method Pattern)  (0) 2020.03.04
전략 패턴(Strategy Pattern)  (0) 2020.03.04
상태 패턴(State Pattern)  (0) 2020.03.04
관찰자 패턴(Observer Pattern)  (0) 2020.03.04
Null 객체  (0) 2020.03.04