985384732a | ||
---|---|---|
.. | ||
config | ||
src/webparts/builder | ||
.editorconfig | ||
.gitignore | ||
.yo-rc.json | ||
README.md | ||
gulpfile.js | ||
package-lock.json | ||
package.json | ||
tsconfig.json |
README.md
Builder Design Pattern
Summary
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.
Compatibility
Applies to
Prerequisites
N/A
Solution
Solution | Author(s) |
---|---|
designpatterns-typescript\builder | @levalencia |
Version history
Version | Date | Comments |
---|---|---|
1.0 | May 15, 2018 | Initial release |
Disclaimer
THIS CODE IS PROVIDED AS IS WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
Builder pattern
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/design_pattern/builder_pattern.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();
}
returnStr += ", Total: " + this.getCost();
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;
IBuilderProps.ts
We created a selectedMeal string property to take the decision on which meal to build.
export interface IBuilderProps {
selectedMeal: string;
}
Builder.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 './Builder.module.scss';
import { IBuilderProps } from './IBuilderProps';
import { escape } from '@microsoft/sp-lodash-subset';
import MealBuilder from "./MealBuilder";
import Meal from "./Meal";
import { IPropertyPaneConfiguration } from "@microsoft/sp-webpart-base";
import {
PropertyPaneDropdown
} from "@microsoft/sp-webpart-base";
import {Version} from "@microsoft/sp-core-library";
import { IBuilderState } from './IBuilderState';
export default class Builder extends React.Component<IBuilderProps, IBuilderState> {
private mealBuilder: MealBuilder ;
private items: string;
private meal: Meal;
constructor(props: IBuilderProps, state: IBuilderState) {
super(props);
this.setInitialState();
//this.setMeal = this.setMeal.bind(this);
this.mealBuilder = new MealBuilder();
this.setMeal(props.selectedMeal);
}
public setInitialState(): void {
this.state = {
items: ""
};
}
public componentWillReceiveProps(nextProps: IBuilderProps): void {
if(nextProps.selectedMeal !== this.props.selectedMeal) {
this.setMeal(nextProps.selectedMeal);
}
}
public render(): React.ReactElement<IBuilderProps> {
return (
<div className={styles.builder}>
<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 Mac Luis!</span>
<p className="ms-font-l ms-fontColor-white">You have selected the following.</p>
<div> {this.state.items}</div>
</div>
</div>
</div>
</div>
);
}
protected get dataVersion(): Version {
return Version.parse("1.0");
}
private setMeal(selectedMeal: string): void {
if(selectedMeal===undefined){
this.meal = this.mealBuilder.prepareVegMeal();
}
if(selectedMeal === "0") {
this.meal = this.mealBuilder.prepareVegMeal();
}
if(selectedMeal === "1") {
this.meal = this.mealBuilder.prepareNonVegMeal();
}
this.state = {
items: this.meal.showItems(),
};
}
}
And finally
BuilderWebPart.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 'BuilderWebPartStrings';
import Builder from './components/Builder';
import { IBuilderProps } from './components/IBuilderProps';
export interface IBuilderWebPartProps {
selectedMeal: string;
}
export default class BuilderWebPart extends BaseClientSideWebPart<IBuilderWebPartProps> {
public render(): void {
const element: React.ReactElement<IBuilderProps > = React.createElement(
Builder,
{
selectedMeal: this.properties.selectedMeal
}
);
ReactDom.render(element, this.domElement);
}
protected onPropertyPaneFieldChanged(propertyPath: string, oldValue: any, newValue: any): void {
this.properties[this.properties.selectedMeal] = newValue;
this.render();
super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
}
protected get dataVersion(): Version {
return Version.parse("1.0");
}
protected get disableReactivePropertyChanges(): boolean {
return true;
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: "Header"
},
groups: [
{
groupName: "Group",
groupFields: [
PropertyPaneDropdown("selectedMeal", {
label: "Select meal",
options: [
{ key: "0", text: "Veg" },
{ key: "1", text: "Nonveg" }
],
selectedKey: 0
})
]
}
]
}
]
};
}
}
Data source implementation is left to the reader