1
\$\begingroup\$

In a project for fun/practice, I made a few builder classes that all started to feel the same, so I decided to generalize them by creating a factory that would automatically assign the similarities that they all shared.

  • Each of my builders had a private variable, data, which would eventually become the built class.
  • Each builder had a reset method that would "clear" the data object.
  • And each builder had a create method that would return the data object and call the reset method so that the builder could then be used again (they're kind of more like some breed of assembly line, mixing qualities from both builders and factories at this point).

Here's my generalized factory for making the builders:

function createBuilder(settersMap) {
  var data = {};

  return Object.keys(settersMap).reduce(function(builder, name) {
    builder[name] = function() {
      settersMap[name].apply(data, arguments);
    }
    return builder;
  }, {
    reset: function() { // so users can clear the builder
      data = {};
    },
    create: function() {
      var created = data;
      data = {};
      return created;
    }
  });
}

I use the factory to create driverBuilder and a carBuilder like so:

// Drivers
var driverBuilder = createBuilder({
  setName: function(first, last) {
    this.name = first + " " + last;
  },
  setAge: function(age) {
    this.age = age;
  },
  setSkillLevel: function(skillLevel) {
    this.skillLevel = skillLevel;
  }
});

var drivers = [];

driverBuilder.setName("Joe", "McMullen");
driverBuilder.setAge("26");
driverBuilder.setSkillLevel(6);
drivers.push(driverBuilder.create());

// Cars
var carBuilder = createBuilder({
  setMake: function(make) {
    this.make = make;
  },
  setModel: function(model) {
    this.model = model;
  },
  setTopSpeed: function(topSpeed) {
    this.topSpeed = this.topSpeed === undefined ? topSpeed : this.topSpeed + topSpeed;
  },
  addTurboBoost: function(boost) {
    this.topSpeed = this.topSpeed === undefined ? boost : this.topSpeed + boost;
  }
});

var cars = [];

carBuilder.setMake("Volkswagen");
carBuilder.setModel("Beetle");
carBuilder.setTopSpeed(150);
carBuilder.addTurboBoost(30);
cars.push(carBuilder.create());

I then spent the next billion years programming up a gui using jquery which you can mess around with if you'd like to check it out:

var RacingLeague = (function() {
  // Utility
  function createBuilder(settersMap) {
    var data = {};

    return Object.keys(settersMap).reduce(function(builder, name) {
      builder[name] = function() {
        settersMap[name].apply(data, arguments);
      }
      return builder;
    }, {
      reset: function() {
        data = {};
      },
      create: function() {
        var created = data;
        data = {};
        return created;
      }
    });
  }

  // Drivers
  var driverBuilder = createBuilder({
    setName: function(first, last) {
      this.name = first + " " + last;
    },
    setAge: function(age) {
      this.age = age;
    },
    setSkillLevel: function(skillLevel) {
      this.skillLevel = skillLevel;
    }
  });

  var drivers = [];

  driverBuilder.setName("Joe", "McMullen");
  driverBuilder.setAge("26");
  driverBuilder.setSkillLevel(6);
  drivers.push(driverBuilder.create());

  driverBuilder.setName("Sam", "Benito");
  driverBuilder.setAge("21");
  driverBuilder.setSkillLevel(3);
  drivers.push(driverBuilder.create());

  driverBuilder.setName("David Jorgensen"); // whoops!
  driverBuilder.reset();
  driverBuilder.setName("David", "Jorgensen");
  driverBuilder.setSkillLevel(5);
  drivers.push(driverBuilder.create());

  // Cars
  var carBuilder = createBuilder({
    setMake: function(make) {
      this.make = make;
    },
    setModel: function(model) {
      this.model = model;
    },
    setTopSpeed: function(topSpeed) {
      this.topSpeed = this.topSpeed === undefined ? topSpeed : this.topSpeed + topSpeed;
    },
    addTurboBoost: function(boost) {
      this.topSpeed = this.topSpeed === undefined ? boost : this.topSpeed + boost;
    }
  });

  var cars = [];

  carBuilder.setMake("Volkswagen");
  carBuilder.setModel("Beetle");
  carBuilder.setTopSpeed(150);
  carBuilder.addTurboBoost(30);
  cars.push(carBuilder.create());

  carBuilder.setMake("Dodge");
  carBuilder.setModel("Charger");
  carBuilder.setTopSpeed(190);
  cars.push(carBuilder.create());

  carBuilder.setMake("Pontiac");
  carBuilder.setModel("Firebird");
  carBuilder.setTopSpeed(180);
  carBuilder.addTurboBoost(20);
  cars.push(carBuilder.create());

  return {
    cars: cars,
    drivers: drivers,
    determineWinner: function(pair1, pair2) {
      var odds = 0.5;
      odds += pair1.driver.skillLevel > pair2.driver.skillLevel ? 0.15 : 0.00;
      odds -= pair1.driver.skillLevel < pair2.driver.skillLevel ? 0.15 : 0.00;
      odds += pair1.car.topSpeed > pair2.car.topSpeed ? 0.15 : 0.00;
      odds -= pair1.car.topSpeed < pair2.car.topSpeed ? 0.15 : 0.00;
      return Math.random() <= odds ? pair1 : pair2;
    }
  };
})();




// DOCUMENT READY
$(document).ready(function() {
  function getDriverId(driver) {
    return driver.name;
  }

  function getDriver(driverId) {
    return RacingLeague.drivers.find(function(driver) {
      return driver.name === driverId;
    });
  }

  function getCarId(car) {
    return `${car.make} ${car.model}`;
  }

  function getCar(carId) {
    carId = carId.split(" ");
    return RacingLeague.cars.find(function(car) {
      return car.make === carId[0] && car.model === carId[1];
    });
  }

  var winsMap = {};
  RacingLeague.drivers.map(getDriverId).forEach(function(driverId) {
    winsMap[driverId] = 0
    $("#score-board").append($(`<span>${driverId} Wins: <span id="${driverId}"></span></span><br>`));
  });
  RacingLeague.cars.map(getCarId).forEach(function(carId) {
    winsMap[carId] = 0
    $("#score-board").append($(`<br><span>${carId} Wins: <span id="${carId}"></span></span>`));
  });

  function refreshWins(winners) {
    if (winners) {
      winsMap[getDriverId(winners.driver)]++;
      winsMap[getCarId(winners.car)]++;
    }

    Object.keys(winsMap).forEach(function(key) {
      $(`[id='${key}']`).html(winsMap[key]);
    });
  }
  refreshWins();

  (function addSelectOptions() {
    RacingLeague.drivers.map(getDriverId).forEach(function(driverId) {
      $('.select-driver').each(function() {
        $(this).append($("<option></option>")
          .attr("value", driverId)
          .text(driverId));
      });
    });
    RacingLeague.cars.map(getCarId).forEach(function(carId) {
      $('.select-car').each(function() {
        $(this).append($("<option></option>")
          .attr("value", carId)
          .text(carId));
      });
    });
  })();

  function declareWinner(obj) {
    refreshWins(obj);
    $("#winner-output").html(`${obj.driver.name} in the ${obj.car.make} ${obj.car.model}`);
  }

  $("#race-button").click(function() {
    declareWinner(RacingLeague.determineWinner({
      driver: getDriver($("#driver1").val()),
      car: getCar($("#car1").val())
    }, {
      driver: getDriver($("#driver2").val()),
      car: getCar($("#car2").val())
    }));
  });
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<body>
  <div id="score-board">
  </div>
  <hr>
  <div id="select-racers">
    <select class="select-driver" id="driver1">
    </select>
    in the
    <select class="select-car" id="car1">
    </select>
    <p>VS</p>
    <select class="select-driver" id="driver2">
    </select>
    in the
    <select class="select-car" id="car2">
    </select>
  </div>
  <br>
  <button id="race-button">Race!</button>
  <br>
  <br>
  <span>The winner is: </span><span id="winner-output"></span>
</body>

If you have any suggestions for code included in the snippet, I would sincerely appreciate it--I'm not the best front end developer; however, I am quite interested in improving my createBuilder factory method and in hearing discussion involving its pros, cons, and alternatives as well.


a jsfiddle link to the project

my question was somewhat inspired by this one

\$\endgroup\$
  • 1
    \$\begingroup\$ This feels over-engineered to me. It's not clear to me what problem you are trying to solve with your builder classes. For example, what advantage does your driverBuilder have over simply creating an array of the object literals you need? \$\endgroup\$ – Jonah May 15 '16 at 4:31
1
\$\begingroup\$

This looks good, except that by resetting the builder, you are throwing away the parameters that you need to build your objects. Yet, parametric construction of objects is the main objective of the Builder design pattern.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Hmm, I think you may have a point. I've never implemented a builder in JavaScript, so I was unsure of how to to 'turn off' the builder once it was finished creating. I decided that it may be useful to just reuse it, but then it occurred to me that a second user may not be sure that the builder is cleared, so I gave the user access to the reset method so they could clear it themself to be sure. A big problem arises if multiple users have access to the same reusable builder because them may both be editing it at the same time. \$\endgroup\$ – Joshua Dawson May 13 '16 at 20:20
  • \$\begingroup\$ When a builder has been used, it should be discarded, and with it, its parameters. \$\endgroup\$ – RWRkeSBZ May 14 '16 at 17:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.