Structural Design Patterns in JavaScript (part 1)

To remember the design patterns structure, categories and benefits check the previous article Creational Design Patterns in JavaScript.

What is a Structural pattern

Structural Design Patterns are Design Patterns that focus on making relationships between entities.

Adapter pattern

The Adapter (Wrapper) pattern creates an intermediary abstraction that translates (maps) an old component to a new one. Clients call methods on the Adapter object which redirects them into calls to the legacy component. This strategy can be implemented using inheritance or aggregation (composition).


Participants:

  • Client – calls the Adapter to request a service
  • Adapter – implements the interface that the client expects or knows
  • Adaptee – the object being adapted with a different interface from what the client expects


Example in typescript:

// adapter.ts
interface Device {
    turnOn(): void;
    turnOff(): void;
    isOn(): boolean;
}

interface SmartDevice {
    activate(): void;
    deActivate(): void;
    isActivated(): boolean;
}

class Phone implements Device {
    private status: boolean;
    private _name: string;

    constructor(name) {
        this._name = name;
    }

    public isOn(): boolean {
        return this.status;
    }

    public turnOn() : void {
        this.status = true;
    }

    public turnOff() : void {
        this.status = false;
    }

    public get name() {
        return this._name;
    }
}

class Laptop implements Device {
    private status: boolean;
    // ... other properties

    public isOn(): boolean {
        return this.status;
    }

    public turnOn(): void {
        this.status = true;
    }
    public turnOff(): void {
        this.status = false;
    }
    // ... other methods
}

// Adaptee
class SmartPhone implements SmartDevice {
    private active: boolean;
    private _name: string;

    constructor(name) {
        this._name = name;
    }

    public activate(): void {
        this.active = true;
    }

    public deActivate(): void {
        this.active = true;
    }
    public isActivated(): boolean {
        return this.active;
    }

    public get name() {
        return this._name;
    }
}
// This adapter maps the old Phone methods to the ones of SmartPhone
class SmartPhoneAdapter implements Device {
    private smartPhone: SmartPhone;

    constructor(phone: SmartPhone) {
        this.smartPhone = phone;
    }

    public isOn(): boolean {
        return this.smartPhone.isActivated();
    }

    public turnOn(): void {
        this.smartPhone.activate();
    }
    public turnOff(): void {
        this.smartPhone.deActivate();
    }
    public get name() {
        return this.smartPhone.name;
    }
}

// the client use only this method which should work properly with old and new device types
function turnOnDevice(d: any) {
    d.turnOn();
    console.log(d.name, d.isOn());
}

let nokia = new Phone('nokia 1100');
turnOnDevice(nokia);

let acer = new Laptop();
turnOnDevice(acer);

let samsungGalaxy = new SmartPhone('Samsung Galaxy S20');
let smartPhoneAdapter = new SmartPhoneAdapter(samsungGalaxy);
turnOnDevice(smartPhoneAdapter);

Make a simple typescript configuration:

{
  "compilerOptions": {
   "target": "es5"
  },
  "files": ["adapter.ts"]
}

Compile the adapter.ts and run the adapter.js in your cli:

tsc
node adapter.js
// output:
// nokia 1100 true
// undefined true
// Samsung Galaxy S20 true

Bridge pattern

The Bridge pattern decouples an abstraction from its implementation so that the two can vary independently.

Abstraction (interface) is a high-level control layer for some entity and it delegates the work to the implementation layer (platform). The Abstraction contains a reference to the Implementation via a property on the class (hence the composition over inheritance).

Participants:

  • Client – request an operation by calling Abstraction
  • Abstraction – declares an interface for first level abstraction and keeps a reference to the Implementor
  • RefinedAbstraction – implements and extends the interface defined by Abstraction
  • Implementor – first level implementor abstraction
  • ConcreteImplementor – implements the Implementor interface

In the bellow example, the EntityService and UserService contains the IApiService property.


Bridge.ts

import { User } from './models/User';
import { UserApiService } from './services/UserApiService';
import { UserService } from './services/UserService';

// Client
const url = 'https://jsonplaceholder.typicode.com/users/';
const apiService = new UserApiService(url);
const userService = new UserService(apiService);

userService.displayUsers();
userService.displayUser(1);
userService.addNewUser({ id: 11, name: 'vio', email: 'vio@gmail.com' } as User);

models/User.ts

 export interface User {
    id: number;
    name: string;
    email: string;
}

models/IApiService.ts

// Implementor
export interface IApiService {
    get(param: number): T;
    getAll(): Promise;
    add(entity: T): Promise;
    // update(entity: T): Promise;
    // delete(entity: T): Promise;
}

services/EntityService.ts

import { IApiService } from '../models/IApiService';

// Abstraction
export abstract class EntityService {
    apiService: IApiService;
    constructor(apiService: IApiService) {
        this.apiService = apiService;
    }
}

services/UserService.ts

import { IApiService } from '../models/IApiService';
import { User } from '../models/User';
import { EntityService } from './EntityService';

// RefinedAbstraction
export class UserService extends EntityService {
    activeUser: User;

    constructor(apiService: IApiService) {
        super(apiService);
    }

    addNewUser(user: User) {
        this.apiService.add(user).then(data => {
            console.log('added user: ', data);
        }).catch(err => console.log(err));
    }

    // setActiveUserEmail(email: string) {
    //     this.activeUser.email = email;
    //     this.apiService.update(this.activeUser);
    // }

    displayUser(id) {
        this.apiService.get>(id).then(user => console.log('user', user));
    }

    async displayUsers() {
        let users = await this.apiService.getAll();
        console.log('users', users);
    }
}

services/UserApiService.ts

const fetch = require('node-fetch');
import { IApiService } from '../models/IApiService';

// ConcreteImplementor
export class UserApiService implements IApiService {
    constructor(
        protected _base: string
    ) { }

    get(id: number): T {
        return fetch(this._base + id).then(res => res.json());
    }

    getAll(): Promise {
        return fetch(this._base).then(res => res.json());
    }

    add(item: T): Promise {
        return fetch(this._base, {
            method: 'POST',
            body: JSON.stringify(item),
            headers: {
                'Content-type': 'application/json; charset=UTF-8',
            },
        }).then(res => res.json());
    }

    // update(entity: T): Promise {
    //   
    // }

    // delete(entity: T): Promise {
    //  
    // }
}

Install the needed packages:

npm install node-fetch
npm install ts-node 

Update tsconfig.json:

{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "ES2015",
      "DOM"
    ]
  },
}

Run the app: ts-node bridge.ts

The displayed users will look like https://jsonplaceholder.typicode.com/users/.

Composite Pattern

The Composite allows composing objects into a tree-like structure.

Participants:

  • Base Component – the common interface (or abstract class) for objects in the composition
  • Leaf – single object (has no children)
  • Composite – made up of either single objects and/or other composite objects

main.ts

import { File } from './File';
import { Folder } from './Folder';

// Client
let file1 = new File('VSCode.exe');
let file2 = new File('Chrome.exe');
let file3 = new File('Summer.jpg');
let file4 = new File('Inchros.dll');

let folder1 = new Folder('Work');
let folder2 = new Folder('Pics');

folder1.addItem(file1);
folder2.addItem(file3);
folder1.addItem(folder2);
folder1.addItem(file2);
folder1.addItem(file4);
folder1.ls();
console.log('---');
folder1.searchItem('chro');
folder1.ls();

Storage.ts

// Base Component
export interface Storage {
    name: string;
    ls(): void;
    setParent(n): void;
}

File.ts

import { Storage } from './models/Storage';
// Leaf
export class File implements Storage {
    name: string;
    parent: any;
   
    constructor(name: string) {
        this.name = name;
        this.parent = null;
    }
    ls() {
        console.log(this.parent.spacer + this.name);
    }
    setParent(node: any) {
        this.parent = node
    }
}

Folder.ts

import { Storage } from './models/Storage';
// Composite object
export class Folder implements Storage {
    private childrens: Storage[] = [];
    name: string;
    parent: any;
    indent = 0;
    spacer = '';

    constructor(name: string) {
        this.name = name;
        this.parent = null;
    };

    ls() {
        console.log(this.spacer + this.name);
        if (this.childrens.length) {
            this.setSpacer(' ');
            this.childrens.forEach((child: any) => {
                if (child.constructor.name === 'Folder') {
                    child.setSpacer(' ');
                }
                child.ls();
            });
        }
    }

    setSpacer(char) {
        this.spacer += char;
    }

    setParent(node: Storage) {
        this.parent = node
    }

    getItem(i: number): Storage {
        return this.childrens[i];
    }

    getItems() {
        return this.childrens;
    }

    addItem(child: Storage) {
        this.childrens.push(child);
        child.setParent(this);
    }

    removeItem(child: Storage) {
        this.childrens = this.childrens.filter(c => c.name !== child.name);
    }

    removeAllItem() {
        this.childrens = [];
    }

    searchItem(title: string) {
        this.childrens = this.childrens.filter(c => c.name.toLowerCase().indexOf(title) >= 0);
    }
}

Running the app: ts-node main.ts will output:

Work
 VSCode.exe
 Pics
  Summer.jpg
 Chrome.exe
 Inchros.dll
---
 Work
  Chrome.exe
  Inchros.dll

Decorator Pattern

The Decorator pattern extends (decorates) an object’s behavior dynamically. Decorators allows runtime changes to statically typed languages as opposed to inheritance which takes place at compile time. Luckily JavaScript is a dynamic language 🙂

Participants:

  • Client – keeps a reference to the decorated Component
  • Component – defines the interface for objects that can have responsibilities added to them dynamically
  • ConcreteComponent – objects that implements the Component interface
  • Decorator – keeps a reference to a Component object and defines an interface based on Component’s interface
  • ConcreteDecorator – adds responsibilities to the component

decorator.ts

// Component
interface Pizza {
    getPrice(): number;
}
// ConcreteComponent
class VeggiePizza implements Pizza {
    public getPrice(): number {
        return 5;
    }
}
// Decorator
class PizzaDecorator implements Pizza {
    protected pizza: Pizza;
    constructor(pizza: Pizza) {
        this.pizza = pizza;
    }
    /**
     * The Decorator delegates all work to the wrapped component
     */
    public getPrice(): number {
        return this.pizza.getPrice();
    }
}
// ConcreteDecorator
class TomatoTopping extends PizzaDecorator {
    /**
     * Decorators may call parent implementation of the operation, instead of
     * calling the wrapped object directly to simplify extension
     * of decorator classes
     */
    public getPrice(): number {
        return super.getPrice() + 1.3;
    }
}
// ConcreteDecorator
class BaconTopping extends PizzaDecorator {
    public getPrice(): number {
        return super.getPrice() + 2;
    }
}
// Client
let basicPizza = new VeggiePizza();
let napolitana = new TomatoTopping(basicPizza);
let margherita = new BaconTopping(new TomatoTopping(basicPizza));

console.log(napolitana.getPrice());
console.log(margherita.getPrice());
/* 
Run: ts-node decorator.ts
Output :
6.3
8.3
*/

Facade Pattern

The Facade pattern provides a simplified interface to a set of subsystems. Typical facades are the API’s which hides the complexities of the business from the presentation layer.

Participants:

  • Facade – delegates client requests to proper subsystem objects
  • Subsystems – implements subsystem functionality, have no knowledge/reference to the facade

facade.ts

// Subsystem
class Order {
    constructor(private dishName: string, private quantity: number, 
         private dishPrice: number, private shippingPrice: number, 
         private user: string) {
    }
    showDetails() {
        console.log(`${this.user} ordered ${this.quantity} X ${this.dishName} 
           for ${this.dishPrice * this.quantity + this.shippingPrice} €`);
    }
}
// Subsystem
class Restaurant {
    constructor(private name: String, private cart: Order[] = []) {}
    
    addOrderToCart(order: Order) {
        this.cart.push(order)
    }
    completeOrders() { }
}
// Subsystem
class DeliveryService {
    private order: Order;

    constructor(private name: string) { }

    acceptOrder(order: Order) {}

    calculateShippingCosts() {
    }

    deliver() { }
}
// Facade
class FoodDelivery {
    constructor(private restaurant: Restaurant, private deliveryService: DeliveryService) { }

    orderFood(orders: Order[]) {
        orders.forEach(order => this.restaurant.addOrderToCart(order));
        this.restaurant.completeOrders();
        orders.forEach(order => {
            this.deliveryService.acceptOrder(order);
            this.deliveryService.calculateShippingCosts();
            this.deliveryService.deliver();
            order.showDetails();
        });
    }
}

// Client
let restaurant = new Restaurant('Joshino');
let deliveryService = new DeliveryService('Glovo');
let facade = new FoodDelivery(restaurant, deliveryService);
let soupOrder = new Order('Miso soup', 1, 3.5, 1, 'Maria');
let sushiOrder = new Order('Nigiri', 5, 2, 1.2, 'Dan');
facade.orderFood([soupOrder, sushiOrder]);


Flyweight Design Pattern

This pattern is used to minimize the memory usage (cache – memoize) by sharing as much data as possible with other similar objects

flyweight-design-pattern-javascript

A short example of cached data for heavy computations:

const flyweight = (intensiveCalculation: (arg: any) => any) => {
    const results = new Map();
    return (arg: any) => {
    	if (results.has(arg)) return results.get(arg);
        const result = intensiveCalculation(arg);
        results.set(arg, result);
        return result;
    }
}

Flyweight is often used in games as it shares objects instead of recreating them.

flyweight.ts

// An example of strategic game which draw many characters (figures) of same type.
export enum FigureType {
  ARCHER = 'Archer',
  CRUSADER = 'Crusader',
}

export interface Figure {
  assignPower(power: any): void;
  mission(): void;
}

class Archer implements Figure {
  // Intrinsic Attribute
  private readonly task: string;
  // Extrinsic Attribute
  private power: string;

  constructor() {
    this.task = 'attack enemy';
  }

  assignPower(power: any) {
    this.power = power;
  }

  mission(): void {
    console.log(`Archer with power ${this.power} and task ${this.task}`);
  }
}

class Crusader implements Figure {
  private readonly task: string;
  private power: string;

  constructor() {
    this.task = 'spy enemy';
  }

  assignPower(power: any) {
    this.power = power;
  }

  mission(): void {
    console.log(`Crusader with power ${this.power} and task ${this.task}`);
  }
}

class FigureFactory {
  private static figuresMap = new Map();

  public static getFigure(type: any): any {
    let figure = null;

    if (this.figuresMap.has(type)) {
      figure = this.figuresMap.get(type);
    } else {
      switch (type) {
        case FigureType.ARCHER:
          figure = new Archer();
          break;
        case FigureType.CRUSADER:
          figure = new Crusader();
          break;
        default:
          throw Error('figure type not found');
      }
      this.figuresMap.set(type, figure);
    }
    return figure;
  }
}

class Game {
  constructor(
    private figureType: string[],
    private powers: string[],
    private numPlayers: number
  ) {
    this.figureType = figureType;
    this.powers = powers;
    this.numPlayers = numPlayers;
  }

  private getRandomValue(list: string[], max: number) {
    const index = Math.floor(Math.random() * Math.floor(max));
    return list[index];
  }

  public play() {
    for (let i = 0; i < this.numPlayers; i++) {
      const player = FigureFactory.getFigure(
        this.getRandomValue(this.figureType, this.figureType.length)
      );
      player.assignPower(this.getRandomValue(this.powers, this.powers.length));
      player.mission();
    }
  }
}

const figureType = ['Archer', 'Crusader'];
const powers = ['doubleSpeed', '3xJump', 'extraLife'];
const numPlayers = 5;

const ageOfEmpires = new Game(figureType, powers, numPlayers);
ageOfEmpires.play();

/*
tsc src/flyweight.ts
node src/flyweight.js

Archer with power extraLife and task attack enemy
Crusader with power doubleSpeed and task spy enemy
Archer with power 3xJump and task attack enemy
Crusader with power extraLife and task spy enemy
Crusader with power doubleSpeed and task spy enemy
*/

Proxy Design Pattern

A Proxy Pattern controls and manage access to the objects they are protecting. Proxies are also called surrogates, handles, wrappers.

Types of proxies:

  • Remote proxy: responsible for representing the object located remotely (ex: RPC calls, REST resources).
  • Virtual Proxy: provides some default and instant results if the real object takes time to produce results. The real object gets created only when a client first requests/accesses the object and after that the client can just refer to the proxy to reuse the object. This avoids duplication of the object and memory saving.
  • Protection proxy: it acts as an authorization layer that verifies if the actual user has access the resource or not.
  • Smart Proxy: provides additional layer of security by interposing specific actions when the object is accessed.

proxy-design-pattern-js

client.ts

import { ProxyInternet } from './proxyInternet.js';

function app() {
    const internet = new ProxyInternet();
    const websites = ['http://site1.com', 'http://site4.com'];
    try {
        websites.forEach(site => internet.connect(site));
    } catch (error) {
        console.log(error);
    }
}

app();

proxyInternet.ts

import { Internet } from './internet.js';
import { ProviderInternet } from './providerInternet.js';

export class ProxyInternet implements Internet {
  private internet;
  public bannedPages: string[] = [];

  constructor() {
    this.internet = new ProviderInternet();
    this.bannedPages.push('http://site1.com');
    this.bannedPages.push('http://site2.com');
    this.bannedPages.push('http://site3.com');
  }

  connect(host: string) {
    if (this.bannedPages.includes(host)) {
      console.log('Access Denied to ' + host);
    } else {
      this.internet.connect(host);
    }
  }
}

providerInternet.ts

import { Internet } from './internet.js';

export class ProviderInternet implements Internet {
    connect(host: string) {
        console.log('Connecting to ' + host);
    }
}

internet.ts

export interface Internet {
    connect(host: string): void
}

Run the app:

tsc
node dist/client.js
Access Denied to http://site1.com
Connecting to http://site4.com
email-newsletter

Dev'Letter

Professional opinion about Web Development Techniques, Tools & Productivity.
Only once a month!

Any suggestion on what to write next time ? Submit now

Opinions

avatar
550
  Subscribe  
Notify of

Related Posts