Notes on Common Design Patterns

What are desgn patterns and why should we use them. Common tried and tested solutions (found in nature repeating) to recurring architectural problems in software. Blueprint as opposed to boilerplate

Described by

  • Intent: describes problem and solution
  • Motivation: further explanation ?
  • Stucture: often visual diagram of how the parts are related
  • Code Example: specific example in language or framework

Pros

  • Common language among software developers

Cons

  • Hinders creative new solutions (an over-reliance on a familiar or favourite tool)
  • Dogmatic
  • Needed in languages that lack certain levels of abstraction

Three main categories of patterns: Creational, Structural, and Behavioural

Singleton

  • Creational pattern
  • Shared state of an application, single class instance
  • Global access point to instance with added protection from being overwritten
  • private constructor with static creation method
  class CurrentUser {
    private static instance: CurrentUser;

    private constructor() {
      if (instance) {
        throw new Error("You can only create one instance!");
      }
      instance = this;
    }

    public static getInstance(): CurrentUser {
        if (!CurrentUser.instance) {
            CurrentUser.instance = new CurrentUser();
        }

        return CurrentUser.instance;
    }
  }

  const user1 = CurrentUser.getInstance()
  const user2 = CurrentUser.getInstance()

  console.log(user1 === user2); // true

Pros

  • strict control over global varibales
  • global access point
  • initialised only when first requested

Cons

  • Violates Single Responsibility Principle (A module should be responsible to one, and only one, actor. –Robert C. Martin)
  • No subclass
  • No abstract or interface classes
  • High coupling across the application
  • Difficult to unit test
  • can mask bad design

Common Uses

  • Database object
  • User authentification

Factory

  • Creational pattern Creates objects in a superclass but allows subclasses to alter. This pattern uses factory methods to create new objects rather than relying on the new keyword. Note: the new keyword can still be used to create the original object. Objects returned from a factory are often refferred to as products.

  • Base class must be sufficiently abstract

Common Uses

  • multiple smaller objects sharing the same properties (ex: new types of Cards)
  • avoids multiple conditional statements each time new type is added

Pros

  • can be a good way to deal with future unknowns
  • extension of framework internal components (ex: <svelte:component>)
  • saves system resources by reusing objects
  • avoids tight coupling between product and its creator
  • Adheres to Single Responsibility Principle
  • Adheres to Open/Closed Principle.

Cons

  • Base class must be sufficiently abstract or inherent problems can occur in subclasses
  • Need storage to keep track of created objects
  • Products need to follow the same interface
  • code complexity and readablilty
  abstract class CardCreator {
    public abstract factoryMethod(): Product;

    public someOperation(): string {

        const product = this.factoryMethod();

        return `CardCreator: card created with ${product.operation()}`;
    }

  }

  class PodcastCreator extends CardCreator {
    public factoryMethod(): Product {
        return new PodcastCreator1();
    }
  }

  interface Card {
    operation(): string;
  }

  class PodcastCard implements Card {
    public operation(): string {
        return '{PodcastCard}';
    }
  }

  function createCard(creator: CardCreator) {
    // ...
    console.log(creator.someOperation());
    // CardCreator: card created with {PodcastCard}
    // ...
  }

Prototype

  • Creational pattern A clone, relies on class inheritance.
  const cycle = {
    sound: [ 'ding-ding']
  }

  const tricycle =  Object.create(cycle, { wheels: { value: 3 }});

  console.log(tricycle) // { wheels: 3 }
  Object.getPrototypeOf(tricycle); // sound: ['ding-ding']

Builder

  • Creational pattern Allows for piece by piece construction of complex objects. Extracts object construction from it’s own class into separate builder objects. Not all steps need be called. A director class can be added to define step order in case reusable routines are required.
  • The object is intanciated and then methods for building it are chained
  class Pizza {
    this.crust = null;
    this.sauce = null;
    this.cheese: string | [] = '';
    this.toppings: [] = [];

    addCrust(type = 'classic') {
      this.crust = type;

      return this;
    }
    addSauce(type: 'tomato' | 'cream' | 'pesto' = 'tomato') {
      this.sauce = type;

      return this;
    }
    addCheese(type = 'mozzarella') {
      this.cheese = type;

      return this;
    }
    build() {
      return this;
    }
  }

Pros

  • avoids use of multiple subclasses or large class constructors

Cons

Mediatior

  • Behavioural Pattern

example: form validation on clicking submit button

Submit button notifies of click and Mediator takes care of validation of form elements

  • allows extraction of relationship between classes into a separtae class
  • individial components don’t need to have knowledge of each other

Pros

  • Adheres to Single Responsibility Principle
  • Adheres to Open/Closed Principle.
  • Improves component reusability

Cons

  • Danger of becoming a God Object

Observer

  • Behavioural Pattern
  interface User {
    id: string;
    email: string;
    createId: (email: string) => string;
  }

  class AlertNotification {
    users: User[];

    constrctor() {
      this.users = [];
    }

    subscribe(email) {
      const user = new User(email)
      this.users.push()
    }

    unSubscribe(id) {

    }

    nofifyAll(event) {
      this.users.forEach((user) => notifyUser(event));
    }
  }

In this example a single method (notifyAll) only needs to be called once to notify all the users; examples: Svelte stores

Iterator

  • Behavioural Pattern an object returning a value and a next method (from MDN)
  function makeRangeIterator(start = 0, end = Infinity, step = 1) {
    let nextIndex = start;
    let iterationCount = 0;

    const rangeIterator = {
      next() {
        let result;
        if (nextIndex < end) {
          result = { value: nextIndex, done: false };
          nextIndex += step;
          iterationCount++;
          return result;
        }
        return { value: iterationCount, done: true };
      },
    };
    return rangeIterator;
  }
  const it = makeRangeIterator(1, 10, 2);

  let result = it.next();
  while (!result.done) {
    console.log(result.value); // 1 3 5 7 9
    result = it.next();
  }

  // [5 numbers returned, that took interval in between: 0 to 10]
  console.log("Iterated over sequence of size:", result.value);

Iterables can be user defined or built in examples: String, Array, TypedArray, Map, Set

User defined:

  const myIterable = {
    *[Symbol.iterator]() {
      yield 1;
      yield 2;
      yield 3;
    },
  };

Adapter

  • Structural pattern Used to adapt a object or class to an incompatible interface to allow for usage. example convert JSON data to XML. The adapter can act as a translator or like a physical plug adapter.

Pros

  • Adheres to Single Responsibility Principle
  • Adheres to Open/Closed Principle.

Cons

  • introduces overhead
  • complexity

Resources

Articles and e-books

Books