Object oriented design

SOLID - Design patterns

SOLID Principles

> Software designing principles for your objects made by Robert C. Martin in order to produce a cleaner object communication

Single

responsibility

Open closed

Liskov

substitution

Interface

segregation

Dependency Inversion

> A class/function must be responsible only for one thing. (Do one job only)

> There must be only one reason for which a class/function can be modified

> Use semantic names, very specific ones for small classes/utils and more generic ones for bigger entities.

Single responsibility

Single responsibility

export class Blog<T extends Article> {
  private _articles: T[];

  constructor() {
    this._articles = [];
  }

  get articles(): T[] {
    return this._articles;
  }
}
export default interface Article {
  id: number
  title: string
  url: string
  image: string
}

The entity Blog represents the model that talks to the database which is an in-memory array in our case for simplicity.

Single responsibility

Single responsibility

export class Blog<T extends Article> {
  private _articles: T[];

  constructor() {
    this._articles = [];
  }

  get articles(): T[] {
    return this._articles;
  }
  
  add(article: T): number {
    this._articles.push(article);

    return article.id;
  }
}

Single responsibility

Single responsibility

export class Blog<T extends Article> {
  ...
  
  add(article: T): number {
    this._articles.push(article);

    return article.id;
  }
  
  remove(id: number): T | undefined {
    const article = this._articles.find(
      (article) => article.id == id,
    );

    this._articles = this._articles.filter(
      (article) => article.id !== id,
    );

    return article;
  }
}

What about storing articles?

Single responsibility

Single responsibility

import fs from 'fs'

export class Blog<T extends Article> {
  ...
  
  toString(): string {
    return this._articles
      .map(
        (article) =>
          `${article.title} can be accessed through ${article.url}`,
      )
      .join('\n');
  }  

  saveToFile(filename: string) {
    fs.writeFileSync(
      `${__dirname}/${filename}`,
      this.toString(),
    );
  }
}

Alright, sounds good, doesn't it?

Why don't we support multi-format storage

like .txt, .json?

Single responsibility

Single responsibility

export class Blog<T extends Article> {
  ...
  
  toJson(): string {
    return JSON.stringify(this._articles, null, 2);
  }

  saveToFile(filename: string, format: string = 'txt') {
    if (format == 'txt')
      fs.writeFileSync(
        `${__dirname}/${filename}`,
        this.toString(),
      );
    if (format == 'json')
      fs.writeFileSync(
        `${__dirname}/${filename}`,
        this.toJson(),
      );
  }
}

Alright, provide me with file extension and I'll figure out how to store it

Aight aight, the last update
can we read from online APIs?  
 😁

Single responsibility

Single responsibility

Hopefully, by now, you've noticed the anti-pattern we're heading towards...

THE GOD CLASS

The god class is a term or an anti-pattern which indicates that you're building a class that literally does every single job your entity wants to have

So, How to fix this issue?!

Single responsibility

Single responsibility

import Article from './types/Article';

export class Blog<T extends Article> {
  private _articles: T[];

  constructor() {
    this._articles = [];
  }

  get articles(): T[] {
    return this._articles;
  }

  add(article: T): number {}

  remove(id: number): T | undefined {}

  toString(): string {}

  toJson(): string {}

  saveToFile(filename: string, format: string = 'txt') {}
}

Single responsibility

Single responsibility

import PersistencyManager from './PersistencyManager';
import Article from './types/Article';

export class Blog<T extends Article> {
  private _articles: T[];

  constructor() {
    this._articles = [];
    this._pm = new PersistencyManager();
  }

  // ...
  
  get pm(): PersistencyManager {
    return this._pm;
  }
  
  // ...
}
import fs from 'fs';

export default class PersistencyManager {
  saveToFile(filename: string, data: any) {
    fs.writeFileSync(`${__dirname}/${filename}`, data);
  }
}
b.add(/*some article*/);
b.add(/*some article*/);
b.add(/*some article*/);

b.pm.saveToFile('articles.txt', b.toString());
b.pm.saveToFile('articles.json', b.toJson());

Single responsibility

> Entities should be open for extension, closed for modification

> Extend your functionality by adding new code not modifying existing one

> Separate your business logic so that your code can become more loosely coupled and never breaks

Open closed

Open closed

import { Color } from './types/Color';
import { Size } from './types/Size';

export class Product {
  constructor(
    public name: string,
    public color: Color,
    public size: Size,
  ) {}
}
Product.ts
export enum Size {
  'small',
  'medium',
  'large',
}
Size.ts
export enum Color {
  'green',
  'red',
  'blue',
}
Color.ts

Open closed

Open closed

import { Product } from './Product';
import { Color } from './types/Color';

export default class ProductFilter {
  filterByColor(products: Product[], color: Color) {
    return products.filter(
      (product: Product) => product.color === color,
    );
  }
}
ProductFilter.ts

What if the customer wants to filter by size?

Open closed

Open closed

import { Product } from './Product';
import { Color } from './types/Color';
import { Size } from './types/Size';

export default class ProductFilter {
  filterByColor(products: Product[], color: Color) {
    return products.filter(
      (product: Product) => product.color === color,
    );
  }

  filterBySize(products: Product[], size: Size) {
    return products.filter(
      (product: Product) => product.size === size,
    );
  }
}
ProductFilter.ts

What if the customer wants to filter by size and color?

What if the customer wants to filter by size or color?

We also need to filter other items like categories, courses

Hey, we forgot to tell you, wen eed a price filter :D

Open closed

Open closed

Abstraction

It gets you out

of clowny zone

Open closed

Open closed

Filtering

Not only responsible

for filtering products

it filters any item

Filters

Filter

Adheres to

Filterar

Open closed

Open closed

import { Product } from '../../Product';

export interface Filter {
  isSatisfied(item: Product): boolean;
}
Filter.ts
import { Product } from '../../Product';
import { Color } from '../../types/Color';
import { Filter } from '../Contracts/Filter';

export default class ColorFilter implements Filter {
  constructor(private color: Color) {}

  isSatisfied(item: Product): boolean {
    return item.color === this.color;
  }
}
ColorFilter.ts
import { Product } from '../../Product';
import { Size } from '../../types/Size';
import { Filter } from '../Contracts/Filter';

export default class SizeFilter implements Filter {
  constructor(private size: Size) {}

  isSatisfied(item: Product): boolean {
    return item.size === this.size;
  }
}
SizeFilter.ts

Contract

Concrete

classes/implementations

Open closed

Open closed

import { Product } from '../Product';
import { Filter } from './Contracts/Filter';

export default class Filterar {
  filter(items: Product[], filter: Filter) {
    return items.filter((item) => filter.isSatisfied(item));
  }
}
Filterar.ts

The filterar doesn't care what filter it gets

But it must have isSatisfied method a.k.a implements the Filter interface

What about filtering using color and size?!

Open closed

Open closed

import { Product } from '../../Product';
import { Filter } from '../Contracts/Filter';

export default class AndFilter implements Filter {
  constructor(private filters: Filter[]) {}

  isSatisfied(item: Product): boolean {
    return this.filters.every((filter) =>
      filter.isSatisfied(item),
    );
  }
}
AndFilter.ts
import { Product } from '../../Product';
import { Filter } from '../Contracts/Filter';

export default class OrFilter implements Filter {
  constructor(private filters: Filter[]) {}

  isSatisfied(item: Product): boolean {
    return this.filters.some((filter) =>
      filter.isSatisfied(item),
    );
  }
}
OrFilter.ts

Rather than accepting a value

like green for color or large

for size, we expect the

filter instance itself

actually, an array of filter

instances

The every function returns true only if all items within the filters array return true a.k.a

all filters are satisfied

The some function returns true only if at least one of the items within the filters array return true a.k.a

any filter is satisfied

Open closed

Open closed

ANOTHER

MENTAL
MODEL

Open closed

Open closed

Don't extract to an interface

but extend and override

I highly recommend that you code to an interface

cause abstraction reduces coupling

Let's inspect it through an example...

Open closed

Open closed

class EmailService {
  public sendEmail(email: string, message: string): void {
    console.log(`Email Sent: ${message} to ${email}`);
  }
}

Some third-party service to send emails to users

class NotificationService {
  private _emailService: EmailService;
  constructor() {
    this._emailService = new EmailService();
  }
  public sendNotification(email: string, message: string) {
    this._emailService.sendEmail(email, message);
  }
}

Maybe our implementation of how to notify user about news

What if users want to be notified

by SMS?

Open closed

Open closed

class SMSService {
  public sendSms(phone: number, message: string): void {
    console.log(`Message ${message} sent to ${phone}`);
  }
}

Open closed

Open closed

class NotificationService {
  private _emailService: EmailService;
  private _smsService: SMSService;

  constructor() {
    this._emailService = new EmailService();
    this._smsService = new SMSService();
  }
  public sendNotification(
    email: string,
    message: string,
    phone?: number,
    smsMessage?: string
  ) {
    this._emailService.sendEmail(email, message);
    if (phone && smsMessage) {
      this._smsService.sendSms(phone, smsMessage);
    }
  }
}

We're by default emailing the user

If user has a phone number, we'll sms him

Open closed

Open closed

class NotificationService {
  private _emailService: EmailService;
  constructor() {
    this._emailService = new EmailService();
  }
  public sendNotification(email: string, message: string) {
    this._emailService.sendEmail(email, message);
  }
}
class OrderNotificationService extends NotificationService {
  private _smsService: SMSService;
  constructor() {
    super();
    this._smsService = new SMSService();
  }

  public sendOrderNotification(
    email: string,
    emailMessage: string,
    phone?: number,
    smsMessage?: string
  ) {
    if (email && emailMessage) {
      this.sendNotification(email, emailMessage);
    }
    if (phone && smsMessage) {
      this._smsService.sendSms(phone, smsMessage);
    }
  }
}

Open closed

Open closed

The previous implementation is also an implementation of the OCP

But why?

Well, we're not modifying the base functionality, we're extending and producing a new one on top of it.

So the class NotificationService is considered open for extension, closed for modification.

But, this implementation produces another issue.

Inheritance Hell

Open closed

Open closed

Inheritance Hell

NotificationService

SMSNotificationService

CallNotificationService

MessengerNotificationService

And any other creative way of disturbing your user to inform him that you published a blog or something

HomeNotificationService

Liskov Substitution

Let Φ(x) be a property provable about objects x of type T. Then Φ(y) should be true for objects y of type S where S is a subtype of T.

Barbara Liskov

To build software systems from interchangeable parts, those parts must adhere to a contract that allows those parts to be substituted one for another.

Robert C. Martin

Liskov Substitution

> Any derived class can be substituted with its parent class without the consumer knowing it

> Every class that implements an interface should be suitable to substitute any other reference  that implements the interface

> Every part of the code should get the same results no matter what instance you inject into it, on the condition that this instance implements the same interface

Liskov Substitution

Liskov Substitution

class Rectangle {
  constructor(
    private _width: number,
    private _height: number,
  ) {}

  get width() {
    return this._width
  }

  get height() {
    return this._height
  }

  area(): number {
    return this._width * this._height
  }
}

Liskov Substitution

Liskov Substitution

class Rectangle {
  constructor(
    private _width: number,
    private _height: number,
  ) {}

  area(): number {
    return this.width * this.height
  }

  toString() {
    return `${this._width}×${this._height}`
  }
}

let rc = new Rectangle(2, 3)
console.log(rc.area()) // 6
console.log(rc.toString()) // 2x3

Liskov Substitution

Liskov Substitution

class Rectangle {
  set width(v: number) {
    this._width = v
  }
  
  set height(v: number) {
    this._height = v
  }
}

let rc = new Rectangle(2, 3)
console.log(rc.area()) // 6
console.log(rc.toString()) // 2x3

rc.width = 4
console.log(rc.area()) // 12
console.log(rc.toString()) // 4x3

Liskov Substitution

Liskov Substitution

class Square extends Rectangle {
  constructor(private size: number) {
    super(size, size)
  }
}

Liskov Substitution

Liskov Substitution

class Square extends Rectangle {
  constructor(private size: number) {
    super(size, size)
  }
}

let sq = new Square(5)

console.log(sq.area) // 25
console.log(sq.toString()) // 5x5

sq.height = 35

console.log(sq.area) // 175
console.log(sq.toString()) // 5x35

Liskov Substitution

Liskov Substitution

class Square extends Rectangle {
  set width(v: number) {
    this._height = this._width = v
  }
  set height(v: number) {
    this._height = this._width = v
  }
}

let sq = new Square(5)

console.log(sq.area) // 25
console.log(sq.toString()) // 5x5

sq.height = 35

console.log(sq.area) // 1225
console.log(sq.toString()) // 35x35

Liskov Substitution

Liskov Substitution

function useIt(rc: any) {
  let { _width } = rc

  rc.height = 10

  console.log(
    `Expected area of ${10 * _width}` +
      ` got instead ${rc.area}`,
  )
}

let rc = new Rectangle(5, 3)
let sq = new Square(5)

useIt(rc) // Expected area of 50 got instead 50
useIt(sq) // Expected area of 50 got instead 100

Liskov Substitution

Liskov Substitution

interface Shape {
  get area(): number
}
class Rectangle implements Shape {
  constructor(
    private _width: number,
    private _height: number,
  ) {}

  set width(v: number) {
    this._width = v
  }

  set height(v: number) {
    this._height = v
  }

  get area(): number {
    return this._height * this._width
  }
}
class Square implements Shape {
  constructor(private _size: number) {}

  set size(v: number) {
    this._size = v
  }

  get area(): number {
    return this._size * this._size
  }
}

Liskov Substitution

Liskov Substitution

interface InterviewResult {
  status: 'Accepted' | 'Rejected'
  message: string
  from: Mail
  to: Mail[]
}

Liskov Substitution

Liskov Substitution

interface InterviewResult {
  status: 'Accepted' | 'Rejected'
  message: string
  from: Mail
  to: Mail[]
}

interface MailService {
  send(from: Mail, to: Mail[]): Promise<InterviewResult>
}

Liskov Substitution

Liskov Substitution

interface InterviewResult {
  status: 'Accepted' | 'Rejected'
  message: string
  from: Mail
  to: Mail[]
}

interface MailService {
  send(from: Mail, to: Mail[]): Promise<InterviewResult>
}
class SendGridEmail implements MailService {
  send(from: Mail, to: Mail[]): Promise<InterviewResult> {
    // Some specific service logic
    return Promise.resolve({
      status: 'Accepted',
      message: 'Hello world',
      from,
      to,
    })
  }
}

Liskov Substitution

Liskov Substitution

interface InterviewResult {
  status: 'Accepted' | 'Rejected'
  message: string
  from: Mail
  to: Mail[]
}

interface MailService {
  send(from: Mail, to: Mail[]): Promise<InterviewResult>
}
class MailGunService implements MailService {
  send(from: Mail, to: Mail[]): Promise<InterviewResult> {
    // Some specific service logic
    return Promise.resolve({
      status: 'Rejected',
      message: 'Sorry :/',
      from,
      to,
    })
  }
}

Liskov Substitution

Liskov Substitution

class UserController {
  constructor(private emailSerivce: MailService) {}

  notifyUser() {
    this.emailSerivce.send('cto@somesite.com', [
      'candidate_one@abc.com',
      'candidate_two@abc.com',
    ])
  }
}

const someUser = new UserController(new SendGridEmail)
const anotherUser = new UserController(new MailGunService)

Liskov Substitution

Liskov Substitution

Interface Segregation

> A client is not meant to be forced to implement methods it won't use

> A client should only and only depend on the methods it's calling

> Modifying a method in a class should not break other classes that do not depend on that method but depend on the interface

> Replace fat interfaces with smaller ones that are specific to the job

Interface Segregation

type Size = 'A4' | 'A3'

class Doc {}

interface Machine {
  print(doc: Doc): string
  fax(doc: Doc): { to: string; content: string }
  scan(doc: Doc): { size: Size; content: string }
}

Interface Segregation

Interface Segregation

class MultiFunctionPrinter implements Machine {
  print(doc: Doc): string {
    return `Print ${doc}`
  }
  fax(doc: Doc): { to: string; content: string } {
    return {
      to: 'ahmed',
      content: doc as string,
    }
  }
  scan(doc: Doc): { size: Size; content: string } {
    return {
      size: 'A4',
      content: doc as string,
    }
  }
}

Interface Segregation

Interface Segregation

class OldFashionPrinter implements Machine {
  print(doc: Doc): string {
    return `Print ${doc}`
  }
}

Compilation error that it doesn't implement Machine correctly, missing 2 methods

class OldFashionPrinter implements Machine {
  print(doc: Doc): string {
    return `Print ${doc}`
  }
  
  fax(doc: Doc): { to: string; content: string } {
    throw new Error('Method not implemented.')
  }
  
  scan(doc: Doc): { size: Size; content: string } {
    throw new Error('Method not implemented.')
  }
}

Interface Segregation

Interface Segregation

interface Machine {
  print(doc: Doc): string
  fax(doc: Doc): { to: string; content: string }
  scan(doc: Doc): { size: Size; content: string }
}
interface IFax {
  fax(doc: Doc): {
    to: string
    content: Doc
  }
}
interface IPrint {
  print(doc: Doc): string
}
interface IScan {
  scan(doc: Doc): {
    date: Date
    content: Doc
  }
}

Interface Segregation

Interface Segregation

class MutliFunctionPrinter implements IPrint, IFax, IScan {
  fax(doc: Doc): { to: string; content: Doc } {
    return {
      to: 'someone',
      content: doc,
    }
  }
  
  print(doc: Doc): string {
    return `Printing ${doc}`
  }
  
  scan(doc: Doc): { date: Date; content: Doc } {
    return {
      date: new Date(),
      content: doc,
    }
  }
}

Interface Segregation

Interface Segregation

class OldFashionedPrinter implements IPrint {
  print(doc: Doc): string {
    return `Printing ${doc}`
  }
  }

Interface Segregation

Interface Segregation

interface Formatable<T> {
  toArray(): T[]
  toJson(): Json
  toString(): string
}
type Primitive =
  | string
  | number
  | null
  | boolean
  | undefined
  | bigint

type JsonArray = Json[]
type JsonObject = { [k: string]: Json }
type Json = Primitive | JsonObject | JsonArray

Interface Segregation

Interface Segregation

abstract class Model<T> implements Formatable<T> {
  toArray(): T[] {
    throw new Error('Method not implemented.')
  }

  toJson(): Json {
    return JSON.stringify(this)
  }
}

Interface Segregation

Interface Segregation

interface Arrayable<T> {
  toArray(): T[]
}

interface Jsonable {
  toJson(): Json
}
abstract class Model<T> implements Arrayable<T>, Jsonable {
  toArray(): T[] {
    throw new Error('Method not implemented.')
  }

  toJson(): Json {
    return JSON.stringify(this)
  }
}

Interface Segregation

Interface Segregation

Dependency Inversion

> High level modules are not meant to depend on low level modules

> High level modules should not depend on concrete implementations but on abstraction

> Low level modules should depend on abstraction

> Changing an implementation in low level modules should not cause any modifications in high level modules

Dependency Inversion

enum Relationship {
  parent = 0,
  child = 1,
  sibling = 2,
}
class Person {
  constructor(public name: string) {}
}
interface Relation {
  from: Person
  type: Relationship
  to: Person
}

Dependency Inversion

Dependency Inversion

class Relationships {
  private _relations: Relation[] = []

  public get relations(): Relation[] {
    return this._relations
  }

  constructor() {
    this._relations = []
  }

  addParentAndChild(parent: Person, child: Person) {
    this._relations.push({
      from: parent,
      type: Relationship.parent,
      to: child,
    })
  }
}

Dependency Inversion

Dependency Inversion

class Research {
  constructor(private relationships: Relationships) {
    let { relations } = relationships

    for (let rel of relations.filter(
      (r) =>
        r.from.name == 'ahmed' &&
        r.type == Relationship.parent,
    )) {
      console.log(`Ahmed has ${rel.to.name}`)
    }
  }
}

Dependency Inversion

Dependency Inversion

let parent = new Person('ahmed')
let child1 = new Person('khaled')
let child2 = new Person('mona')

let rels = new Relationships()

rels.addParentAndChild(parent, child1)
rels.addParentAndChild(parent, child2)

new Research(rels)

Dependency Inversion

Dependency Inversion

class Research {
  constructor(private browser: RelationshipBrowser) {
    for (let p of browser.findAllChildren('ahmed')) {
      console.log(p)
    }
  }
}

Dependency Inversion

Dependency Inversion

interface RelationshipBrowser {
  findAllChildren(name: string): Person[]
}
class Relationships implements RelationshipBrowser {
  findAllChildren(name: string): Person[] {
    return this._relations
      .filter(
        (r) =>
          r.from.name == name &&
          r.type == Relationship.parent,
      )
      .map((r) => r.to)
  }
}

Dependency Inversion

GOF - Design patterns

A design problem is a general problem that hasn't occurred in a single program but many programs if followed the same technique.

Personal definition

 a design pattern is a general repeatable solution to a commonly occurring problem in software design

sourcemaking 🔗

Design patterns are typical solutions to commonly occurring problems in software design. They are like pre-made blueprints that you can customize to solve a recurring design problem in your code.

refactoring-guru 🔗

What makes a design pattern?

Intention

Problem

Solution

Types of design patterns

Structural

Creational

Behavioral

Creational patterns

Creational patterns are related to the mechanisms of constructing your objects

Dependency injection was some sort of a pattern to make communication between objects

But sometimes these constructions become so complex that DI isn't sufficient.

Creational patterns

Abstract factory

Builder

Factory method

Singleton

Prototype

Object pool

Factory Patterns

Simple Factory
Abstract Factory
Factory Method

Why we need factory patterns?

Object creation can become more complex even if it's one object

Optional parameter hell

  • Wholesale object creation can be outsourced to
    • A separate method (Factory method)
    • A separate class (Abstract factory)

Factory Method

Intent The factory method provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created.

Image from refactoring.guru

Factory Method

MySQL or SQLite

Local or AWS

Can be omitted

Can be Database, Storage

Factory Method

Factory Method

interface Shape {
  area(): number
}
class Square implements Shape {
  constructor(private side: number) {}

  area(): number {
    return this.side * this.side
  }
}
class Circle implements Shape {
  constructor(private radius: number) {}

  area(): number {
    return this.radius * this.radius * Math.PI
  }
}
class Rectangle implements Shape {
  constructor(
    private width: number,
    private height: number,
  ) {}

  area(): number {
    return this.width * this.height
  }
}

Factory Method

Factory Method

interface ShapeFactory {
  createShape(): Shape
}
class RectangleFactory implements ShapeFactory {
  createShape(): Shape {
    return new Rectangle(3, 2)
  }
}
class CircleFactory implements ShapeFactory {
  createShape(): Shape {
    return new Circle(3)
  }
}
class SquareFactory implements ShapeFactory {
  createShape(): Shape {
    return new Square(5)
  }
}

Factory Method

Factory Method

Shape

Rectangle

Square

Circle

ShapeFactory

Rectangle
Factory

Square
Factory

Circle
Factory

Abstract Contracts

Concrete Implementations

Factory Method

Abstract Factory

Intent Abstract Factory is a creational design pattern that lets you produce families of related objects without specifying their concrete classes.

Image from refactoring.guru

Abstract Factory

The consumer

Some contract that produces multiple instances that are some how related

Some implementation

that produces related instances of type X maybe

Some implementation

that produces related instances of type Y maybe

Maybe Database and Storage drivers

Abstract Factory

Abstract Factory

Button

Dialog

MacOS

Linux

Windows

MacOS

Linux

Windows

Concrete Implementations

Abstract Contracts

Abstract Factory

Abstract Factory

Abstract Factory

Builder

Intent Builder is a creational design pattern that lets you construct complex objects step by step. The pattern allows you to produce different types and representations of an object using the same construction code.

Image from refactoring.guru

Builder

Can be omitted in some cases.

The consumer

Directs the usage of the builder, which instance to create

and maybe change the builder during the runtime

The contract itself which specifies how to build instances 

The implementations that produce complex objects of some type

Builder

Builder

The problem: class constructor is overloaded with parameters a.k.a parameter hell

class User {
  first_name: string
  last_name: string
  email: string
  github_link: string
  twitter_link: string
  cart: { id: number; price: number }[]
  constructor(
    first_name: string,
    last_name: string,
    email: string,
    github_link: string,
    twitter_link: string,
    cart: { id: number; price: number }[],
  ) {
    this.first_name = first_name
    this.last_name = last_name
    this.github_link = github_link
    this.twitter_link = twitter_link
    this.email = email
    this.cart = cart
  }
}

Builder

Builder

The problem: class constructor is overloaded with parameters a.k.a parameter hell

class User {
  constructor(
    public first_name: string,
    public last_name: string,
    public email: string,
    public github_link: string,
    public twitter_link: string,
    public cart: { id: number; price: number }[],
  ) {}
}

Builder

Builder

Singleton

Intent Singleton is a creational design pattern that lets you ensure that a class has only one instance while providing a global access point to this instance.

Image from refactoring.guru

Singleton

Singleton

Singleton

class Logger {
  private static instance: Logger

  private constructor() {}

  static getInstance(): Logger {
    if (!Logger.instance) {
      Logger.instance = new Logger()
    }

    return Logger.instance
  }
}

let logger = Logger.getInstance()
let logger1 = Logger.getInstance()

console.log(logger == logger1) // true

Singleton

Singleton

Prototype

Intent Prototype is a creational design pattern that lets you copy existing objects without making your code dependent on their classes.

Image from refactoring.guru

Prototype

Consumer

The contract specifies how

to clone objects

The concrete implementations which control the cloning of each objects and handle edge cases like deep copying objects

Prototype

Prototype

interface Cloneable {
  clone(): Cloneable
}

class Person implements Cloneable {
  clone(): Cloneable {
    return new Person() // different details
  }
}

Prototype

Prototype

class Address {
  constructor(
    public street: string,
    public city: string,
    public country: string,
  ) {}
}

class Person {
  constructor(
    public name: string,
    public address: Address,
  ) {}
}

Prototype

Prototype

class Address {
  constructor(
    public street: string,
    public city: string,
    public country: string,
  ) {}
}

class Person {
  constructor(
    public name: string,
    public address: Address,
  ) {}
}

let john = new Person(
  'John doe',
  new Address('123 coding street', 'London', 'UK'),
)

let jane = new Person(
  'Jane Doe',
  new Address('123 coding street', 'London', 'UK'),
)

Potential duplication for all users

that live in London :"

Prototype

Prototype

interface Cloneable {
  clone(): Cloneable
}

class Address {
  constructor(
    public street: string,
    public city: string,
    public country: string,
  ) {}
}

class Person implements Cloneable {
  constructor(
    public name: string,
    public address: Address,
  ) {}

  clone(): Person {
    return new Person(this.name, this.address)
  }
}

Prototype

Prototype

interface Cloneable {
  clone(): Cloneable
}

class Address {/*...*/}

class Person implements Cloneable {
  // ...
  
  clone(): Person {
    return new Person(this.name, this.address)
  }
}

let john = new Person(
  'John Doe',
  new Address('123 coding st', 'London', 'UK'),
)

let jane = john.clone()
jane.name = 'Jane Doe'
jane.address.street = '123 London road'

console.log(john)
console.log(jane)

An inner object

which is passed by

reference

Prototype

Prototype

class Address implements Cloneable {
  constructor(
    public street: string,
    public city: string,
    public country: string,
  ) {}

  clone(): Address {
    return new Address(this.street, this.city, this.country)
  }
}

class Person implements Cloneable {
  constructor(
    public name: string,
    public address: Address,
  ) {}

  clone(): Person {
    return new Person(this.name, this.address.clone())
  }
}

This approach is fine by now,

but if we have several objects nested

each object must implement the Cloneable

SERIALIZATION

Prototype

Serialization

Serialization is the action of transform an object-like to another storable reprsentation like a string.

Ahmed Osama

Prototype

> Serializing your objects can be used for persisting them against your database or a log file, therefore it can be used for persisting your sessions into your database or queuing jobs.

Prototype

Prototype

class Address {
  constructor(
    public street: string,
    public city: string,
    public country: string,
  ) {}
}

class Person {
  constructor(
    public name: string,
    public address: Address,
  ) {}
}

Prototype

Prototype

class Address {...}

class Person {...}


let john = new Person(
  'John Doe',
  new Address('123 coding street', 'London', 'UK'),
)

let jane = JSON.parse(JSON.stringify(john))

jane.name = 'Jane Doe'
jane.address.street = '123 London road'

console.log(john)
console.log(jane)

Jane loses the type Person 

and the property address loses

the type Address

Prototype

Prototype

class Serializer {
  constructor(
    private types: { new (...args: any[]): any }[],
  ) {}
}

Storing an array of constructable objects

to  serialize and clone

  1. Mark all inner objects with a type
  2. Serialize the object into a clone
  3. Reconstruct the clone with all types again

Prototype

Prototype

class Serializer {
  constructor(
    private types: { new (...args: any[]): any }[],
  ) {}
  
  clone(object: object) {
    this.mark(object)

    let copy = JSON.parse(JSON.stringify(object))

    return this.reconstruct(copy)
  }
}

Prototype

Prototype

class Serializer {
  private mark(object: Record<string, any>) {
    let idx = this.types.findIndex(
      (t) => t.name === object.constructor.name,
    )

    if (idx !== -1) {
      object['typeIndex'] = idx

      for (let k in object) {
        if (object.hasOwnProperty(k)) {
          this.mark(object[k])
        }
      }
    }
  }
  
  clone(object: object) {
    this.mark(object)
  }
}

Applying this approach which includes any nested objects of other types

Checking if the registered types has 

an object with the name constructor name

Marking each object with a special key that it has a type

Prototype

Prototype

class Serializer {
  private reconstruct(object: Record<string, any>) {
    if (object.hasOwnProperty('typeIndex')) {
      let obj = new this.types[object.typeIndex]()

      for (let key in object) {
        if (
          object.hasOwnProperty(key) &&
          object[key] !== null
        )
          obj[key] = this.reconstruct(object[key])
      }

      delete obj.typeIndex

      return obj
    }

    return object
  }
}

If the object has the special key, it's newable

Recursively newing every nested object

Removing the special keys and returning the reconstructed object

Return the object untouched if the object has no special key

Prototype

Prototype

class Serializer {
  constructor(
    private types: { new (...args: any[]): any }[],
  ) {}
  
  private reconstruct(object: Record<string, any>) {/*...*/}
  private mark(object: Record<string, any>) {/*...*/}
  
  clone(object: object) {
    this.mark(object)

    let copy = JSON.parse(JSON.stringify(object))

    return this.reconstruct(copy)
  }
}

Prototype

Prototype

import { Address } from './Address'
import { Person } from './Person'
import { Serializer } from './Serializer'

let john = new Person(
  'John Doe',
  new Address('123 coding street', 'London', 'UK'),
)

let s = new Serializer([Person, Address])

let jane = s.clone(john) as Person

jane.name = 'Jane Doe'
jane.address.street = '123 London road'

console.log(john.greet())
console.log(jane.toString())
console.log(jane.greet())

Prototype

Prototype

Structural patterns

They are concerned about the assembly of classes and objects to build bigger structures

A better way to manage Object and Class composition

Providing your objects with more functionality through composing them with other objects.

Structural patterns

Adapter

Bridge

Composite

Decorator

Facade

Flyweight

Private class data

Proxy

Structural patterns

Adapter

Intent  Adapter is a structural design pattern that allows objects with incompatible interfaces to collaborate

Image from refactoring.guru

Adapter

Adapter

Adapter

The consumer 

of the Service

The contract which specifies what the client needs

Some service like mailing or caching

The adapter is a class that can work with both

the client and the service

by implementing the Client Interface and delegate to the service

Adapter

Implementing the Adapter

class Target {
    public request(): string {
        return 'Target: The default target\'s behavior.';
    }
}
class Adaptee {
    public specificRequest(): string {
        return '.eetpadA eht fo roivaheb laicepS';
    }
}

Implementing the Adapter

class Adapter extends Target {
  constructor(private adaptee: Adaptee) {}

  public request(): string {
    const result = this.adaptee
      .specificRequest()
      .split('')
      .reverse()
      .join('')
    return `Adapter: (TRANSLATED) ${result}`
  }
}

Implementing the Adapter

Implementing the Adapter

function clientCode(target: Target) {
  console.log(target.request())
}

console.log(
  'Client: I can work just fine with the Target objects:',
)
const target = new Target()
clientCode(target)

console.log('')

const adaptee = new Adaptee()
console.log(
  "Client: The Adaptee class has a weird interface. See, I don't understand it:",
)
console.log(`Adaptee: ${adaptee.specificRequest()}`)

console.log('')

console.log(
  'Client: But I can work with it via the Adapter:',
)
const adapter = new Adapter(adaptee)
clientCode(adapter)

Implementing the Adapter

Implementing the Adapter

Bridge

Intent Bridge is a structural design pattern that lets you split a large class or a set of closely related classes into two separate hierarchies—abstraction and implementation—which can be developed independently of each other.

Image from refactoring.guru

Bridge

The abstraction provides a high level control

The implementation defines the interface for common implementations

Concrete implementations contain 

specific logic for some service

More like concrete

implementation of abstraction A

The client only uses the provided abstraction

Bridge

Bridge

class Abstraction {
  protected implementation: Implementation

  constructor(implementation: Implementation) {
    this.implementation = implementation
  }

  public operation(): string {
    const result =
      this.implementation.operationImplementation()
    return `Abstraction: Base operation with:\n${result}`
  }
}

Bridge

Bridge

class ExtendedAbstraction extends Abstraction {
  public operation(): string {
    const result =
      this.implementation.operationImplementation()
    return `ExtendedAbstraction: Extended operation with:\n${result}`
  }
}

Bridge

Bridge

interface Implementation {
    operationImplementation(): string;
}

Bridge

Bridge

function clientCode(abstraction: Abstraction) {
  console.log(abstraction.operation())
}

let implementation = new ConcreteImplementationA()
let abstraction = new Abstraction(implementation)
clientCode(abstraction)

console.log('')

implementation = new ConcreteImplementationB()
abstraction = new ExtendedAbstraction(implementation)
clientCode(abstraction)

Bridge

Bridge

Composite

Intent  Composite is a structural design pattern that lets you compose objects into tree structures and then work with these structures as if they were individual objects.

Image from refactoring.guru

Composite

The client should be able

to work with Leaf as it does

with a Composite

Component describes how

each Leaf/Composite shall

behave

Leaf is a single element that has no sub-elements underneath it

Composite is the same as a Leaf, it adheres to the Component interface, but it can have multiple Leaves underneath it as well as having multiple Composites.

As the name suggests, a Composite is a composition of Leaves/Composites

Composite

Implementing composition

export interface Component<T> {
  execute(): string
}
abstract class Leaf<T> implements Component<T> {
  execute(): string {
    return 'hello from leaf'
  }
}

Implementing composition

export abstract class Composite<T> implements Component<T> {
  private _children: Component<T>[] = []

  public get children(): Component<T>[] {
    return this._children
  }

  execute(): string {
    return 'Hello from composite'
  }

  operation() {
    this._children.forEach((child) => {
      child.execute()
    })
  }

  add(component: Component<T>) {
    this._children.push(component)
  }

  remove(component: Component<T>) {
    this._children.splice(
      this._children.indexOf(component),
      1,
    )
  }
}

Implementing composition

Implementing composition

export class Todo<T> extends Leaf<T> {}
export class Project<T> extends Composite<Todo<T>> {}

Implementing composition

Implementing composition

Decorator

Intent Decorator is a structural design pattern that lets you attach new behaviors to objects by placing these objects inside special wrapper objects that contain the behaviors.

Image from refactoring.guru

Decorator

The common interface for both components and their decorators

The wrappable component that may need extra functionality

The client can wrap a component in multiple decorators to achieve more functional component

Provides the contract for all decorators to add extra behavior for our components

Variant extra functionalities that can be nested

Decorator

Implementing decorator

interface Component {
    operation(): string;
}
class ConcreteComponent implements Component {
  public operation(): string {
    return 'ConcreteComponent'
  }
}
class Decorator implements Component {
  protected component: Component

  constructor(component: Component) {
    this.component = component
  }

  public operation(): string {
    return this.component.operation()
  }
}

Can be an abstraction aswell

Implementing decorator

class ConcreteDecoratorA extends Decorator {
  public operation(): string {
    return `ConcreteDecoratorA(${super.operation()})`
  }
}
class ConcreteDecoratorB extends Decorator {
  public operation(): string {
    return `ConcreteDecoratorB(${super.operation()})`
  }
}

Implementing decorator

Implementing decorator

function clientCode(component: Component) {
  console.log(`RESULT: ${component.operation()}`)
}

const simple = new ConcreteComponent()
console.log("Client: I've got a simple component:")
clientCode(simple)
console.log('')

const decorator1 = new ConcreteDecoratorA(simple)
const decorator2 = new ConcreteDecoratorB(decorator1)
console.log("Client: Now I've got a decorated component:")
clientCode(decorator2)

Implementing decorator

Facade

Intent Facade is a structural design pattern that provides a simplified interface to a library, a framework, or any other complex set of classes.

Image from refactoring.guru

Facade

The facade produces

a suitable wrapper to handle subsystem functionality

Can be added to remove any complex functionality from the main facade, can be used by both the facade and the client

Some complex functionality like handling the Filesystem entity or some other entity that must be used altogether

The client uses the facade instead of the subsystem directly.

Note: The construction of a facade may

be convoluted, thus, other patterns can be used

Implementing the facade

class Facade {
  constructor(
    protected subsystem1: Subsystem1,
    protected subsystem2: Subsystem2,
  ) {}

  public operation(): string {
    let result = 'Facade initializes subsystems:\n'
    result += this.subsystem1.operation1()
    result += this.subsystem2.operation1()
    result +=
      'Facade orders subsystems to perform the action:\n'
    result += this.subsystem1.operationN()
    result += this.subsystem2.operationZ()

    return result
  }
}

Implementing the facade

class Subsystem1 {
  public operation1(): string {
    return 'Subsystem1: Ready!\n'
  }

  // ...

  public operationN(): string {
    return 'Subsystem1: Go!\n'
  }
}
class Subsystem2 {
  public operation1(): string {
    return 'Subsystem2: Get ready!\n'
  }

  // ...

  public operationZ(): string {
    return 'Subsystem2: Fire!'
  }
}

Implementing the facade

Implementing the facade

function clientCode(facade: Facade) {
  // ...

  console.log(facade.operation())

  // ...
}
const subsystem1 = new Subsystem1()
const subsystem2 = new Subsystem2()
const facade = new Facade(subsystem1, subsystem2)
clientCode(facade)

Implementing the facade

Implementing the facade

Flyweight

Intent Flyweight is a structural design pattern that lets you fit more objects into the available amount of RAM by sharing common parts of the state between multiple objects instead of keeping all of the data in each object

Image from refactoring.guru

Flyweight

The class we want to create multiple instances of

The class we want to create multiple instances of

The class we want to create multiple instances of

The class we want to create multiple instances of

Flyweight

Implementing flyweight

class Flyweight {
  private sharedState: any

  constructor(sharedState: any) {
    this.sharedState = sharedState
  }

  public operation(uniqueState): void {
    const s = JSON.stringify(this.sharedState)
    const u = JSON.stringify(uniqueState)
    console.log(
      `Flyweight: Displaying shared (${s}) and unique (${u}) state.`,
    )
  }
}

Implementing flyweight

class FlyweightFactory {
  private flyweights: { [key: string]: Flyweight } = <any>{}

  constructor(initialFlyweights: string[][]) {
    for (const state of initialFlyweights) {
      this.flyweights[this.getKey(state)] = new Flyweight(
        state,
      )
    }
  }

  private getKey(state: string[]): string {
    return state.join('_')
  }
}

Implementing flyweight

Implementing flyweight

class FlyweightFactory {
  /* Previous logic */

  public getFlyweight(sharedState: string[]): Flyweight {
    const key = this.getKey(sharedState)

    if (!(key in this.flyweights)) {
      console.log(
        "FlyweightFactory: Can't find a flyweight, creating new one.",
      )

      this.flyweights[key] = new Flyweight(sharedState)
    }

    return this.flyweights[key]
  }
}

Implementing flyweight

Implementing flyweight

class FlyweightFactory {
  /* Previous logic */

  public listFlyweights(): void {
    const count = Object.keys(this.flyweights).length

    console.log(
      `\nFlyweightFactory: I have ${count} flyweights:`,
    )

    for (const key in this.flyweights) {
      console.log(key)
    }
  }
}

Implementing flyweight

Proxy

Intent Proxy is a structural design pattern that lets you provide a substitute or placeholder for another object. A proxy controls access to the original object, allowing you to perform something either before or after the request gets through to the original object.

Image from refactoring.guru

Proxy

Some contract for service, 

maybe a payment gateway

and you can have multiple concretes out of it

The actual concrete

implementation of the service

contract, maybe Stripe API

The proxy that provides the

same API code as the concrete

Service but provides different

behaviour that suits your need

The client should be using

the proxy as if it was the service

without any difference in his

application work flow

Proxy

Proxy

interface Subject {
    request(): void;
}

class RealSubject implements Subject {
  public request(): void {
    console.log('RealSubject: Handling request.')
  }
}

Proxy

Proxy

class Proxy implements Subject {
  private realSubject: RealSubject

  constructor(realSubject: RealSubject) {
    this.realSubject = realSubject
  }

  public request(): void {
    if (this.checkAccess()) {
      this.realSubject.request()
      this.logAccess()
    }
  }

  private checkAccess(): boolean {
    console.log(
      'Proxy: Checking access prior to firing a real request.',
    )

    return true
  }

  private logAccess(): void {
    console.log('Proxy: Logging the time of request.')
  }
}

Proxy

Proxy

function clientCode(subject: Subject) {
  // ...

  subject.request()

  // ...
}

console.log(
  'Client: Executing the client code with a real subject:',
)
const realSubject = new RealSubject()
clientCode(realSubject)

console.log('')

console.log(
  'Client: Executing the same client code with a proxy:',
)
const proxy = new Proxy(realSubject)
clientCode(proxy)

Proxy

Structural patterns comparison

Adapter

Facade

Proxy

Bridge

Decorator

Builts a communication channel between two different interfaces

Provides a way of modifying the behaviour that an object does.

Provides a communication channel between different abstractions

Builts a communication channel between two different interfaces

Provides a simple/single interface to cover up for a whole mess of decoupled system

Builts a communication channel between two different interfaces

Provides the same interface of some functionality with additional/less functionality or different accessibility levels

Behavioural patterns

Chain of resp

Command

Interpreter

Iterator

Mediator

Memento

Observer

Strategy

State

Temp Method

Visitor

Chain of responsibility

Intent Chain of Responsibility is a behavioural design pattern that lets you pass requests along a chain of handlers. Upon receiving a request, each handler decides either to process the request or to pass it to the next handler in the chain.

Image from refactoring.guru

Defines an interface for handling a request.

Maintains a reference (successor) to the next Handler object on the chain.

The client wants to perform some tasks via composing multiple sub-tasks

Multiple handlers to process the request/code in several steps.

Chain of responsibility

Base handler is optional but, if existed, it provides the skeleton for handling some logic.

Implementing chain of responsibility

interface Handler {
  setNext(handler: Handler): Handler

  handle(request: string): string | null
}
abstract class AbstractHandler implements Handler {
  private nextHandler!: Handler

  public setNext(handler: Handler): Handler {
    this.nextHandler = handler
    return handler
  }

  public handle(request: string): string | null {
    if (this.nextHandler) {
      return this.nextHandler.handle(request)
    }

    return null
  }
}

Implementing chain of responsibility

class SquirrelHandler extends AbstractHandler {
  public handle(request: string): string | null {
    if (request === 'Nut') {
      return `Squirrel: I'll eat the ${request}.`
    }
    return super.handle(request)
  }
}
class DogHandler extends AbstractHandler {
  public handle(request: string): string | null {
    if (request === 'MeatBall') {
      return `Dog: I'll eat the ${request}.`
    }
    return super.handle(request)
  }
}
class SquirrelHandler extends AbstractHandler {
  public handle(request: string): string | null {
    if (request === 'Nut') {
      return `Squirrel: I'll eat the ${request}.`
    }
    return super.handle(request)
  }
}

Implementing chain of responsibility

function clientCode(handler: Handler) {
  const foods = ['Nut', 'Banana', 'Cup of coffee']

  for (const food of foods) {
    console.log(`Client: Who wants a ${food}?`)

    const result = handler.handle(food)
    if (result) {
      console.log(`  ${result}`)
    } else {
      console.log(`  ${food} was left untouched.`)
    }
  }
}
const monkey = new MonkeyHandler()
const squirrel = new SquirrelHandler()
const dog = new DogHandler()

monkey.setNext(squirrel).setNext(dog)
console.log('Chain: Monkey > Squirrel > Dog\n')
clientCode(monkey)
console.log('')

console.log('Subchain: Squirrel > Dog\n')
clientCode(squirrel)

Command

Intent Command is a behavioral design pattern that turns a request into a stand-alone object that contains all information about the request. This transformation lets you pass requests as a method arguments, delay or queue a request’s execution, and support undoable operations.

Image from refactoring.guru

Command

The invoker is like the

remote controller or the Commander

The entity that's responsible for sending commmands

The command is the contract that all our concretes must adhere to in order to have unified API

Obviously, concrete implementations, a.k.a actual commnds

The receiver is what we're operating our commands on

Command

Implementing command

interface Command {
  execute(): void
}

Implementing command

class SimpleCommand implements Command {
  private payload: string

  constructor(payload: string) {
    this.payload = payload
  }

  public execute(): void {
    console.log(
      `SimpleCommand: See, I can do simple things like printing (${this.payload})`,
    )
  }
}

Implementing command

Implementing command

class ComplexCommand implements Command {
  constructor(
    private receiver: Receiver,
    private a: string,
    private b: string,
  ) {
    this.receiver = receiver
    this.a = a
    this.b = b
  }

  public execute(): void {
    console.log(
      'ComplexCommand: Complex stuff should be done by a receiver object.',
    )
    this.receiver.doSomething(this.a)
    this.receiver.doSomethingElse(this.b)
  }
}

Implementing command

Implementing command

class Receiver {
  public doSomething(a: string): void {
    console.log(`Receiver: Working on (${a}.)`)
  }

  public doSomethingElse(b: string): void {
    console.log(`Receiver: Also working on (${b}.)`)
  }
}

Implementing command

Implementing command

class Invoker {
  private onStart: Command

  private onFinish: Command

  public setOnStart(command: Command): void {
    this.onStart = command
  }

  public setOnFinish(command: Command): void {
    this.onFinish = command
  }

  private isCommand(object): object is Command {
    return object.execute !== undefined
  }
}

Implementing command

Implementing command

class Invoker {
  /* previous logic */

  public doSomethingImportant(): void {
    console.log(
      'Invoker: Does anybody want something done before I begin?',
    )
    if (this.isCommand(this.onStart)) {
      this.onStart.execute()
    }

    console.log(
      'Invoker: ...doing something really important...',
    )

    console.log(
      'Invoker: Does anybody want something done after I finish?',
    )
    if (this.isCommand(this.onFinish)) {
      this.onFinish.execute()
    }
  }
}

Implementing command

Implementing command

const invoker = new Invoker()
invoker.setOnStart(new SimpleCommand('Say Hi!'))
const receiver = new Receiver()
invoker.setOnFinish(
  new ComplexCommand(receiver, 'Send email', 'Save report'),
)

invoker.doSomethingImportant()

Implementing command

Implementing command

Iterator

Intent Iterator is a behavioral design pattern that lets you traverse elements of a collection without exposing its underlying representation (list, stack, tree, etc.).

Image from refactoring.guru

Iterator

Defines the contract for all iterators, whether they support backward iterations, resetting cursor, and other operations

The iterator implementation itself, keeps track of the current position in the traversal of the aggregation.

Also known as the Aggregator

Defines the contract for creating iterator objects.

Implements the Aggregate/IterableCollection contract to return some specific iterator.

Iterator

Implementing iterator

interface Iterator<T> {
  current(): T

  next(): T

  key(): number

  valid(): boolean

  rewind(): void
}

Implementing iterator

interface Aggregator {
  getIterator(): Iterator<string>
}

interface IterableCollection {
  getIterator(): Iterator<string>
}

Implementing iterator

Implementing iterator

export class WordsCollection implements Aggregator {
  private words: string[] = []

  getIterator(): Iterator<string> {
    return new AlphabetIterator(this)
  }

  getCount(): number {
    return this.words.length
  }

  getItems(): string[] {
    return this.words
  }

  addWord(word: string): void {
    this.words.push(word)
  }

  getReverseIterator(): Iterator<string> {
    return new AlphabetIterator(this, true)
  }
}

Implementing iterator

Implementing iterator

export class AlphabetIterator implements Iterator<string> {
  private position: number = 0

  constructor(
    private collection: WordsCollection,
    private reverse: boolean = false,
  ) {
    if (reverse) {
      this.position = collection.getCount() - 1
    }
  }

  current(): string {...}

  next(): string {...}

  key(): number {...}

  valid(): boolean {...}

  rewind(): void {...}
}

Implementing iterator

Implementing iterator

Observer

Intent Observer is a behavioural design pattern that lets you define a subscription mechanism to notify multiple objects about any events that happen to the object they’re observing.

Image from refactoring.guru

Observer

Also known as the Subject or Observable, it defines the thing we subscribe to or observe

The observer itself that waits for actions from whatever it subscribes to

The implementations of our subscriber a.k.a observer.

Observer

Implementing observer

interface Observable {
  // Attach an observer to the subject.
  attach(observer: Observer): void

  // Detach an observer from the subject.
  detach(observer: Observer): void

  // Notify all observers about an event.
  notify(): void
}

Implementing observer

/**
 * The Observer interface declares the update method, used by subjects.
 */
interface Observer {
  // Receive update from subject.
  update(observable: Observable): void
}

Implementing observer

Implementing observer

class ConcreteObserverA implements Observer {
  public update(subject: Subject): void {
    if (
      subject instanceof ConcreteSubject &&
      subject.state < 3
    ) {
      console.log(
        'ConcreteObserverA: Reacted to the event.',
      )
    }
  }
}

class ConcreteObserverB implements Observer {
  public update(subject: Subject): void {
    if (
      subject instanceof ConcreteSubject &&
      (subject.state === 0 || subject.state >= 2)
    ) {
      console.log(
        'ConcreteObserverB: Reacted to the event.',
      )
    }
  }
}

Implementing observer

Implementing observer

class ConcreteObservable implements Observable {
  public state: number

  private observers: Observer[] = []

  public attach(observer: Observer): void {
    const isExist = this.observers.includes(observer)
    if (isExist) {
      return console.log(
        'Subject: Observer has been attached already.',
      )
    }

    console.log('Subject: Attached an observer.')
    this.observers.push(observer)
  }
}

Implementing observer

Implementing observer

class ConcreteObservable implements Observable {
  // previous implementation

  public detach(observer: Observer): void {
    const observerIndex = this.observers.indexOf(observer)
    if (observerIndex === -1) {
      return console.log('Subject: Nonexistent observer.')
    }

    this.observers.splice(observerIndex, 1)
    console.log('Subject: Detached an observer.')
  }
}

Implementing observer

Implementing observer

class ConcreteObservable implements Observable {
  // previous implementation

  public notify(): void {
    console.log('Subject: Notifying observers...')
    for (const observer of this.observers) {
      observer.update(this)
    }
  }
}

Implementing observer

Implementing observer

class ConcreteObservable implements Observable {
  // previous implementation

  public someBusinessLogic(): void {
    console.log("\nSubject: I'm doing something important.")
    this.state = Math.floor(Math.random() * (10 + 1))

    console.log(
      `Subject: My state has just changed to: ${this.state}`,
    )

    this.notify()
  }
}

Implementing observer

Implementing observer

// client code
const subject = new ConcreteSubject()

const observer1 = new ConcreteObserverA()
subject.attach(observer1)

const observer2 = new ConcreteObserverB()
subject.attach(observer2)

subject.someBusinessLogic()
subject.someBusinessLogic()

subject.detach(observer2)

subject.someBusinessLogic()

Implementing observer

State

Intent State is a behavioural design pattern that lets an object alter its behaviour when its internal state changes. It appears as if the object changed its class.

Image from refactoring.guru

State

The API expects to have variant states and act upon these variations.
It accepts a State implementation, maybe it's initialState, however, it delegates its work to it

The contract that defines how your state will be represented, it must have the same methods your Context wants to have delegated

The concrete implementations of your state contract, which defines how your class will behave due to the current state

State

Implementing state

class Context {
  constructor(private state: State) {
    this.transitionTo(state)
  }

  public transitionTo(state: State): void {
    console.log(
      `Context: Transition to ${
        (<any>state).constructor.name
      }.`,
    )
    this.state = state
    this.state.setContext(this)
  }

  public request1(): void {
    this.state.handle1()
  }

  public request2(): void {
    this.state.handle2()
  }
}

Implementing state

abstract class State {
  protected context!: Context

  public setContext(context: Context) {
    this.context = context
  }

  public abstract handle1(): void

  public abstract handle2(): void
}

Implementing state

Implementing state

class ConcreteStateA extends State {
  public handle1(): void {
    console.log('ConcreteStateA handles request1.')
    console.log(
      'ConcreteStateA wants to change the state of the context.',
    )
    this.context.transitionTo(new ConcreteStateB())
  }

  public handle2(): void {
    console.log('ConcreteStateA handles request2.')
  }
}

Implementing state

Implementing state

class ConcreteStateB extends State {
  public handle1(): void {
    console.log('ConcreteStateB handles request1.')
  }

  public handle2(): void {
    console.log('ConcreteStateB handles request2.')
    console.log(
      'ConcreteStateB wants to change the state of the context.',
    )
    this.context.transitionTo(new ConcreteStateA())
  }
}

Implementing state

Implementing state

/* client code */
const context = new Context(new ConcreteStateA())
context.request1()
context.request2()

Implementing state

Implementing state

Strategy

Intent Strategy is a behavioural design pattern that lets you define a family of algorithms, put each of them into a separate class, and make their objects interchangeable.

Image from refactoring.guru

Strategy

The class which will make use of one strategy, can be thought of as the client code that uses the exported strategy via some API code.

The contract that all our strategies have to adhere to

The actual strategies we'll use during the runtime

Strategy

Strategy

class Context {
  private strategy: Strategy

  constructor(strategy: Strategy) {
    this.strategy = strategy
  }

  public setStrategy(strategy: Strategy) {
    this.strategy = strategy
  }

  public doSomeBusinessLogic(list: string[]): void {
    console.log(
      "Context: Sorting data using the strategy (not sure how it'll do it)",
    )
    const result = this.strategy.doAlgorithm(list)
    console.log(result.join(','))
  }
}

Strategy

Strategy

interface Strategy {
  doAlgorithm(data: string[]): string[]
}

Strategy

Strategy

class ConcreteStrategyA implements Strategy {
  public doAlgorithm(data: string[]): string[] {
    return data.sort()
  }
}

class ConcreteStrategyB implements Strategy {
  public doAlgorithm(data: string[]): string[] {
    return data.reverse()
  }
}

Strategy

Strategy

const context = new Context(new ConcreteStrategyA())
console.log('Client: Strategy is set to normal sorting.')
context.doSomeBusinessLogic(['c', 'd', 'b', 'e', 'a'])

console.log('')

console.log('Client: Strategy is set to reverse sorting.')
context.setStrategy(new ConcreteStrategyB())
context.doSomeBusinessLogic(['a', 'b', 'c', 'd', 'e'])

Strategy

Strategy

Wrapping Up

Wrapping Up

  • SOLID principles are your first way of refactoring
  • Creational design patterns help you construct objects properly
  • Structural design patterns initiate communication between objects
  • Behavioural design patterns define how your objects respond to certain actions
  • Design patterns are mostly used when building complex software
  • Try to not overcomplicate things by implementing design patterns everywhere (KISS)
  • If you're refactoring something, question yourself if you truly need to do it (YAGNI)

SecTheater

  • SOLID principles are your first way of refactoring
  • Creational design patterns help you construct objects properly
  • Structural design patterns initiate communication between objects
  • Behavioural design patterns define how your objects respond to certain actions
  • Design patterns are mostly used when building complex software
  • Try to not overcomplicate things by implementing design patterns everywhere (KISS)
  • If you're refactoring something, question yourself if you truly need to do it (YAGNI)

Wrapping Up

SecTheater

Raise up your techinal skills

SecTheater

Theoretical object oriented design - SOLID principles and design patterns

By Security Theater

Theoretical object oriented design - SOLID principles and design patterns

A theoretical walkthrough on how to design your application in a clean way using the SOLID principles and Design patterns and practical implementation in PHP (Laravel), JavaScript (Node) and GoLang.

  • 58