jeudi 4 janvier 2018

Javascript ES6 generic builder pattern for large hierarchy

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 main reason I went with my current implementation is that 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.

Aucun commentaire:

Enregistrer un commentaire