dimanche 11 septembre 2022

c++: Designing builders for an hierarchy of classes [duplicate]

Suppose we have an abstract base class A from which a hierarchy of descendant classes could be derived:

class A {
 public:
  virtual ~A() = 0;
  int getX() const { return x_; }

 private:
  int x_;
};

A::~A(){};

In the code below I derive, as an example, a child class B and a grandchild class C from A:

class B : public A {
 public:
  const std::string& getY() const { return y_; }

 private:
  std::string y_;
};

class C : public B {
 public:
  const std::string& getZ() const { return z_; }

 private:
  std::string z_;
};

In the example above the classes were kept simple to avoid cluttering the question. However, in real life these classes will have each several different properties that should be set during their instantiation. Also, all properties will have default values and the user will be able to set just the desired properties (or eventually none of them) when creating the objects.

In this scenario, the use of a builder pattern to initialize the objects seems to be a common approach.There are several different builder designs available, and I would like the user to be able to instantiate the classes above in the following manner:

B_Builder b_builder;             // creates the builder object
b_builder.setX(10);              // sets a property inherited from class A
b_builder.setY("Test");          // sets a property defined in class B
B b_object = b_builder.build();  // the builder instantiate an object of class B

C_Builder c_builder;             // creates the builder object
c_builder.setX(11);              // sets a property inherited from class A
c_builder.setY("Test2");         // sets a property inherited from class B
c_builder.setZ("Test23");        // sets a property defined in class C
C c_object = c_builder.build();  // the builder instantiate an object of class C

One possible way to implement these builders would be to create the B_Builder and C_Builder both separately and from scratch. The problem with this approach is that, since the builders will have to initialize commonly inherited properties, there will be code duplication. The problem will be more noticeable as more properties are inherited or deeper is the inheritance chain.

Thus, I considered using an hierarchy of builders, which would avoid code duplication (I've already seen this approach in a Java post, but could not find it again to link it here). The solution I came up with uses the builder inheritance chain to temporarily store the object properties and to set the object properties once it is created.

Below you can check the current solution (for the code below to work, I had to add the builder classes as friends of the classes they actually build):

class A_Builder {
 public:
  virtual ~A_Builder() = 0;
  void setX(int x) { x_ = x; }
 
 protected:
   template<typename T>
   T& build_() const {
     T* t = new T;
     t->x_ = this->x_;
     return *t; 
   }

 private:
  int x_;

};

A_Builder::~A_Builder(){};

class B_Builder : public A_Builder {
 public:
  void setY(std::string y) { y_ = y; }

  B& build() const {
    return build_<B>();
  }

protected:
  template<typename T>
  T& build_() const {
    T& c = A_Builder::build_<T>();
    c.y_ = y_;
    return c;
  }
  
private:
  std::string y_;

};

class C_Builder : public B_Builder {
 public:
  void setZ(std::string z) { z_ = z; }

  C& build() const {
    return build_<C>();
  }

protected:
  template<typename T>
  T& build_() const {
    T& c = B_Builder::build_<T>();
    c.z_ = z_;
    return c;
  }

private:
  std::string z_;

};

The solution looks scalable, exposes the object building interface I was looking for and avoids code duplication. However, I didn't like the builder inheritance scheme in its current state because, every time a new builder needs to be created (i.e. inherited) the user will have also to (in addition to the inheritance) write a build_ method that is almost identical to the builder_ found in parent's builders.

I am not a c++ expert, but I am quite sure that a cleaner and more elegant inherited builder scheme could be devised (e.g. which would avoid the build_ method problem). It can even be that such a builder design already exists, but I am not aware and could not find it trough my search.

So, any help/advice regarding the improvement of this object building scheme is very welcome! Thank you in advance!

Aucun commentaire:

Enregistrer un commentaire