0

I am trying to come up with a generic builder function for a large hierarchy of objects in Javascript. The goal is to implement as much functionality as possible at the top of the hierarchy. After playing around for a little while, I have ended up with a structure I like, although am not completely happy with.

The structure currently looks somewhat like this. I have attached a working (simplified) version below:

enter image description here

class AbstractItem {

  constructor(build) {
    if (this.constructor === AbstractItem) {
      throw new TypeError("Oops! AbstractItem should not be instantiated!");
    }
    this._id = build.id;
  }

  /*
   The generic ItemBuilder is part of an abstract superclass.
    Every item should have an ID, thus the builder validates this.
    It also provides a generic build() function, so it does not have to be re-implemented by every subclass.
  */
  static get Builder() {

  /*
     This constant references the constructor of the class for which the Builder function was called.
      So, if it was called for ConcreteItem, it will reference the constructor of ConcreteItem.
      This allows us to define a generic build() function.
    */
    const BuildTarget = this;

    class ItemBuilder {

      constructor(id) {      
        if (!id) {
          throw new TypeError('An item should always have an id!');
        }        
        this._id = id;
      }
   
   //The generic build method calls the constructor function stored in the BuildTarget variable and passes the builder to it.
      build() {
        return new BuildTarget(this);
      }

      get id() {
        return this._id;
      }
    }

    return ItemBuilder;
  }

  doSomething() {
    throw new TypeError("Oops! doSomething() has not been implemented!");
  }

  get id() {
    return this._id;
  }
}

class AbstractSubItem extends AbstractItem {

  constructor(build) {  
    super(build);    
    if (this.constructor === AbstractSubItem) {
      throw new TypeError("Oops! AbstractSubItem should not be instantiated!");
    }    
    this._name = build.name;
  }

 /*
   AbstractSubItem implements a different version of the Builder that also requires a name parameter.
  */
  static get Builder() {

  /*
     This builder inherits from the builder used by AbstractItem by calling the Builder getter function and thus retrieving the constructor.
    */
    class SubItemBuilder extends super.Builder {

      constructor(id, name) {
        super(id);
        if (!name) {
          throw new TypeError('A subitem should always have a name!');
        }
        this._name = name;
      }

      get name() {
        return this._name;
      }
    }

    return SubItemBuilder;
  }

  get name() {
    return this._name;
  }
}

class ConcreteItem extends AbstractItem {

  doSomething() {
    console.log('Hello world! My name is ' + this.id + '.');
  }
}

class ConcreteSubItem extends AbstractSubItem {

  doSomething() {
    console.log('Hello world! My name is ' + this.name + ' (id: ' + this.id + ').');
  }
}

new ConcreteItem.Builder(1).build().doSomething();
new ConcreteSubItem.Builder(1, 'John').build().doSomething();

In my opinion, there are some pros and cons to my current approach.

Pros

  • The Builder() method provides a common interface that can be used to obtain a builder for all implementing classes.
  • My concrete classes can inherit the builder class without any additional effort.
  • Using inheritance, the builder can be easily expanded if needed.
  • The builder code is part of the abstract class, so it is clear what is being built when reading the code.
  • The calling code is easy to read.

Cons

  • It is not clear, looking at the Builder() getter function, which parameters are required to avoid an exception. The only way to know this is to look at the constructor (or at the comments), which is buried a couple of layers deep.
  • It feels counter-intuitive having the SubItemBuilder inherit from super.Builder, rather than a top-level class. Likewise, it may not be clear to other how to inherit from the ItemBuilder without looking at the SubItemBuilder example.
  • It is not really clear, looking at the AbstractItem class, that it should be constructed using the builder.

Is there a way to improve my code to negate some of the cons I've mentioned? Any feedback would be very much appreciated.

thijsfranck
  • 778
  • 1
  • 10
  • 24
  • 1
    Sorry, this doesn't look like the [builder pattern](https://en.wikipedia.org/wiki/Builder_pattern) at all. It doesn't even simplify your calling code! Compare your `new ConcreteSubItem.Builder(1, 'John').build().doSomething();` to `new ConcreteSubItem(1, 'John').doSomething();` or `new ConcreteSubItem({id: 1, name: 'John'}).doSomething();`. – Bergi Jan 04 '18 at 13:34
  • Do never create `class`es in getters or methods. The instances won't share anything, and `ConcreteItem.Builder !== ConcreteItem.Builder`. – Bergi Jan 04 '18 at 13:35
  • Thanks a lot! You're right that my implementation seems really complex for the simple example. In my actual use case I was ending up with a constructor that took many parameters at the lower sections of my hierarchy (a significant portion of them optional). I had not considered your approach of passing an object to the constructor. That could be a good way of limiting the size of the constructor. What do you think of this: new ConcreteSubItem(1, 'John', {optional1: true, optional2: false}) – thijsfranck Jan 04 '18 at 13:43
  • 1
    Yes, passing an object is the standard JS approach for functions with many inputs. Usually you'd pass only one object overall, but if there are very few essential parameters and many optional ones you might as well split it between positional parameters and object properties. – Bergi Jan 04 '18 at 13:49

0 Answers0