Typescript design patterns for Sharepoint Framework Part 3 - Builder

Builder pattern builds a complex object using simple objects and using a step by step approach. This type of design pattern comes under creational pattern as this pattern provides one of the best ways to create an object.

A Builder class builds the final object step by step. This builder is independent of other objects.

For this pattern, we have taken an existing example https://www.tutorialspoint.com/designpattern/builderpattern.htm and translated it to Typescript. Data Access implementation details are left to the reader.

The idea on this example is to show how you can build a Complex object from single objects, a Meal from (burger, fries, soda). Suppose you have a Sharepoint List for Burgers, another list for Sodas, another one for desserts, and you want to build different Meals (Menus), so this would be a perfect sample.

UML

This is more or less the diagram of the classes were are coding below.

Project structure

We have created a component with all the needed class, lets discuss them one by one.

IItem.ts

This interface is the one that every item needs to implement to come with a common structure for all products.

import IPacking from "./IPacking";

interface IItem {  
    name(): string;
    packing(): IPacking;
    price(): number;
}

export default IItem;  
Ipacking.ts

This interface is the one that all packaging will use, eg: Bottle, Wrapper, etc, its the way to define common behavior and properties for each product packing.

interface IPacking {  
    pack(): string;
}

export default IPacking;  
Bottle.ts

This is one type of packing, it implements the IPacking interface.

import IPacking from "./IPacking";

class Bottle implements IPacking {  
    public pack(): string {
       return "Bottle";
    }
}

export default Bottle;  
Wrapper.ts
import IPacking from "./IPacking";

class Wrapper implements IPacking {  
    public pack(): string {
       return "Wrapper";
    }
}

export default Wrapper;  
Burger.ts

This is an abstract class from which all our specific burgers need to implement, its there to have a common structure for name, packing and pricing.

import IItem from "./IItem";  
import Wrapper from "./Wrapper";  
import IPacking from "./IPacking";

abstract class Burger implements IItem {  
    public name(): string {
        throw new Error("Method not implemented.");
    }

    public packing(): IPacking {
        return new Wrapper();
    }

    public abstract price(): number ;

}

export default Burger;  
ChickenBurger.ts
import Burger from "./Burger";

class ChickenBurger extends Burger {  
    public price(): number {
        return 15;
    }

    public name(): string {
        return "Chicken Burger";
    }
}

export default ChickenBurger;
VegBurger.ts
import Burger from "./Burger";

class VegBurger extends Burger {  
    public price(): number {
        return 11;
    }

    public name(): string {
        return "Veg Burger";
    }
}

export default VegBurger;
Colddrink.ts
import IItem from "./IItem";  
import IPacking from "./IPacking";  
import Bottle from "./Bottle";

abstract class ColdDrink implements IItem {  
    public name(): string {
        throw new Error("Method not implemented.");
    }
    public packing(): IPacking {
        return new Bottle();
    }

    public abstract price(): number ;

}

export default ColdDrink;
Coke.ts
import ColdDrink from "./ColdDrink";

class Coke extends ColdDrink {  
    public price(): number {
       return 2.5;
    }

    public name(): string {
        return "Coca Cola";
    }
}

export default Coke;
Pepsi.ts
import ColdDrink from "./ColdDrink";

class Pepsi extends ColdDrink {  
    public price(): number {
       return 1.5;
    }

    public name(): string {
        return "Pepsi Cola";
    }
}

export default Pepsi;
Meal.ts

This class will represent a full meal behavior, here we have the methods to add items to the Meal, get the cost and show the items belonging to the Meal.

import IItem from "./IItem";

class Meal {  
    private items: IItem[];

    public addItem(item: IItem): void {
        this.items.push(item);
    }

    public getCost(): number {
        let cost: number  = 0;
        for(let item of this.items) {
            cost+= item.price();
        }

        return cost;
    }

    public showItems(): string {
        let returnStr: string;
        for(let item of this.items) {
            returnStr +="Item:" + item.name;
            returnStr +=", Packing:" + item.packing().pack();
            returnStr +=", Price: " + item.price();
        }
        return returnStr;
    }
}

export default Meal;  
MealBuilder.ts

Mealbuilder its just the class that uses the classes explained before to construct any type of meal, for sake of simplicity, we created only 2 meals here.

import Meal from "./Meal";  
import VegBurger from "./VegBurger";  
import Coke from "./Coke";  
import ChickenBurger from "./ChickenBurger";

class MealBuilder {  
    public prepareVegMeal(): Meal {
        let meal: Meal= new Meal();
        meal.addItem(new VegBurger());
        meal.addItem(new Coke());
        return meal;
    }

    public prepareNonVegMeal(): Meal {
        let meal: Meal= new Meal();
        meal.addItem(new ChickenBurger());
        meal.addItem(new Coke());
        return meal;
    }
}

export default MealBuilder;  
ITypescriptDesignPatterns03BuilderProps.ts

We created a selectedMeal string property to take the decision on which meal to build.

export interface ITypescriptDesignPatterns03BuilderProps {  
  description: string;
  selectedMeal: string;
}
TypescriptDesignPatterns03Builder.tsx

This is our component class, here we have a constructor and in the constructor we call the setMeal method, with the selected meal option as a parameter, and then we can define which meal to prepare. Once the meal is prepared, in the render method we can use the showItems method

import * as React from "react";  
import styles from "./TypescriptDesignPatterns03Builder.module.scss";  
import { ITypescriptDesignPatterns03BuilderProps } from "./ITypescriptDesignPatterns03BuilderProps";  
import { escape } from "@microsoft/sp-lodash-subset";  
import MealBuilder from "./MealBuilder";  
import Meal from "./Meal";  
import { IPropertyPaneConfiguration } from "@microsoft/sp-webpart-base/lib/propertyPane/propertyPane/IPropertyPane";  
import {  
  PropertyPaneDropdown
} from "@microsoft/sp-webpart-base";
import Version from "@microsoft/sp-core-library/lib/Version";

export default class TypescriptDesignPatterns03Builder extends React.Component<ITypescriptDesignPatterns03BuilderProps, {}> {

  private mealBuilder: MealBuilder ;
  private items: string;
  private meal: Meal;

  constructor(props: ITypescriptDesignPatterns03BuilderProps, state: any) {
    super(props);
    this.setMeal(props.selectedMeal);
    this.mealBuilder = new MealBuilder();
  }

  public render(): React.ReactElement<ITypescriptDesignPatterns03BuilderProps> {
    return (
        <div className={styles.typescriptDesignPatterns03Builder}>
          <div className={styles.container}>
            <div className={`ms-Grid-row ms-bgColor-themeDark ms-fontColor-white ${styles.row}`}>
              <div className="ms-Grid-col ms-lg10 ms-xl8 ms-xlPush2 ms-lgPush1">
                <span className="ms-font-xl ms-fontColor-white">Welcome to Burger Company!</span>
                <p className="ms-font-l ms-fontColor-white">You have selected the following.</p>
                  <span className={styles.label}>{this.meal.showItems()}</span>
              </div>
            </div>
          </div>
        </div>
      );
    }

  protected get dataVersion(): Version {
    return Version.parse("1.0");
  }

  private setMeal(selectedMeal: string): void {
     if(selectedMeal === "VegMeal") {
        this.meal = this.mealBuilder.prepareVegMeal();
     }
     if(selectedMeal === "NonVegMeal") {
      this.meal = this.mealBuilder.prepareNonVegMeal();
   }
  }
}

And finally

TypescriptDesignPatterns03BuilderWebPart.ts

Here what we do is just to use our component and sending the parameter of the selected meal, which is just a normal dropdown with 2 hardcoded values.

import * as React from "react";  
import * as ReactDom from "react-dom";  
import { Version } from "@microsoft/sp-core-library";  
import {  
  BaseClientSideWebPart,
  IPropertyPaneConfiguration,
  PropertyPaneTextField,
  PropertyPaneDropdown
} from "@microsoft/sp-webpart-base";

import * as strings from "TypescriptDesignPatterns03BuilderWebPartStrings";  
import TypescriptDesignPatterns03Builder from "./components/TypescriptDesignPatterns03Builder";  
import { ITypescriptDesignPatterns03BuilderProps } from "./components/ITypescriptDesignPatterns03BuilderProps";  
import { ITypescriptDesignPatterns03BuilderWebPartProps } from "./ITypescriptDesignPatterns03BuilderWebPartProps";

export default class TypescriptDesignPatterns03BuilderWebPart extends  
  BaseClientSideWebPart<ITypescriptDesignPatterns03BuilderWebPartProps> {

  public render(): void {
    const element: React.ReactElement<ITypescriptDesignPatterns03BuilderProps > = React.createElement(
      TypescriptDesignPatterns03Builder,
      {
        description: this.properties.description,
        selectedMeal: this.properties.selectedMeal
      }
    );

    ReactDom.render(element, this.domElement);
  }

  protected get dataVersion(): Version {
    return Version.parse("1.0");
  }

  protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration  {
    return {
      pages: [
        {
          header: {
            description: "Header"
          },
          groups: [
            {
              groupName: "Group",
              groupFields: [
                PropertyPaneDropdown("meal", {
                  label: "Select meal",
                  options: [
                    { key: "Veg", text: "Veg" },
                    { key: "Nonveg", text: "Nonveg" }
                  ],
                  selectedKey: "Nonveg"
                })
              ]
            }
          ]
        }
      ]
    };
  }
}

Data source implementation is left to the reader.
This project is in my github repo: https://github.com/levalencia/TypescriptDesignPatterns03-Builder