Author: Fernando Doglio
Translator: Frontend Xiaozhi
Source: medium
If you have dreams and dry goods, search for [Moving to the World] Follow this brushing wit who is still doing dishes in the early morning.
This article GitHub https://github.com/qq449245884/xiaozhi has been included, the first-line interview complete test site, information and my series of articles.
Design patterns are templates that can help developers solve problems. There are too many patterns involved in this article, and they tend to address different needs. However, they can be divided into three different groups:
- structural model handles the relationship between different components (or classes) and forms a new structure to provide new functions. Examples of structural patterns are
Composite
,Adapter
andDecorator
. - behavior model abstracts the common behavior between components into an independent entity. Examples of behavior patterns are commands, strategies, and one of my personal favorites:
observer pattern.
- The creation mode focuses on the instantiation of classes, making it easier for us 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 mode because it ensures that no matter how many times we try to instantiate a class, we only have one instance available.
Handling database connections and the like can be singleton mode, because we want to handle only one at a time, without having to reconnect 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 can ensure that there will not be multiple instances. In the above example, you can see how the pseudo-class that wraps the database connection can benefit from this pattern.
This example shows that no matter getInstance
method, the connection is always the same.
The results of the above operation:
0.4047087250990713
0.4047087250990713
0.4047087250990713
0.4047087250990713
0.4047087250990713
Factory mode
factory mode is a creation mode, just like the
singleton mode. However, this mode does not directly work on the objects we care about, but only manages its creation.
Explain: Suppose we write code to simulate moving vehicles. There are many types of vehicles, such as cars, bicycles, and airplanes. The moving code should be encapsulated in each vehicle
class, but move
method can be universal.
The question here is how to handle object creation? There can be a single creator
class with 3 methods, or a method that receives parameters. In either case, extending the logic to support the creation of more vehices
requires constant growth of the same class.
However, if you decide to use the factory method pattern, you can do the following:
Now, the code required to create a new object is encapsulated in a new class, each class corresponding to a vehicle type. This ensures that if you need to add a vehicle in the future, you only need to add a new class without modifying any existing things.
Let's take a 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 是“抽象的”,因为没有人会实例化它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()
There is a lot of code above, but we can use the diagram above to understand it. In essence, the last thing we care about is custom handlers, which are called handlers here, not creators, because they are not just created objects, they 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 any other types of LOC.
Observer mode
Among all the modes, my favorite is the observer mode, because of the type of behavior we can achieve it.
How does it work? In essence, the pattern indicates that you have a set of observer objects that will react to changes in the state of the entity being observed. In order to achieve this, once a change is received at the observed end, 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
, and this observer will represent an object that reacts to changes Observable
In the above example, we assume that there is a InputElement
entity that was clicked (similar to the way to have an HTML input field on the front end), and a ConsoleLogger
to record everything that happened in the console.
The advantage of this model is that it allows us to understand Observable
and react to it without having to mess up its internal code. We can continue to add observers who perform other operations, even observers who react to specific events, and then let their code determine the operation performed on each notification.
Decorative pattern
decoration pattern attempts to add behavior to existing objects at runtime. In a sense, we can think of it as dynamic inheritance, because even if we haven't created a new class to add behavior, we are creating new objects with extended functionality.
Think of it this way: suppose we have a move
with a method Dog
, 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 the SuperDog
and SwimmingDog
classes. However, if we want to mix the two together, we must create a new class again to extend their behavior, but there is a better way.
Composition allows us to encapsulate custom behaviors in different classes, and then use the pattern to create new instances of these classes by passing the original objects to their constructors. Let's take a 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 a few details:
- In fact, the
SuperDecorator
class extends theAnimal
class, and theDog
class extends the same class. This is because the decorator needs to provide the same public interface as the class it is trying to decorate. SuperDecorator
class isabstract
, which means that it is not used, but it is used to define a constructor that will keep a copy of the original object in a protected property. The coverage of the public interface is done inside the custom decorator.SuperAnimal
andSwimmingAnimal
are actual decorators, they are decorators that add extra 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
Regarding the Composite mode, it is actually the combined mode, also called the partial overall mode. This mode 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 the formats are combined with each other and organized into a corresponding structure in a recursive manner. This method is actually a combination, which embeds some of the components into the whole Among.
The interesting thing about this pattern is that it is not a simple object group, it can contain entities or entity groups, and each group can contain more groups at the same time. This is what we call a tree.
Look at 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 put Box
into other Box
, which is a classic example of combination. Because what we want to achieve is to get the complete delivery price, we need to add the price of each element box
box
).
The result of the above operation:
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, when dealing with multiple objects that follow the same interface, consider using this pattern. By hiding the complexity in a single entity (the combination itself), you will find that it helps simplify the way you interact with the group.
That's it for today's sharing. Thank you for watching. See you in the next issue.
Original: https://blog.bitsrc.io/design-patterns-in-typescript-e9f84de40449
code is deployed, the possible bugs cannot be known in real time. In order to solve these bugs afterwards, a lot of time was spent on log debugging. By the way, I would like to recommend a useful BUG monitoring tool Fundebug .
communicate with
There are dreams and dry goods. search 160b97b1ecffc1 [Great Move to the World] attention to the brushing wisdom who is still doing dishes in the early morning.
This article GitHub https://github.com/qq449245884/xiaozhi has been included, the first-line interview complete test site, information and my series of articles.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。