Builder

While Factory Patterns expose the internal components (such as the payload and stages of a rocket), the Builder Pattern encapsulates them by exposing only the building steps and provides the final products directly. At the same time, the Builder Pattern also encapsulates the internal structures of a product. This makes it possible for a more flexible abstraction and implementation of building complex objects.

The Builder Pattern also introduces a new role called director, as shown in the following diagram. It is quite like the client in the Abstract Factory Pattern, although it cares only about build steps or pipelines:

Builder

Now the only constraint from RocketBuilder that applies to a product of its subclass is the overall shape of a Rocket. This might not bring a lot of benefits with the Rocket interface we previously defined, which exposes some details of the rocket that the clients (by clients I mean those who want to send their satellites or other kinds of payload to space) may not care about that much. For these clients, what they want to know might just be which orbit the rocket is capable of sending their payloads to, rather than how many and what stages this rocket has.

Participants

The participants of a typical Builder Pattern implementation include the following:

  • Builder: RocketBuilder

Defines the interface of a builder that builds products.

  • Concrete builder: FalconBuilder

Implements methods that build parts of the products, and keeps track of the current building state.

  • Director

Defines the steps and collaborates with builders to build products.

  • Final product: Falcon

The product built by a builder.

Pattern scope

The Builder Pattern has a similar scope to the Abstract Factory Pattern, which extracts abstraction from a complete collection of operations that will finally initiate the products. Compared to the Abstract Factory Pattern, a builder in the Builder Pattern focuses more on the building steps and the association between those steps, while the Abstract Factory Pattern puts that part into the clients and makes its factory focus on producing components.

Implementation

As now we are assuming that stages are not the concern of the clients who want to buy rockets to carry their payloads, we can remove the stages property from the general Rocket interface:

interface Rocket { 
  payload: Payload; 
} 

There is a rocket family called sounding rocket that sends probes to near space. And this means we don't even need to have the concept of stages. SoundingRocket is going to have only one engine property other than payload (which will be a Probe), and the only engine will be a SolidRocketEngine:

class Probe implements Payload { 
  weight: number; 
} 
 
class SolidRocketEngine extends Engine { } 
 
class SoundingRocket implements Rocket { 
  payload: Probe; 
  engine: SolidRocketEngine; 
} 

But still we need rockets to send satellites, which usually use LiquidRocketEngine:

class LiquidRocketEngine extends Engine { 
  fuelLevel = 0; 
   
  refuel(level: number): void { 
    this.fuelLevel = level; 
  } 
} 

And we might want to have the corresponding  LiquidRocketStage abstract class that handles refuelling:

abstract class LiquidRocketStage implements Stage { 
  engines: LiquidRocketEngine[] = []; 
   
  refuel(level = 100): void { 
    for (let engine of this.engines) { 
      engine.refuel(level); 
    } 
  } 
} 

Now we can update  FreightRocketFirstStage and FreightRocketSecondStage as subclasses of LiquidRocketStage:

class FreightRocketFirstStage extends LiquidRocketStage { 
  constructor(thrust: number) { 
    super(); 
     
    let enginesNumber = 4; 
    let singleEngineThrust = thrust / enginesNumber; 
     
    for (let i = 0; i < enginesNumber; i++) { 
      let engine = 
        new LiquidRocketEngine(singleEngineThrust); 
      this.engines.push(engine); 
    } 
  } 
} 
 
class FreightRocketSecondStage extends LiquidRocketStage { 
  constructor(thrust: number) { 
    super(); 
    this.engines.push(new LiquidRocketEngine(thrust)); 
  } 
} 

The FreightRocket will remain the same as it was:

type FreightRocketStages = 
  [FreightRocketFirstStage, FreightRocketSecondStage]; 
 
class FreightRocket implements Rocket { 
  payload: Satellite; 
  stages = [] as FreightRocketStages; 
} 

And, of course, there is the builder. This time, we are going to use an abstract class that has the builder partially implemented, with generics applied:

abstract class RocketBuilder< 
  TRocket extends Rocket, 
  TPayload extends Payload 
> { 
  createRocket(): void { } 
   
  addPayload(payload: TPayload): void { } 
   
  addStages(): void { } 
   
  refuelRocket(): void { } 
   
  abstract get rocket(): TRocket; 
} 

Note

There's actually no abstract method in this abstract class. One of the reasons is that specific steps might be optional to certain builders. By implementing no-op methods, the subclasses can just leave the steps they don't care about empty.

Here is the implementation of the Director class:

class Director { 
  prepareRocket< 
    TRocket extends Rocket, 
    TPayload extends Payload 
  >( 
    builder: RocketBuilder<TRocket, TPayload>, 
    payload: TPayload 
  ): TRocket { 
    builder.createRocket(); 
    builder.addPayload(payload); 
    builder.addStages(); 
    builder.refuelRocket(); 
    return builder.rocket; 
  } 
} 

Note

Be cautious, without explicitly providing a building context, the builder instance relies on the building pipelines being queued (either synchronously or asynchronously). One way to avoid risk (especially with asynchronous operations) is to initialize a builder instance every time you prepare a rocket.

Now it's time to implement concrete builders, starting with SoundingRocketBuilder, which builds a SoundingRocket with only one SolidRocketEngine:

class SoundingRocketBuilder 
extends RocketBuilder<SoundingRocket, Probe> { 
  private buildingRocket: SoundingRocket; 
   
  createRocket(): void { 
    this.buildingRocket = new SoundingRocket(); 
  } 
   
  addPayload(probe: Probe): void { 
    this.buildingRocket.payload = probe; 
  } 
   
  addStages(): void { 
    let payload = this.buildingRocket.payload; 
    this.buildingRocket.engine = 
      new SolidRocketEngine(payload.weight); 
  } 
   
  get rocket(): SoundingRocket { 
    return this.buildingRocket; 
  } 
} 

There are several notable things in this implementation:

  • The addStages method relies on the previously added payload to add an engine with the correct thrust specification.
  • The refuel method is not overridden (so it remains no-op) because a solid rocket engine does not need to be refueled.

We've sensed a little about the context provided by a builder, and it could have a significant influence on the result. For example, let's take a look at FreightRocketBuilder. It could be similar to SoundingRocket if we don't take the addStages and refuel methods into consideration:

class FreightRocketBuilder 
extends RocketBuilder<FreightRocket, Satellite> { 
  private buildingRocket: FreightRocket; 
   
  createRocket(): void { 
    this.buildingRocket = new FreightRocket(); 
  } 
   
  addPayload(satellite: Satellite): void { 
    this.buildingRocket.payload = satellite; 
  } 
   
  get rocket(): FreightRocket { 
    return this.buildingRocket; 
  } 
} 

Assume that a payload that weighs less than 1000 takes only one stage to send into space, while payloads weighing more take two or more stages:

addStages(): void { 
  let rocket = this.buildingRocket; 
  let payload = rocket.payload; 
  let stages = rocket.stages; 
   
  stages[0] = new FreightRocketFirstStage(payload.weight * 4); 
   
  if (payload.weight >= FreightRocketBuilder.oneStageMax) { 
    stages[1] = FreightRocketSecondStage(payload.weight); 
  } 
} 
 
static oneStageMax = 1000; 

When it comes to refueling, we can even decide how much to refuel based on the weight of the payloads:

refuel(): void { 
  let rocket = this.buildingRocket; 
  let payload = rocket.payload; 
  let stages = rocket.stages; 
   
  let oneMax = FreightRocketBuilder.oneStageMax; 
  let twoMax = FreightRocketBuilder.twoStagesMax; 
   
  let weight = payload.weight; 
   
  stages[0].refuel(Math.min(weight, oneMax) / oneMax * 100); 
   
  if (weight >= oneMax) { 
    stages[1] 
      .refuel((weight - oneMax) / (twoMax - oneMax) * 100); 
  } 
} 
 
static oneStageMax = 1000; 
static twoStagesMax = 2000; 

Now we can prepare different rockets ready to launch, with different builders:

let director = new Director(); 
 
let soundingRocketBuilder = new SoundingRocketBuilder(); 
let probe = new Probe(); 
let soundingRocket 
  = director.prepareRocket(soundingRocketBuilder, probe); 
 
let freightRocketBuilder = new FreightRocketBuilder(); 
let satellite = new Satellite(0, 1200); 
let freightRocket 
  = director.prepareRocket(freightRocketBuilder, satellite); 

Consequences

As the Builder Pattern takes greater control of the product structures and how the building steps influence each other, it provides the maximum flexibility by subclassing the builder itself, without changing the director (which plays a similar role to a client in the Abstract Factory Pattern).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.234.150