Patrones de Diseno de Typescript aplicados a Sharepoint Framework - Contructor (Builder Pattern)

Escrito por  Luis Valencia

El patrón  constructor nos permite construir objetos complejos usando objetos simples de una manera sencilla y paso a paso.  Este patrón es de la categoría de patrones creaciones y nos provee una de las maneras mas sencillas para simplificar la creación de objetos.

La idea de este ejemplo es contruir un objeto complejo a partir de objetos sencillos, una Comida a partir del objeto hamburgesa, papas frías, gaseosa).   Supón que tienes una lista en SharePoint para almacenar hamburgesas, otra para gaseosas, otra para postres, y quieres construir diferentes tipos de menu, eso seria el ejemplo perfecto.

UML

El diagrama UML de lo que vamos a construir es el siguiente (Figura 1):

Imagen 1.- Digrama UML.

Estructura del Proyecto

Para el ejemplo hemos creado un componente con todas las clases necesarias y vamos a discutir las clases una por una.

Imagen 2.- Estructura del Proyecto.

IItem.ts

Esta interfaz es la que necesitan implementar todos los productos para que estos tengan una estructura común.

1Import Ipacking from “./Ipacking”;
2
1interface Iitem { 
2
1    name(): string;
2
1    packing(): Ipacking;
2
1    price(): number;
2
1}
2
1 
2
1export default Iitem; 
2

Ipacking.ts

Esta interfaz es la que necesitan implementar todos los tipos de Paquetes, por ejemplo botellas, bolsa, etc.  Es la forma que podríamos definir comportamiento y propiedades communes segun el tipo de paquete usado por el producto.

1interface IPacking { 
2
1    pack(): string;
2
1
2
1export default IPacking; 
2

Bottle.ts

Este es un tipo de paquete, implementa la interfaz IPacking.

1import IPacking from "./IPacking";
2
1class Bottle implements IPacking { 
2
1    public pack(): string {
2
1       return "Bottle";
2
1    }
2
1}
2
1export default Bottle; 
2

Wrapper.ts

1import IPacking from "./IPacking";
2
1class Wrapper implements IPacking { 
2
1    public pack(): string {
2
1       return "Wrapper";
2
1    }
2
1}
2
1export default Wrapper;
2

Burger.ts

Esta es la clase abstracta de la cual nuestras clases especificas tienen que implementar, esta clase existe para que todas las hamburguesas tengan una estructura y comportamiento comun.

1import IItem from "./IItem"; 
2
1import Wrapper from "./Wrapper"; 
2
1import IPacking from "./IPacking";
2
1abstract class Burger implements IItem { 
2
1    public name(): string {
2
1        throw new Error("Method not implemented.");
2
1    }
2
1    public packing(): IPacking {
2
1        return new Wrapper();
2
1    }
2
1    public abstract price(): number ;
2
1}
2
1export default Burger; 
2

ChickenBurger.ts

La hamburguesa de pollo, implementada los métodos necesarios.

1import Burger from "./Burger";
2
1class ChickenBurger extends Burger { 
2
1    public price(): number {
2
1        return 15;
2
1    }
2
1    public name(): string {
2
1        return "Chicken Burger";
2
1    }
2
1}
2
1export default ChickenBurger;
2

VegBurger.ts

La hamburguesa vegetariana, implementando los métodos necesarios.

1import Burger from "./Burger";
2
1class VegBurger extends Burger { 
2
1    public price(): number {
2
1        return 11;
2
1    }
2
1    public name(): string {
2
1        return "Veg Burger";
2
1    }
2
1}
2
1export default VegBurger;
2

Colddrink.ts

Todas las geseosas implementarían la clase abstracta de ColdDrink que define las propiedades y comportamiento común entre ellas.

1import IItem from "./IItem"; 
2
1import IPacking from "./IPacking"; 
2
1import Bottle from "./Bottle";
2
1abstract class ColdDrink implements IItem { 
2
1    public name(): string {
2
1        throw new Error("Method not implemented.");
2
1    }
2
1    public packing(): IPacking {
2
1        return new Bottle();
2
1    }
2
1    public abstract price(): number ;
2
1}
2
1export default ColdDrink;
2

Coke.ts

Una implementación de la clase anterior para definir su precio y nombre.

1import ColdDrink from "./ColdDrink";
2
1class Coke extends ColdDrink { 
2
1    public price(): number {
2
1       return 2.5;
2
1    }
2
1    public name(): string {
2
1        return "Coca Cola";
2
1    }
2
1}
2
1export default Coke;
2

Pepsi.ts

1import ColdDrink from "./ColdDrink";
2
1class Pepsi extends ColdDrink { 
2
1    public price(): number {
2
1       return 1.5;
2
1    }
2
1    public name(): string {
2
1        return "Pepsi Cola";
2
1    }
2
1}
2
1export default Pepsi;
2

Meal.ts

Esta clase representa el comportamiento completo para la construcción de un Menu, tiene métodos para agregar ítems al Menu, obtener el costo total, y mostrar los productos del menú.

1import IItem from "./IItem";
2
1class Meal { 
2
1    private items: IItem[];
2
1    public addItem(item: IItem): void {
2
1        this.items.push(item);
2
1    }
2
1    public getCost(): number {
2
1        let cost: number  = 0;
2
1        for(let item of this.items) {
2
1            cost+= item.price();
2
1        }
2
1 
2
1        return cost;
2
1    }
2
1    public showItems(): string {
2
1        let returnStr: string;
2
1        for(let item of this.items) {
2
1            returnStr +="Item:" + item.name;
2
1            returnStr +=", Packing:" + item.packing().pack();
2
1            returnStr +=", Price: " + item.price();
2
1        }
2
1        return returnStr;
2
1    }
2
1}
2
1export default Meal; 
2

MealBuilder.ts

Esta clase es la que construye los Menus, retornando un objeto tipo Meal para cada tipo de Menu, el cual tiene diferentes productos.  Por simplicidad se han creado solo 2 menus.

1import Meal from "./Meal"; 
2
1import VegBurger from "./VegBurger"; 
2
1import Coke from "./Coke"; 
2
1import ChickenBurger from "./ChickenBurger";
2
1class MealBuilder { 
2
1    public prepareVegMeal(): Meal {
2
1        let meal: Meal= new Meal();
2
1        meal.addItem(new VegBurger());
2
1        meal.addItem(new Coke());
2
1        return meal;
2
1    }
2
1    public prepareNonVegMeal(): Meal {
2
1        let meal: Meal= new Meal();
2
1        meal.addItem(new ChickenBurger());
2
1        meal.addItem(new Coke());
2
1        return meal;
2
1    }
2
1}
2
1export default MealBuilder; 
2

ITypescriptDesignPatterns03BuilderProps.ts

Hemos creado una propiedad que nos guardara la decisión tomada por el usuario final (selectedMeal), y la agregamos a las “Props” del componente React.

1export interface ITypescriptDesignPatterns03BuilderProps { 
2
1  description: string;
2
1  selectedMeal: string;
2
1}
2

TypescriptDesignPatterns03Builder.tsx

Esta es nuestra clase de componente, aquí tenemos un constructor y en el constructor llamamos el método SetMeal, en el cual enviamos la opción elegida por el usuario como parámetro. Una vez el menú esta construido, en el método render podemos utilizar el método showItems para mostrar lo seleccionado.

1import * as React from "react"; 
2
1import styles from "./TypescriptDesignPatterns03Builder.module.scss"; 
2
1import { ITypescriptDesignPatterns03BuilderProps } from "./ITypescriptDesignPatterns03BuilderProps"; 
2
1import { escape } from "@microsoft/sp-lodash-subset"; 
2
1import MealBuilder from "./MealBuilder"; 
2
1import Meal from "./Meal"; 
2
1import { IPropertyPaneConfiguration } from "@microsoft/sp-webpart-base/lib/propertyPane/propertyPane/IPropertyPane"; 
2
1import { 
2
1  PropertyPaneDropdown
2
1} from "@microsoft/sp-webpart-base";
2
1import Version from "@microsoft/sp-core-library/lib/Version";
2
1export default class TypescriptDesignPatterns03Builder extends React.Component<ITypescriptDesignPatterns03BuilderProps, {}> {
2
1  private mealBuilder: MealBuilder ;
2
1  private items: string;
2
1  private meal: Meal;
2
1  constructor(props: ITypescriptDesignPatterns03BuilderProps, state: any) {
2
1    super(props);
2
1    this.setMeal(props.selectedMeal);
2
1    this.mealBuilder = new MealBuilder();
2
1  }
2
1  public render(): React.ReactElement<ITypescriptDesignPatterns03BuilderProps> {
2
1    return (
2
1        <div className={styles.typescriptDesignPatterns03Builder}>
2
1          <div className={styles.container}>
2
1            <div className={`ms-Grid-row ms-bgColor-themeDark ms-fontColor-white ${styles.row}`}>
2
1              <div className="ms-Grid-col ms-lg10 ms-xl8 ms-xlPush2 ms-lgPush1">
2
1                <span className="ms-font-xl ms-fontColor-white">Welcome to Burger Company!</span>
2
1                <p className="ms-font-l ms-fontColor-white">You have selected the following.</p>
2
1                  <span className={styles.label}>{this.meal.showItems()}</span>
2
1              </div>
2
1            </div>
2
1          </div>
2
1        </div>
2
1      );
2
1    }
2
1  protected get dataVersion(): Version {
2
1    return Version.parse("1.0");
2
1  }
2
1  private setMeal(selectedMeal: string): void {
2
1     if(selectedMeal === "VegMeal") {
2
1        this.meal = this.mealBuilder.prepareVegMeal();
2
1     }
2
1     if(selectedMeal === "NonVegMeal") {
2
1      this.meal = this.mealBuilder.prepareNonVegMeal();
2
1   }
2
1  }
2
1}
2

Y finalmente

TypescriptDesignPatterns03BuilderWebPart.ts

Esta es la clase del webpart que genera Sharepoint Framework, aca lo único que hacemos es definir la propiedad de tipo DropDown, y luego utilizar el componente con la opción seleccionada.

1import * as React from "react"; 
2
1import * as ReactDom from "react-dom"; 
2
1import { Version } from "@microsoft/sp-core-library"; 
2
1import { 
2
1  BaseClientSideWebPart,
2
1  IPropertyPaneConfiguration,
2
1  PropertyPaneTextField,
2
1  PropertyPaneDropdown
2
1} from "@microsoft/sp-webpart-base";
2
1import * as strings from "TypescriptDesignPatterns03BuilderWebPartStrings"; 
2
1import TypescriptDesignPatterns03Builder from "./components/TypescriptDesignPatterns03Builder"; 
2
1import { ITypescriptDesignPatterns03BuilderProps } from "./components/ITypescriptDesignPatterns03BuilderProps"; 
2
1import { ITypescriptDesignPatterns03BuilderWebPartProps } from "./ITypescriptDesignPatterns03BuilderWebPartProps";
2
1export default class TypescriptDesignPatterns03BuilderWebPart extends 
2
1  BaseClientSideWebPart<ITypescriptDesignPatterns03BuilderWebPartProps> {
2
1  public render(): void {
2
1    const element: React.ReactElement<ITypescriptDesignPatterns03BuilderProps > = React.createElement(
2
1      TypescriptDesignPatterns03Builder,
2
1      {
2
1        description: this.properties.description,
2
1        selectedMeal: this.properties.selectedMeal
2
1      }
2
1    );
2
1    ReactDom.render(element, this.domElement);
2
1  }
2
1  protected get dataVersion(): Version {
2
1    return Version.parse("1.0");
2
1  }
2
1  protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration  {
2
1    return {
2
1      pages: [
2
1        {
2
1          header: {
2
1            description: "Header"
2
1          },
2
1          groups: [
2
1            {
2
1              groupName: "Group",
2
1              groupFields: [
2
1                PropertyPaneDropdown("meal", {
2
1                  label: "Select meal",
2
1                  options: [
2
1                    { key: "Veg", text: "Veg" },
2
1                    { key: "Nonveg", text: "Nonveg" }
2
1                  ],
2
1                  selectedKey: "Nonveg"
2
1                })
2
1              ]
2
1            }
2
1          ]
2
1        }
2
1      ]
2
1    };
2
1  }
2
1}
2

El Proyecto se encuentra en mi repositorio de github: https://github.com/levalencia/TypescriptDesignPatterns03-Builder

Luis Valencia MVP SharePoint levm38@outlook.com

@levalencia

http://www.luisevalencia.com