problem - add new behaviour to set of types

  • different class perform same action differently
  • sometimes add new kind of action to set of related of classes
Manager manager;
manager.serialize();
 
Underling underling;
underling.serialize();

example code

 
#include <cassert>
#include <cmath>
#include <iostream>
 
 
class ASTVisitor;
 
 
///////////////////////////////////////////////////////////////////////////////
// An abstract syntax tree expresses the syntactic relationships between
// elements in some form of a program. For instance, the expression `1 + 2`
// would be captured by the tree:
//
//                                   (+)
//                                   / \  
//                                 (1) (2) 
//
// In general, all nodes represent some intermediate computation. The leaf
// nodes of the tree are constants/literals, and the internal nodes reflect
// operations on values.
///////////////////////////////////////////////////////////////////////////////
 
 
class ASTNode {
public:
  virtual void accept(ASTVisitor& visitor) = 0;
};
 
 
class Constant : public ASTNode {
public:
  Constant(int64_t value)
    : value{value}
      { }
 
  void accept(ASTVisitor& visitor) override;
  int64_t getValue() const noexcept { return value; }
 
private:
  int64_t value;
};
 
 
class BinaryOperator : public ASTNode {
public:
 
  enum Kind : uint8_t {
    ADD, SUBTRACT, MULTIPLY, DIVIDE
  };
  
  BinaryOperator(Kind kind, ASTNode& left, ASTNode& right)
    : kind{kind},
      left{left},
      right{right}
      { }
 
  void accept(ASTVisitor& visitor) override;
  
  // Given the simplicity of these operations, it's possible this should be
  // a POD instead.
  Kind getKind() const noexcept { return kind; }
  ASTNode& getLeft() const noexcept { return left; }
  ASTNode& getRight() const noexcept { return right; }
 
private:
  Kind kind;
  ASTNode& left;
  ASTNode& right;
};
 
 
char
getOperatorChar(BinaryOperator::Kind kind) {
  switch (kind) {
    case BinaryOperator::ADD:      return '+';
    case BinaryOperator::SUBTRACT: return '-';
    case BinaryOperator::MULTIPLY: return '*';
    case BinaryOperator::DIVIDE:   return '/';
  }
}
 
 
///////////////////////////////////////////////////////////////////////////////
// A Visitor based infrastructure can allow for extending the underlying
// operations of the abstract syntax trees. First we write the glue that will
// bind things together. This includes the core visitor interface as well as
// the acceptors for the underlying node kinds.
///////////////////////////////////////////////////////////////////////////////
 
class ASTVisitor {
public:
  virtual void visit(ASTNode& node) = 0;
  virtual void visit(Constant& constant) = 0;
  virtual void visit(BinaryOperator& constant) = 0;
};
 
 
void Constant::accept(ASTVisitor& visitor) {
  // Note: because the actual type of node is known within the specific
  // implementation of accept, the appropriate visit method will be invoked.
  visitor.visit(*this);
}
 
 
void BinaryOperator::accept(ASTVisitor& visitor) {
  visitor.visit(*this);
}
 
 
///////////////////////////////////////////////////////////////////////////////
// At this point, we can go hogwild in writing new visitors that operate on
// the AST in different ways. This type of flexible extension is where the
// Visitor pattern is most useful. It allows for the core model to be in one
// place (like a library), while behaviors can be developed separately (such
// as an application that makes use of a library).
///////////////////////////////////////////////////////////////////////////////
 
 
class Evaluator : public ASTVisitor {
public:
  void visit(ASTNode& node) override {
    assert(false && "Invalid node during evaluation");
  }
 
  // For this visitor, we will always store the result in `result` after a call
  // to `visit`.
 
  void visit(Constant& constant) override {
    result = constant.getValue();
  }
 
  void visit(BinaryOperator& op) override {
    op.getLeft().accept(*this);
    int64_t left = result;
    op.getRight().accept(*this);
    int64_t right = result;
 
    switch (op.getKind()) {
      case BinaryOperator::ADD:      result = left + right; break;
      case BinaryOperator::SUBTRACT: result = left - right; break;
      case BinaryOperator::MULTIPLY: result = left * right; break;
      case BinaryOperator::DIVIDE:   result = left / right; break;
    }
  }
 
  int64_t getResult() { return result; }
 
private:
  int64_t result = 0;
};
 
 
class PrettyPrinter : public ASTVisitor {
public:
  void visit(ASTNode& node) override {
    assert(false && "Invalid node during evaluation");
  }
 
  void visit(Constant& constant) override {
    std::cout << constant.getValue();
  }
 
  void visit(BinaryOperator& op) override {
    char opChar = getOperatorChar(op.getKind());
    std::cout << '(';
    op.getLeft().accept(*this);
    std::cout << ' ' << opChar << ' ';
    op.getRight().accept(*this);
    std::cout << ')';
  }
};
 
 
class RPNPrinter : public ASTVisitor {
public:
  void visit(ASTNode& node) override {
    assert(false && "Invalid node during evaluation");
  }
 
  void visit(Constant& constant) override {
    std::cout << constant.getValue();
  }
 
  void visit(BinaryOperator& op) override {
    op.getLeft().accept(*this);
    std::cout << " ";
    op.getRight().accept(*this);
    std::cout << ' ' << getOperatorChar(op.getKind());
  }
};
 
 
int
main() {
  Constant one{1};
  Constant two{2};
  Constant five{5};
  
  BinaryOperator add(BinaryOperator::ADD, one, two);
  BinaryOperator mult(BinaryOperator::MULTIPLY, add, five);
 
  Evaluator evaluator;
  mult.accept(evaluator);
  std::cout << evaluator.getResult() << "\n";
 
  PrettyPrinter pretty;
  mult.accept(pretty);
  std::cout << "\n";
 
  RPNPrinter rpn;
  mult.accept(rpn);
  std::cout << "\n";
 
  return 0;
}

example diagram

classDiagram
    class ASTNode {
        +virtual void accept(ASTVisitor& visitor)
    }
    
    class Constant {
        -int64_t value
        +void accept(ASTVisitor& visitor)
        +int64_t getValue()
    }
    
    class BinaryOperator {
        -Kind kind
        -ASTNode& left
        -ASTNode& right
        +void accept(ASTVisitor& visitor)
        +Kind getKind()
        +ASTNode& getLeft()
        +ASTNode& getRight()
    }
    
    class ASTVisitor {
        +virtual void visit(ASTNode& node)
        +virtual void visit(Constant& constant)
        +virtual void visit(BinaryOperator& binaryOp)
    }
    
    class Evaluator {
        -int64_t result
        +void visit(ASTNode& node)
        +void visit(Constant& constant)
        +void visit(BinaryOperator& op)
        +int64_t getResult()
    }
    
    class PrettyPrinter {
        +void visit(ASTNode& node)
        +void visit(Constant& constant)
        +void visit(BinaryOperator& op)
    }
    
    class RPNPrinter {
        +void visit(ASTNode& node)
        +void visit(Constant& constant)
        +void visit(BinaryOperator& op)
    }
    
    ASTNode <|-- Constant
    ASTNode <|-- BinaryOperator
    ASTVisitor <|-- Evaluator
    ASTVisitor <|-- PrettyPrinter
    ASTVisitor <|-- RPNPrinter