9
Author: Fernando Doglio
Translator: Front-end Xiaozhi
Source: medium

If you have dreams and dry goods, search on [Moving the World] still washing dishes in the early morning.

This article https://github.com/qq449245884/xiaozhi . There are complete test sites, materials and my series of articles for interviews with first-tier manufacturers.

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

  • Structural schema deals with relationships between different components (or classes) and forms new structures to provide new functionality. Examples of structural patterns are Composite , Adapter and Decorator .
  • Behavior Patterns Abstract common behavior between components into a separate entity. Examples of behavioral patterns are commands, policies and one of my personal favorites: Observer pattern.
  • Creation patterns focus on instantiation of classes, making it easier for us to create new entities. I'm talking about factory methods, singletons and abstract factories.

singleton pattern

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

Singleton patterns like handling database connections, as we want to handle only one at a time without having to reconnect on every user 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, it is guaranteed that there will not be multiple instances. In the example above, you can see how a pseudo-class that wraps a database connection can benefit from this pattern.

This example shows that no matter getInstance method, the connection is always the same.

The result of running the above:

0.4047087250990713
0.4047087250990713
0.4047087250990713
0.4047087250990713
0.4047087250990713

factory pattern

factory pattern is a creation pattern, just like the singleton pattern. However, this pattern does not work directly on the object we care about, but only manages its creation.

To explain: Suppose we simulate moving vehicles by writing code. There are many types of vehicles, such as cars, bicycles, and airplanes. The moving code should be encapsulated in each vehicle class, but move methods can be generic.

The question here is how to handle object creation? There can be a single creator class with 3 methods, or a method that takes a parameter. In either case, extending that logic to support creating more vehices would require growing the same class.

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

clipboard.png

Now, the code needed to create new objects is encapsulated into a new class, one for each vehicle type. This ensures that if a vehicle needs to be added in the future, only a new class needs to be added and nothing that already exists needs to be modified.

Let's see how we can achieve this TypeScript


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 是“抽象的”,因为没有人会实例化它instantiate it
// 我们要扩展它并实现抽象方法
abstract class VehicleHandler {

    // 这是真正的处理程序需要实现的方法
    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()

The code above is a lot, but we can understand it using the diagram above. Essentially in the end, we care about custom handlers, call them handlers here, not creators, because they don't just create objects, they also have logic to use them (moveVehicle method).

The beauty of this pattern is that if you were to add a new vehicle type, all you had to do was add its vehicle class and its handler class, without adding any other classes' LOC.

Observer pattern

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

How does it work? Essentially, the pattern says that you have a set of observer objects that will react to changes in the state of the observed entity. To achieve this, once a change is received on the observed side, it is responsible for notifying its observers 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 then review

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, with two abstract classes, we can define Observer , this observer will represent an object that reacts to changes Observable In the above example, we assume that we have a InputElement entity that is clicked (similar to how you would have an HTML input field on the front end), and a ConsoleLogger that logs everything that happens to the console.

The beauty of this mode is that it allows us to understand Observable and react to it without having to mess with its internal code. We can go on and add observers that do other things, even those that react to specific events, and let their code decide what to do with each notification.

decorative pattern

Decorative mode attempts to add behavior to an existing object at runtime. In a sense, we can think of it as dynamic inheritance, because even though new classes are not being created to add behavior, we are creating new objects with extended functionality.

Think of it this way: Let's say we have a move with a 061d78993ef790 method, and now you want to extend its behavior because we want a Dog

Typically, we need to add the move behavior to the Dog class and then extend the class in two ways, the SuperDog and SwimmingDog classes. However, if we wanted to mix the two, we would again have to create a new class to extend their behavior, however, there is a better way.

Composition lets us encapsulate custom behavior in different classes, and then use the pattern to create new instances of those 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()

Note a few details:

  • Indeed, SuperDecorator class extends Animal class, and Dog class extends the same class. This is because a decorator needs to provide the same public interface as the class it is trying to decorate.
  • SuperDecorator class is abstract , which means it's not used, it's just used to define a constructor that keeps a copy of the original object in a protected property. Overriding of public interfaces is done inside custom decorators.
  • SuperAnimal and SwimmingAnimal are actual decorators, they are decorators that add extra behavior.

The benefit of having this setup is that since all decorators also indirectly extend the Animal class, if you want to mix the two behaviors, you can do:

const superSwimmingDog =  new SwimmingAnimal(superDog)

superSwimmingDog.move()

Composite

Regarding the composite mode, it is actually a combination mode, also known as a part of the overall mode, which is often used in our lives.

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

The interesting thing about this pattern is that it is not a simple group of objects, it can contain entities or groups of entities, each group can contain more groups at the same time, this is what we call a tree.

See 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 Box , which is a classic example of a combination. Because what we want to achieve is to get the full delivery price, we need to add the price of each element box box ).

The result of running the above:

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 dealing with multiple objects that conform to the same interface. By hiding the complexity in a single entity (the composition itself), you'll find it helps simplify how you interact with your group.

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


Original: https://blog.bitsrc.io/design-patterns-in-typescript-e9f84de40449

code is deployed cannot be known in real time. In order to solve these bugs afterwards, a lot of time is spent on log debugging. By the way, I recommend a useful bug monitoring tool Fundebug .

comminicate

If you have dreams and dry goods, search on [Moving the World] Follow this brush bowl wisdom who is still washing dishes in the early morning.

This article https://github.com/qq449245884/xiaozhi . There are complete test sites, materials and my series of articles for interviews with first-tier manufacturers.


王大冶
68k 声望104.9k 粉丝