你應該知道的 5 種 TypeScript設計模式

前端小智發表於2022-01-07
作者:Fernando Doglio
譯者:前端小智
來源:medium

有夢想,有乾貨,微信搜尋 【大遷世界】 關注這個在凌晨還在刷碗的刷碗智。

本文 GitHub https://github.com/qq449245884/xiaozhi 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。

設計模式是可以幫助開發人員解決問題的模板。在本中涉及的模式太多了,而且它們往往針對不同的需求。但是,它們可以被分為三個不同的組:

  • 結構模式處理不同元件(或類)之間的關係,並形成新的結構,以提供新的功能。結構模式的例子有CompositeAdapterDecorator
  • 行為模式將元件之間的公共行為抽象成一個獨立的實體。行為模式的例子有命令、策略和我個人最喜歡的一個:觀察者模式
  • 建立模式 專注於類的例項化,讓我們更容易建立新的實體。我說的是工廠方法,單例和抽象工廠。

單例模式

單例模式可能是最著名的設計模式之一。它是一種建立模式,因為它確保無論我們嘗試例項化一個類多少次,我們都只有一個可用的例項。

處理資料庫連線之類的可以單例模式,因為我們希望一次只處理一個,而不必在每個使用者請求時重新連線。

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())
})

現在,雖然不能直接例項化類,但是使用getInstance方法,可以確保不會有多個例項。在上面的示例中,可以看到包裝資料庫連線的偽類如何從該模式中獲益。

這個事例展示了無論我們呼叫getInstance方法多少次,這個連線總是相同的。

上面的執行結果:

0.4047087250990713
0.4047087250990713
0.4047087250990713
0.4047087250990713
0.4047087250990713

工廠模式

工廠模式是一種建立模式,就像單例模式一樣。但是,這個模式並不直接在我們關心的物件上工作,而是隻負責管理它的建立。

解釋一下:假設我們通過編寫程式碼來模擬移動車輛,車有很多型別,例如汽車、自行車和飛機,移動程式碼應該封裝在每個vehicle類中,但是呼叫它們的move 方法的程式碼可以是通用的。

這裡的問題是如何處理物件建立?可以有一個具有3個方法的單一creator類,或者一個接收引數的方法。在任何一種情況下,擴充套件該邏輯以支援建立更多vehices都需要不斷增長相同的類。

但是,如果決定使用工廠方法模式,則可以執行以下操作:

clipboard.png

現在,建立新物件所需的程式碼被封裝到一個新類中,每個類對應一個車輛型別。這確保瞭如果將來需要新增車輛,只需要新增一個新類,而不需要修改任何已經存在的東西。

接著來看看,我們如何使用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()

上面的程式碼很多,但我們可以使用上面的圖表來理解它。本質上最後,我們關心的是自定義處理程式,這裡稱它為處理程式,而不是創造者,因為他們不只是建立的物件,他們也有邏輯,使用它們(moveVehicle方法)。

這個模式的美妙之處在於,如果您你要新增一個新的vehicle型別,所要做的就是新增它的vehicle類和它的處理程式類,而不增加任何其他類的LOC。

觀察者模式

在所有的模式,我最喜歡的是觀察者模式,因為型別的行為我們可以實現它。

它是如何工作的呢?本質上,該模式表明你擁有一組觀察者物件,這些物件將對被觀察實體狀態的變化做出反應。為了實現這一點,一旦在被觀察端接收到一個更改,它就負責通過呼叫它的一個方法來通知它的觀察者。

在實踐中,此模式的實現相對簡單,讓我們快速檢視一下程式碼,然後回顧一下

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()

正如你所看到的,通過兩個抽象類,我們可以定義Observer,該觀察者將表示對Observable實體上的更改做出反應的物件。 在上面的示例中,我們假設具有一個被單擊的InputElement實體(類似於在前端具有HTML輸入欄位的方式),以及一個ConsoleLogger,用於記錄控制檯發生的所有事情。

這種模式的優點在於,它使我們能夠了解Observable的內部狀態並對其做出反應,而不必弄亂其內部程式碼。 我們可以繼續新增執行其他操作的觀察者,甚至包括對特定事件做出反應的觀察者,然後讓它們的程式碼決定對每個通知執行的操作。

裝飾模式

裝飾模式試圖在執行時向現有物件新增行為。 從某種意義上說,我們可以將其視為動態繼承,因為即使沒有建立新類來新增行為,我們也正在建立具有擴充套件功能的新物件。

這樣考慮:假設我們擁有一個帶有move方法的Dog類,現在您想擴充套件其行為,因為我們想要一隻超級狗和一隻可以游泳的狗。

通常,我們需要在 Dog 類中新增move 行為,然後以兩種方式擴充套件該類,即SuperDogSwimmingDog類。 但是,如果我們想將兩者混合在一起,則必須再次建立一個新類來擴充套件它們的行為,但是,有更好的方法。

組合讓我們可以將自定義行為封裝在不同的類中,然後使用該模式通過將原始物件傳遞給它們的建構函式來建立這些類的新例項。 讓我們看一下程式碼:


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()

注意幾個細節:

  • 實際上,SuperDecorator類擴充套件了Animal類,與Dog類擴充套件了相同的類。 這是因為裝飾器需要提供與其嘗試裝飾的類相同的公共介面。
  • SuperDecorator類是abstract ,這意味著並沒有使用它,只是使用它來定義建構函式,該建構函式會將原始物件的副本保留在受保護的屬性中。 公共介面的覆蓋是在自定義裝飾器內部完成的。
  • SuperAnimalSwimmingAnimal是實際的裝飾器,它們是新增額外行為的裝飾器。

進行此設定的好處是,由於所有裝飾器也間接擴充套件了Animal類,因此如果你要將兩種行為混合在一起,則可以執行以下操作:

const superSwimmingDog =  new SwimmingAnimal(superDog)

superSwimmingDog.move()

Composite(組合)

關於Composite模式,其實就是組合模式,又叫部分整體模式,這個模式在我們的生活中也經常使用。

比如編寫過前端的頁面,肯定使用過<div>等標籤定義一些格式,然後格式之間互相組合,通過一種遞迴的方式組織成相應的結構,這種方式其實就是組合,將部分的元件鑲嵌到整體之中。

關於此模式的有趣之處在於,它不是一個簡單的物件組,它可以包含實體或實體組,每個組可以同時包含更多組,這就是我們所說的樹。

看一個例子:

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())

在上面的示例中,我們可以將product 放入Box中,也可以將Box放入其他Box中,這是組合的經典示例。因為我們要實現的是獲得完整的交付價格,因此需要在大box裡新增每個元素的價格(包括每個小box的價格)。

上面執行的結果:

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

因此,在處理遵循同一介面的多個物件時,請考慮使用此模式。 通過將複雜性隱藏在單個實體(組合本身)中,您會發現它有助於簡化與小組的互動方式。

今天的分享就到這裡了,感謝大家的觀看,我們下期再見。


原文:https://blog.bitsrc.io/design...

程式碼部署後可能存在的BUG沒法實時知道,事後為了解決這些BUG,花了大量的時間進行log 除錯,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug

交流

有夢想,有乾貨,微信搜尋 【大遷世界】 關注這個在凌晨還在刷碗的刷碗智。

本文 GitHub https://github.com/qq44924588... 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。

相關文章