Five TypeScript design patterns you should know

Posted by plodos on Wed, 05 Jan 2022 06:45:52 +0100

By Fernando Doglio
Translator: front end Xiaozhi
Source: medium

There are dreams and dry goods. Wechat search [Daqian world] pays attention to this bowl washing wisdom who is still washing dishes in the early morning.

This article GitHub https://github.com/qq449245884/xiaozhi It has been included. There are complete test sites, materials and my series of articles for the interview of front-line large factories.

Design patterns are templates that can help developers solve problems. There are too many patterns involved in this, and they often target different needs. However, they can be divided into three different groups:

  • Structural patterns deal with the relationship between different components (or classes) and form new structures to provide new functions. Examples of structural patterns are Composite, Adapter, and Decorator.
  • Behavior patterns abstract the common behavior between components into an independent entity. Examples of behavior patterns are commands, strategies, and my personal favorite: the observer pattern.
  • The creation pattern focuses on class instantiation, which makes it easier to create new entities. I'm talking about factory methods, singletons and abstract factories.

Singleton mode

The singleton pattern is probably one of the most famous design patterns. It is a creation pattern because it ensures that no matter how many times we try to instantiate a class, we have only one available instance.

Singleton mode can be used for processing database connections, because we want to process only one at a time without having to reconnect at each user's request.

class MyDBConn {
  protected static instance: MyDBConn | null = null
  private id:number = 0

  constructor() {
    this.id = Math.random()
  }

  public getID():number {
    return this.id
  }

  public static getInstance():MyDBConn {
    if (!MyDBConn.instance) {
      MyDBConn.instance = new MyDBConn()
    }
    return MyDBConn.instance
  }
}

const connections = [
  MyDBConn.getInstance(),
  MyDBConn.getInstance(),
  MyDBConn.getInstance(),
  MyDBConn.getInstance(),
  MyDBConn.getInstance()
]

connections.forEach( c => {
    console.log(c.getID())
})

Now, although the class cannot be instantiated directly, using the getInstance method can ensure that there will not be multiple instances. In the example above, you can see how the pseudo classes that wrap database connections benefit from this pattern.

This example shows that the connection is always the same no matter how many times we call the getInstance method.

The above operation results:

0.4047087250990713
0.4047087250990713
0.4047087250990713
0.4047087250990713
0.4047087250990713

Factory mode

Factory mode is a creation mode, just like singleton mode. However, this pattern does not work directly on the object we care about, but is only responsible for managing its creation.

Explain: suppose we simulate mobile vehicles by writing code. There are many types of vehicles, such as cars, bicycles and aircraft. The mobile code should be encapsulated in each vehicle class, but the code calling their move method can be general.

The question here is how to deal with object creation? You can have a single creator class with three methods, or a method that receives parameters. In either case, extending this logic to support the creation of more vehicles requires growing the same classes.

However, if you decide to use the factory method mode, you can do the following:

Now, the code needed to create a new object is encapsulated in a new class, each corresponding to a vehicle type. This ensures that if you need to add vehicles in the future, you only need to add a new class without modifying anything that already exists.

Next, let's look at how we use TypeScript to achieve this:

interface Vehicle {
    move(): void
}

class Car implements Vehicle {

    public move(): void {
        console.log("Moving the car!")
    }
}

class Bicycle implements Vehicle {

    public move(): void {
        console.log("Moving the bicycle!")
    }
}

class Plane implements Vehicle {

    public move(): void {
        console.log("Flying the plane!")
    }
}

// VehicleHandler is "abstract" because no one will instantiate it
// We want to extend it and implement abstract methods
abstract class VehicleHandler {

    // This is the method that the real handler needs to implement
    public abstract createVehicle(): Vehicle 

    public moveVehicle(): void {
        const myVehicle = this.createVehicle()
        myVehicle.move()
    }
} 

class PlaneHandler extends VehicleHandler{

    public createVehicle(): Vehicle {
        return new Plane()
    }
}

class CarHandler  extends VehicleHandler{

    public createVehicle(): Vehicle {
        return new Car()
    }
}

class BicycleHandler  extends VehicleHandler{

    public createVehicle(): Vehicle {
        return new Bicycle()
    }
}

/// User code...
const planes = new PlaneHandler()
const cars = new CarHandler()

planes.moveVehicle()
cars.moveVehicle()

There is a lot of code above, but we can use the chart above to understand it. In essence, finally, we are concerned about custom handlers, which are called handlers here, not creators, because they are not only created objects, but also have logic to use them (moveVehicle method).

The beauty of this pattern is that if you want to add a new vehicle type, all you have to do is add its vehicle class and its handler class without adding the LOC of any other class.

Observer mode

Of all the patterns, my favorite is the observer pattern, because of the type of behavior we can implement it.

How does it work? In essence, this pattern indicates that you have a set of observer objects that will respond to changes in the state of the observed entity. To achieve this, once a change is received at the observed end, it is responsible for notifying its observer by calling one of its methods.

In practice, the implementation of this pattern is relatively simple. Let's take a quick look at the code and review it

type InternalState = {
  event: String
}

abstract class Observer {
  abstract update(state:InternalState): void
}

abstract class Observable {
  protected observers: Observer[] = []
  protected state:InternalState = { event: ""}

  public addObserver(o: Observer):void {
    this.observers.push(o)
  }

  protected notify () {
    this.observers.forEach(o => o.update(this.state))
  }
}


class ConsoleLogger extends Observer  {

    public update(newState: InternalState) {
        console.log("New internal state update: ", newState)
    }
}

class InputElement extends Observable {

    public click():void {
        this.state = { event: "click" }
        this.notify()
    }

}

const input = new InputElement()
input.addObserver(new ConsoleLogger())

input.click()

As you can see, through two abstract classes, we can define Observer, which will represent the object that responds to changes on the Observable entity. In the above example, we assume that we have a clicked InputElement entity (similar to the way we have HTML input fields in the front end) and a ConsoleLogger to record everything that happens to the console.

The advantage of this pattern is that it enables us to understand and respond to the internal state of Observable without messing up its internal code. We can continue to add observers who perform other operations, even those who respond to specific events, and then let their code decide what to do with each notification.

Decoration mode

Decoration mode attempts to add behavior to an existing object at run time. In a sense, we can think of it as dynamic inheritance, because even if we don't create a new class to add behavior, we are creating a new object with extended functionality.

Consider this: suppose we have a Dog class with a move method, and now you want to extend its behavior, because we want a super Dog and a Dog that can swim.

Usually, we need to add the move behavior to the Dog class, and then extend the class in two ways, namely SuperDog and SwimmingDog. However, if we want to mix the two together, we must create a new class again to extend their behavior, but there are better ways.

Composition allows us to encapsulate custom behaviors in different classes, and then use this pattern to create new instances of these classes by passing the original objects to their constructors. Let's look at the code:

abstract class Animal {

    abstract move(): void
}

abstract class SuperDecorator extends Animal {
    protected comp: Animal
    
    constructor(decoratedAnimal: Animal) {
        super()
        this.comp = decoratedAnimal
    }
    
    abstract move(): void
}

class Dog extends Animal {

    public move():void {
        console.log("Moving the dog...")
    }
}

class SuperAnimal extends SuperDecorator {

    public move():void {
        console.log("Starts flying...")
        this.comp.move()
        console.log("Landing...")
    }
}

class SwimmingAnimal extends SuperDecorator {

    public move():void {
        console.log("Jumps into the water...")
        this.comp.move()
    }
}


const dog = new Dog()

console.log("--- Non-decorated attempt: ")
dog.move()

console.log("--- Flying decorator --- ")
const superDog =  new SuperAnimal(dog)
superDog.move()

console.log("--- Now let's go swimming --- ")
const swimmingDog =  new SwimmingAnimal(dog)
swimmingDog.move()

Pay attention to several details:

  • In fact, the SuperDecorator class extends the Animal class, the same class as the Dog class. This is because the decorator needs to provide the same public interface as the class it is trying to decorate.
  • The SuperDecorator class is abstract, which means that it is not used, but is used to define a constructor that keeps a copy of the original object in protected properties. The coverage of the common interface is completed inside the custom decorator.
  • SuperAnimal and SwimmingAnimal are actual decorators that add additional behavior.

The advantage of this setting is that since all decorators also indirectly extend the Animal class, if you want to mix the two behaviors together, you can do the following:

const superSwimmingDog =  new SwimmingAnimal(superDog)

superSwimmingDog.move()

Composite

The Composite mode is actually a combination mode, also known as the partial overall mode. This mode is also often used in our life.

For example, if you have written a front-end page, you must have used < div > and other tags to define some formats, and then the formats are combined with each other and organized into corresponding structures in a recursive way. This method is actually combination, inlaying some components into the whole.

The interesting thing about this pattern is that it is not a simple object group. It can contain entities or entity groups. Each group can contain more groups at the same time, which is what we call a tree.

Take an example:

interface IProduct {
  getName(): string
  getPrice(): number
}

class Product implements IProduct {
  private price:number
  private name:string

  constructor(name:string, price:number) {
    this.name = name
    this.price = price
  }

  public getPrice():number {
    return this.price
  }

  public getName(): string {
    return this.name
  }
}

class Box implements IProduct {

    private products: IProduct[] = []
    
    contructor() {
        this.products = []
    }
    
    public getName(): string {
        return "A box with " + this.products.length + " products"
    } 
    
    add(p: IProduct):void {
        console.log("Adding a ", p.getName(), "to the box")
        this.products.push(p)
    }

    getPrice(): number {
        return this.products.reduce( (curr: number, b: IProduct) => (curr + b.getPrice()),  0)
    }
}

//Using the code...
const box1 = new Box()
box1.add(new Product("Bubble gum", 0.5))
box1.add(new Product("Samsung Note 20", 1005))

const box2 = new Box()
box2.add( new Product("Samsung TV 20in", 300))
box2.add( new Product("Samsung TV 50in", 800))

box1.add(box2)

console.log("Total price: ", box1.getPrice())

In the above example, we can put product into box or box into other boxes. This is a classic example of combination. Because what we want to achieve is to obtain the complete delivery price, we need to add the price of each element in the large box (including the price of each small box).

Results of the above run:

Adding a  Bubble gum to the box
Adding a  Samsung Note 20 to the box
Adding a  Samsung TV 20in to the box
Adding a  Samsung TV 50in to the box
Adding a  A box with 2 products to the box
Total price:  2105.5

Therefore, consider using this pattern when working with multiple objects that follow the same interface. By hiding complexity in a single entity (the composition itself), you will find that it helps to simplify the way you interact with the group.

That's all for today's sharing. Thank you for watching. See you next time.

Original text: https://blog.bitsrc.io/design...

The bugs that may exist after code deployment cannot be known in real time. Afterwards, in order to solve these bugs, we spent a lot of time on log debugging. By the way, we recommend a useful BUG monitoring tool Fundebug.

communication

There are dreams and dry goods. Wechat search [Daqian world] pays attention to this bowl washing wisdom who is still washing dishes in the early morning.

This article GitHub https://github.com/qq44924588... It has been included. There are complete test sites, materials and my series of articles for the interview of front-line large factories.

Topics: Front-end html5 Vue.js html