Chapter 3. Creational Design Patterns

Creational design patterns in object-oriented programming are design patterns that are to be applied during the instantiation of objects. In this chapter, we'll be talking about patterns in this category.

Consider we are building a rocket, which has payload and one or more stages:

class Payload { 
  weight: number; 
} 
 
class Engine { 
  thrust: number; 
} 
 
class Stage { 
  engines: Engine[]; 
} 

In old-fashioned JavaScript, there are two major approaches to building such a rocket:

  • Constructor with new operator
  • Factory function

For the first approach, things could be like this:

function Rocket() { 
  this.payload = { 
    name: 'cargo ship' 
  }; 
   
  this.stages = [ 
    { 
      engines: [ 
        // ... 
      ] 
    } 
  ]; 
} 
 
var rocket = new Rocket(); 

And for the second approach, it could be like this:

function buildRocket() { 
  var rocket = {}; 
   
  rocket.payload = { 
    name: 'cargo ship' 
  }; 
   
  rocket.stages = [ 
    { 
      thrusters: [ 
        // ... 
      ] 
    } 
  ]; 
   
  return rocket; 
} 
 
var rocket = buildRocket(); 

From a certain angle, they are doing pretty much the same thing, but semantically they differ a lot. The constructor approach suggests a strong association between the building process and the final product. The factory function, on the other hand, implies an interface of its product and claims the ability to build such a product.

However, neither of the preceding implementations provides the flexibility to modularly assemble rockets based on specific needs; this is what creational design patterns are about.

In this chapter, we'll cover the following creational patterns:

  • Factory method: By using abstract methods of a factory instead of the constructor to build instances, this allows subclasses to change what's built by implementing or overriding these methods.
  • Abstract factory: Defining the interface of compatible factories and their products. Thus by changing the factory passed, we can change the family of built products.
  • Builder: Defining the steps of building complex objects, and changing what's built either by changing the sequence of steps, or using a different builder implementation.
  • Prototype: Creating objects by cloning parameterized prototypes. Thus by replacing these prototypes, we may build different products.
  • Singleton: Ensuring only one instance (under a certain scope) will be created.

It is interesting to see that even though the factory function approach to creating objects in JavaScript looks primitive, it does have parts in common with some patterns we are going to talk about (although applied to different scopes).

Factory method

Under some scenarios, a class cannot predict exactly what objects it will create, or its subclasses may want to create more specified versions of these objects. Then, the Factory Method Pattern can be applied.

The following picture shows the possible structure of the Factory Method Pattern applied to creating rockets:

Factory method

A factory method is a method of a factory that builds objects. Take building rockets as an example; a factory method could be a method that builds either the entire rocket or a single component. One factory method might rely on other factory methods to build its target object. For example, if we have a createRocket method under the Rocket class, it would probably call factory methods like createStages and createPayload to get the necessary components.

The Factory Method Pattern provides some flexibility upon reasonable complexity. It allows extendable usage by implementing (or overriding) specific factory methods. Taking createStages method, for example, we can create a one-stage rocket or a two-stage rocket by providing different createStages method that return one or two stages respectively.

Participants

The participants of a typical Factory Method Pattern implementation include the following:

  • Product: Rocket

Define an abstract class or an interface of a rocket that will be created as the product.

  • Concrete product: FreightRocket

Implement a specific rocket product.

  • Creator: RocketFactory

Define the optionally abstract factory class that creates products.

  • Concrete creator: FreightRocketFactory

Implement or overrides specific factory methods to build products on demand.

Pattern scope

The Factory Method Pattern decouples Rocket from the constructor implementation and makes it possible for subclasses of a factory to change what's built accordingly. A concrete creator still cares about what exactly its components are and how they are built. But the implementation or overriding usually focuses more on each component, rather than the entire product.

Implementation

Let's begin with building a simple one-stage rocket that carries a 0-weight payload as the default implementation:

class RocketFactory { 
  buildRocket(): Rocket { } 
   
  createPayload(): Payload { } 
   
  createStages(): Stage[] { } 
} 

We start with creating components. We will simply return a payload with 0 weight for the factory method createPayload and one single stage with one single engine for the factory method createStages:

createPayload(): Payload { 
  return new Payload(0); 
} 
 
createStages(): Stage[] { 
  let engine = new Engine(1000); 
  let stage = new Stage([engine]); 
   
  return [stage]; 
} 

After implementing methods to create the components of a rocket, we are going to put them together with the factory method buildRocket:

buildRocket(): Rocket { 
  let rocket = new Rocket(); 
   
  let payload = this.createPayload(); 
  let stages = this.createStages(); 
   
  rocket.payload = payload; 
  rocket.stages = stages; 
   
  return rocket; 
} 

Now we have the blueprint of a simple rocket factory, yet with certain extensibilities. To build a rocket (that does nothing so far), we just need to instantiate this very factory and call its buildRocket method:

let rocketFactory = new RocketFactory(); 
let rocket = rocketFactory.buildRocket(); 

Next, we are going to build two-stage freight rockets that send satellites into orbit. Thus, there are some differences compared to the basic factory implementation.

First, we have a different payload, satellites, instead of a 0-weight placeholder:

class Satellite extends Payload { 
  constructor( 
    public id: number 
  ) { 
    super(200); 
  } 
} 

Second, we now have two stages, probably with different specifications. The first stage is going to have four engines:

class FirstStage extends Stage { 
  constructor() { 
    super([ 
      new Engine(1000), 
      new Engine(1000), 
      new Engine(1000), 
      new Engine(1000) 
    ]); 
  } 
} 

While the second stage has only one:

class SecondStage extends Stage { 
  constructor() { 
    super([ 
      new Engine(1000) 
    ]); 
  } 
} 

Now we have what this new freight rocket would look like in mind, let's extend the factory:

type FreightRocketStages = [FirstStage, SecondStage]; 
 
class FreightRocketFactory extends RocketFactory { 
  createPayload(): Satellite { } 
   
  createStages(): FreightRocketStages { } 
} 

Tip

Here we are using the type alias of a tuple to represent the stages sequence of a freight rocket, namely the first and second stages. To find out more about type aliases, please refer to https://www.typescriptlang.org/docs/handbook/advanced-types.html.

As we added the id property to Satellite, we might need a counter for each instance of the factory, and then create every satellite with a unique ID:

nextSatelliteId = 0; 
 
createPayload(): Satellite { 
  return new Satellite(this.nextSatelliteId++); 
} 

Let's move on and implement the createStages method that builds first and second stage of the rocket:

createStages(): FreightRocketStages { 
  return [ 
    new FirstStage(), 
    new SecondStage() 
  ]; 
} 

Comparing to the original implementation, you may have noticed that we've automatically decoupled specific stage building processes from assembling them into constructors of different stages. It is also possible to apply another creational pattern for the initiation of every stage if it helps.

Consequences

In the preceding implementation, the factory method buildRocket handles the outline of the building steps. We were lucky to have the freight rocket in the same structure as the very first rocket we had defined.

But that won't always happen. If we want to change the class of products (Rocket), we'll have to override the entire buildRocket with everything else but the class name. This looks frustrating but it can be solved, again, by decoupling the creation of a rocket instance from the building process:

buildRocket(): Rocket { 
  let rocket = this.createRocket(); 
   
  let payload = this.createPayload(); 
  let stages = this.createStages(); 
   
  rocket.payload = payload; 
  rocket.stages = stages; 
   
  return rocket; 
} 
 
createRocket(): Rocket { 
  return new Rocket(); 
} 

Thus we can change the rocket class by overriding the createRocket method. However, the return type of the buildRocket of a subclass (for example, FreightRocketFactory) is still Rocket instead of something like FreightRocket. But as the object created is actually an instance of FreightRocket, it is valid to cast the type by type assertion:

let rocket = FreightRocketFactory.buildRocket() as FreightRocket; 

The trade-off is a little type safety, but that can be eliminated using generics. Unfortunately, in TypeScript what you get from a generic type argument is just a type without an actual value. This means that we may need another level of abstraction or other patterns that can use the help of type inference to make sure of everything.

The former option would lead us to the Abstract Factory Pattern.

Note

Type safety could be one reason to consider when choosing a pattern but usually, it will not be decisive. Please note we are not trying to switch a pattern for this single reason, but just exploring.

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

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