Ember Handbook

Builder Pattern

Intent

Builder is a creational pattern providing you a construction code to produce different types and representations of an object.

Structure

A Builder interface describes the construction pieces common to all types of builders. A concrete implementation of that interface provides the shapes and faces relative for that builder. A Director knows how to use the pieces and assemble the representation for the given object.

The participants are (Gamma, Helm, Johnson and Vlissidies, 1994, p. 99):

Builder
Specifies an abstract interface for the creating parts of a Product object.
ConcreteBuilder
Knows how to build individual pieces by implementing the Builder interface.
Director
Constructs an object using the Builder interface.
Product
The complex object that is represented with the Builder interface. The product knows its internal representation and includes classes that define the constituent parts, including interfaces for assembling parts to the final result.

Object Oriented Implementation

On top of it a Director can be used, that knows how to call the build* in the correct order. Here is how:

class CardDirector {
  construct(type: string, builder: CardBuilder) {
    builder.reset();

    switch (type) {
      case "simple":
        builder.buildBody();
        break;

      case "dialog":
        builder.buildHeader();
        builder.buildBody();
        builder.buildFooter();
        break;
    }

    return builder.getCard();
  }
}

Declarative Implementation

A composable component is best suited to implement the builder pattern declaratively. The yielded elements are pieces to construct different shapes of a particular type. The composable component can act as a director itself with the simple case being the default. The participants mentioned above are spread a bit differently and are as follows:

Builder
The builder interface defines the yield of a concrete builder component. Since this is not officially supported in ember, typescript can help here to let us define an interface for it, yet apart from documentation purpose there is no other benefit from doing so (see component.ts below).
ConcreteBuilder
The component, that yields in regards to the builder interface.
Director
Can be the ConcreteBuilderComponent itself or a separate component, that wraps the Builder, see subsection Using a Director below.
Product
The object being represented by the with the ConcreteBuilder, see subsection Using a Director below.

Remark: header, footer and body have the same template, only the css class is different (that’s why there is only one template shown).

The simplest use-case (since it is a conceptual component, we give it a sensible default):

Simplest Card
<Card>
  Simplest Card
</Card>

Using the pieces from the builder:

Header
Card body
<Card as |cb|>
  <cb.Header>
    Header
  </cb.Header>
  <cb.Body>
    Card body
  </cb.Body>
  <cb.Footer>
    Footer
  </cb.Footer>
</Card>

The Director is optional, since the <Card> is one itself (for the default case),

Future Additions

This pattern allows for future additions, without breaking the existing code.

  1. Adding more elements by extendng the <Card> with a <Card::List> component to provide a piece for lists within a card. Even further the list can be a builder pattern on its own, yielding prieces to create individual items.

  2. Exchanging the UI part. In the given example, the <Card> component is used, which can be easily replaced with another component, e.g. <MaterialCard> as long as both implement the CardYield interface.

    Combine it with the strategy pattern to pass in the respective builder components.

Using a Director

Using the <CardDirectory> as a wrapper to take the @builder as input and yield the ready-to-use variable to construct your own object.

<CardDirector @builder={{component "material-card"}} as |cb|>
  <cb.Header>...</cb.Header>
  ...
</CardDirector>

The tradeoff here is, the logic to control the order is not be part of the <CardDirector> itself but must still be handled from within the context. The <CardDirector> as its own component is only the passthrough from passed in @builder.

Better approach is to integrate the director as part of the context, e.g. a component handling business logic depending on the object being passed in:

<ProductCard @product={{@product}} />

The <ProductCard> will become the director and depending on the @product knows how to assemble the pieces in the correct order to accordingly represent the product.

Applicability

  • Use the builder pattern when you want to be able to create different shapes and representations.
  • Works in harmony with the composable component.
  • Nesting works smoothly on the declarative approach.
  • Easily extendable with more pieces.