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
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
About the Author:
Luis Valencia, CTO at Software Estrategico, Medellin, Colombia, independent blogger and still a coder, after 17 years of experience in the field and regardless of my position, and mostly with SharePoint/Office Products, I still love to code, open Visual Studio and bring solutions to users and to the community its what makes me wake up every morning.
Feel free to contact me via twitter direct messages, @levalencia
Reference:
Valencia, L (2018). Typescript design patterns for SharePoint Framework Part 3 – Builder. Available at: http://www.luisevalencia.com/2018/03/19/typescript-design-patterns-for-sharepoint-framework-part-3-builder/ [Accessed 12 November 2018]