【TS】358- 淺析 TypeScript 設計模式
- 2019 年 10 月 6 日
- 筆記
設計模式就是軟件開發過程中形成的套路,就如同你在玩lol中的「正方形打野」,「四一分推」,又或者籃球運動中的「二夾一」,「高位單打」一樣,屬於經驗的總結。
熟悉設計模式有什麼好處呢?
- 讓你在編程過程中更有自信,使用經過無數前人印證過的最好的設計,自然底氣十足
- 提升編程效率,避免開發過程中的猶豫
- 更能掌控項目,方便預估開發時間,方便對團隊成員進行管理
由於設計模式和軟件開發的語言,平台都沒有關係,因此,前端工程師對設計模式也是有需求的。
設計模式是對人類工程歷史總結,而不單單只是軟件工程。
現在大家談的前端工程化,如果脫離設計模式,只能算徒有其表,設計模式才是工程化的靈魂。當然,既然是經驗和歷史總結,有時候並不需要系統地進行學習,口口相傳也是可以的,但是單獨系統地講解設計模式,就是要將「公共知識」轉變為「共有知識」,戳破皇帝的新衣,讓大家真正能言之有物,交流通暢。
類型分類
可以將設計模式分為三種類型,分別為創建型,結構型,和行為型。
創建型模式主要解決對象創建什麼,由誰創建,何時創建的3w問題,對類的實例化進行了抽象,分離概念和實現,使得系統更加符合單一職責原則。
結構型模式描述如何將類或者對象組合在一起,形成更大的數據結構,因此也可以分為類結構型和對象結構型。
行為型模型對不同的對象劃分責任和算法的抽象,關注類和對象之間的相互作用,同樣也分為類和對象。
可以看到三種類型的模式正好解決了編程中的數據結構從哪裡來?如何組合?如何交流?的問題。
創建型模式
創建型模式一共有4個,分別為工廠(工廠,工廠方法,抽象工廠合併),建造者,原型,單例。
工廠模式
重要程度:⭐⭐⭐⭐⭐ 難度:⭐ 命名建議:xxxFactory,FactoryOfxxx
工廠模式簡而言之,就是要替代掉「new操作符」!
為什麼需要替代new操作符?
因為有時候創建實例時需要大量的準備工作,而將這些準備工作全部放在構造函數中是非常危險的行為,有必要將創建實例的邏輯和使用實例的邏輯分開,方便以後擴展。
舉個例子:
class People { constructor(des) { // 出現異步不能使用async await // 函數調用時可能還未完成初始化 get('someUrl').then(data => { this.name = data.name get('someUrl?name=' + this.name).then(data => { this.age = data.age }) }) // 非成員函數耦合性變大 this.des = handleDes(des) } }
而使用Typescript,配合工廠模式,實現如下:
// 還真別說,形式上好看的代碼,質量一般都比較高 class People { name: string = '' age: number = 0 des: string = '' constructor(name: string, age: number, des: string) { this.name = name this.age = age this.des = des } } async function peopleFactory(description:any){ const name = await get('someUrl') const age = await get('someUrl?name='+name) const des = handle(description) return new People(name,age,des) }
這樣的封裝,能清楚地分離對象的創建和使用。同時,如果之後的類的定義發生了改變,可以直接修改People,創建類的準備數據發生了改變,則修改工廠函數。
但是,選擇工廠模式的原因是因為構造函數足夠複雜或者對象的創建面臨巨大的不確定性,只需要傳入變量即可構造的情況下,用工廠函數實際上是得不償失的。
幾乎所有的設計模式都會帶來代碼可讀性下降的風險,因此需要找到代碼可讀性降低和可維護性,穩定性之間的平衡!
你也可以用函數根據參數返回相應的工廠函數,又或者用一個類集中管理工廠函數來處理複雜度。
建造者模式
重要程度:⭐⭐⭐⭐ 難度:⭐⭐ 命名建議:xxxBuilder
建造者模式用於直接構建複雜對象,比如上例中的構造函數參數,如果採用一個結構表示:
constructor(peopleConfig:any) { this.name = peopleConfig.name this.age = peopleConfig.age this.des = peopleConfig.des }
那麼有必要將這個人對象的構建單獨封裝起來:
class PeopleConfigBuilder{ name: string = '' age: number = 0 des: string = '' async buildName(){ this.name = await get('someUrl') } async buildAge(){ await get('someUrl?name='+this.name) } async buildDes(description: any){ this.des = handleDes(description) } } class People { name: string = '' age: number = 0 des: string = '' constructor(peopleConfig: PeopleCofigBuilder) { this.name = peopleConfig.name this.age = peopleConfig.age this.des = peopleConfig.des } } async function peopleFactory(description:any){ const builder = new PeopleConfigBuilder() builder.buildName() builder.buildAge() builder.buildDes() return new People(builder) }
當然,僅僅三個屬性的對象,遠遠沒有達到複雜對象的程度,因此,只有在對象十分複雜的時候,才需要應用到建造者模式。
原型模式
重要程度:⭐⭐ 難度:⭐ 命名建議:xxxPrototype
創建新對象時是基於一個對象的拷貝,而不是重新實例化一個類。
舉例說明,比如上例中的peopleConfig,其實peopleConfig應該是有固定模板的:
function peopleConfigPrototype (){ return { name: '', age: 0, des: '' } }
這樣每次返回的都是新的對象,也可以相當於是對象的拷貝,但是如果直接拷貝對象,應該怎麼寫呢?
const peopleConfigPrototype = { name: '', age: 0, des: '' } const peopleConfig = Object.create(peopleConfigPrototype) // 採用Object.create方法,當前對象將被複制到peopleConfig的__proto__上
還有另一種方式進行對象拷貝,但是會丟掉對象中的函數:
const peopleConfig = JSON.parse(JSON.stringfy(peopleConfigProtytype))
注意JSON操作會阻塞線程,導致性能急劇下降,一般不考慮這種方式。
單例模式
重要程度:⭐⭐⭐⭐⭐ 難度:⭐⭐ 命名建議:xxxSingle,xxxSingleton,xxxUnum
單例模式的目的是限制一個類只能被實例化一次,防止多次實例化。其中,根據類被實例化的時間,又被分為懶漢單例和餓漢單例。懶漢單例是指在第一次調用實例的時候實例化,餓漢單例是指在類加載的時候就實例化。
/* 懶漢單例 */ class PeopleSingle{ // 靜態成員instance static instance = null // 私有構造函數 private constructor(){ } public static getInstance(){ if(PeopleSingle.instance === null){ PeopleSingle.instance = new PeopleSingle() } return PeopleSingle.instance } } PeopleSingle.getInstance() /* 餓漢單例 */ class PeopleSingle{ static instance = new PeopleSingle() private constructor(){ } } PeopleSingle.instance
四種創建模式都有其使用場景,需要針對使用場景進行組合,才能寫出高質量的代碼。
結構型模式
結構型模式一共有7種:適配器,橋接,組合,裝飾,外觀,享元,代理
適配器模式
重要程度:⭐⭐⭐⭐ 難度:⭐⭐⭐ 命名建議:xxxAdapter,xxxWraper
想想你的轉接頭,實際上就是被適配對象(adaptee)上套上一層封裝,將其接口與目標對象(target)相匹配,所以適配器又叫wraper(包皮)。
比如,有一個目標類UsbC:
class UsbC{ slowCharge(){ console.log('slow charging') } superCharge(){ console.log('super charging') } }
有一個被適配目標MicroUsb:
class MicroUsb{ slowCharge(){ console.log('slow charging') } }
所以adapter是如此:
// 精髓在implements target上 class MicroToCAdapter implements UsbC{ microUsb: MicroUsb constructor(microUsb: MicroUsb){ this.microUsb = microUsb } slowCharge(){ this.microUsb.slowCharge() } superCharge(){ console.log('cannot super charge, slow charging') } } // 這樣就可以直接 new MicroTOCAdapter(new MicroUsb()).superCharge()
適配器模式對多個不同接口的匹配非常有效,實際情況中沒有必要完全使用類來封裝,一個函數也可以搞定。
橋接模式
重要程度:⭐⭐⭐⭐ 難度:⭐⭐⭐ 命名建議:xxxBridge,xxx(具體實現)
橋接模式的主要目的,是將抽象與實現解耦,使得二者可以獨立地進行變化,以應對不斷更細的需求。
其實通俗地來說,就是將所有概念想像成「靈魂——肉體」,凡是能用這個概念代入的,都可以用橋接模式重構。
比如汽車這個概念和顏色這個概念,可以將顏色作為汽車的成員變量,但是當顏色變得更加複雜時,比如漸變,模糊,圖案等屬性加入,不得不將其解耦,橋接模式就很重要了。
我們先定義抽象類Car和Color(Ts的抽象類功能對於實現之一模式非常重要):
abstract class Color { color: string abstract draw(): void } abstract class Car { color: Color abstract setColor(color: Color): void }
再定義其實例:
class Red extends Color { constructor() { super() } draw() { this.color = 'red' } } class Van extends Car { constructor() { super() } setColor(color: Color) { this.color = color } }
抽象類和實現是解耦的,這時候我們如果要利用所有的類,就需要一個橋接類:
class PaintingVanBridge { van: Car red: Color constructor() { this.red = new Red() this.red.draw() this.van = new Van() this.van.setColor(this.red) } }
橋接模式會增加大量代碼,所以一定要在使用之前對功能模塊有一個恰當的評估!
裝飾模式
重要程度:⭐⭐⭐⭐⭐ 難度:⭐ 命名建議:xxxDecorator,xxx(具體實現)
裝飾模式是在現有類或對象的基礎上,添加一些功能,使得類和對象具有新的表現。
還是之前的Car和Color的問題,可以直接繼承Car,添加顏色,這是一個裝飾模式:
class Car { name: string constructor(name: string) { this.name = name } } class Benz extends Car { color: string constructor(name: string, color: string) { super(name) this.color = color } }
但是採用繼承的方式是靜態的,而且會導致在繼承復用的過程中耦合,比如Car2繼承Car,在創建新的子類時錯把Car2作為父類,結果就很容易出錯了。
為了解決這個問題,可以採用Ts的裝飾器特性:
function colorDecorator<T extends { new(...args: any[]): {} }>(color: string) { return function (constructor: T) { return class extends constructor { name = 'shit' color = color } } } @colorDecorator<Car>('red') class Car { name: string constructor(name: string) { this.name = name } }
裝飾器會攔截Car的構造函數,生成一個繼承自Car的新的類,這樣更加靈活(但是注意這個過程只發生在構造函數階段)。
外觀模式
重要程度:⭐⭐⭐⭐⭐ 難度:⭐ 命名建議:xxx(具體實現)
簡單一句話總結:「封裝複雜,接口簡單」,為所有的子系統提供一致的接口,比如輪胎,方向盤和車。
class Tyre{ name: string constructor(name: string){ this.name = name } } class Steering{ turnRight(){} turnLeft(){} } interface CarConfig{ tyreName: string ifTurnRight: boolean } class Car{ tyre:Tyre steering:Steering constructor(carConfig: CarConfig){ this.tyre = new Tyre(carConfig.name) this.steering = new Steering() if(carConfig.ifTurnRight){ this.steering.turnRight } } }
可以活用Typescript的接口功能實現這一模式。
享元模式
重要程度:⭐ 難度:⭐⭐ 命名建議:xxx(具體實現)
享元模式避免重新創建對象,其實只要有緩存對象的意思,並且共用一個對象實例,就是享元模式。
比如需要對一個Car的實例進行展示(可以搭配工廠模式):
class Car{ name: string color: string changeColor(color: string){ this.color = color } changeName(name: string){ this.name = name } } class CarFactory{ static car: Car static getCar():Car{ if(CarFactory.car === null){ CarFactory.car = new Car() } return CarFactory.car } } CarFactory.getCar().changeColor('red')
注意,由於是使用的同一個引用,因此會存在修改的問題。
代理模式
重要程度:⭐⭐⭐⭐ 難度:⭐ 命名建議:xxxProxy
對接口進行一定程度的隱藏,用於封裝複雜類。
比如Car有很多屬性,我們只需要一個簡單的版本:
class Car{ a: number = 1 b: number = 2 c: number = 3 d: number = 4 name: string = 'name' test(){ console.log('this is test') } } class CarProxy{ private car: Car name: number constructor(){ if(this.car === null){ this.car = new Car } this.name = this.car.name } test(){ this.car.test() } }
行為型模式
行為型模式一共有5種:命令,中介者,觀察者,狀態,策略
命令模式
重要程度:⭐⭐⭐⭐⭐ 難度:⭐⭐ 命名建議:xxxCommand
命令模式的主要目的是讓請求者和響應者解耦,並集中管理。
比如大家常用的請求,其實可以這樣封裝:
function requestCommand(command: string){ let method = 'get' let queryString = '' let data = null let url = '' const commandArr = command.split(' ') url = commandArr.find(el=>el.indexOf('http')) const methods = commandArr.filter(el=>el[0]==='-') methods[0].replace('-','') method = methods[0] const query = commandArr.filter(el=>el.indexOf('=')) if(query.length > 0){ queryString = '?' query.forEach(el=>{ queryString += el + '&' }) } const dataQuery = commandArr.filter(el=>el[0]==='{') // 對json的判斷還不夠細緻 data = JSON.parse(dataQuery) if(method === 'get' || method === 'delete'){ return axios[method](url+query) } return axios[method](url+query,data) } requestCommand('--get https://www.baidu.com name=1 test=2') requestCommand('--post https://www.baidu.com {"name"=1,"test":2}')
注意命令模式需要提供詳盡的文檔,並且儘可能集中管理。
中介模式
重要程度:⭐⭐⭐⭐⭐ 難度:⭐⭐⭐⭐ 命名建議:xxxCotroller,xxxMiddleWare,xxx(具體實現)
全權負責兩個模塊之間的通訊,比如MVC,MVVM就是非常典型的中介模式。
中介模式,橋接模式,代理模式的區別是:
代理模式一對一,只能代理特定類和對象,是對其的擴展或是約束。
橋接模式一對多,是對類或對象成員或屬性的擴展。
中介模式多對多,全權承包所有兩個概念間的關係。
比如4s店,車,和買家之間的關係:
class Car{ name: string = 'Benz' } class Buyer{ name: string = 'Sam' buy(car: Car){ console.log(`${this.name}購買了${car.name}`) } } class FourSShop{ constructor(){ const benz = new Car() const sam = new Buyer() sam.buy(benz) } }
可以想像中介模式是一個立體的概念,可以理解成是兩個概念發生關係的地點。
觀察者模式
重要程度:⭐⭐⭐⭐⭐ 難度:⭐⭐⭐ 命名建議:xxxObserver,xxxEventHandler
觀察者模式的目的是為了「檢測變更」,既然要檢測變更,自然需要記錄之前的信息:
class Observer{ states: string[] = [] update(state: string){ this.states.push(state) } } class People{ state: string = '' observer: Observer // 可以用getter setter優化 setState(newState: string){ if(this.state !== newState){ this.state = newState this.notify(this.state) } } notify(state: string){ if(this.observer !== null){ this.observer.update(state) } } setObserver(observer: Observer){ this.observer = observer } } const observer = new Observer() const people = new People().serObserver(observer) people.setState('shit') console.log(observer.state)
可以把觀察者模式看成是「記錄事件」,這對於理解觀察者模式和狀態模式區別很有幫助。
實際上前端很多事件處理,就是基於觀察者模式的,在上例中的update中的state,就是事件名稱,js的事件循環會輪流處理states的狀態變化。
狀態模式
重要程度:⭐⭐⭐⭐⭐ 難度:⭐⭐⭐ 命名建議:xxxState
與觀察者模式相對,表示的是「記錄狀態」,只要狀態變更,表現即不同,這是設計數據驅動的基礎。
class State{ tmp: string set store(state: string){ if(this.tmp !== state){ // do something this.tmp = state } } get store(): string{ return this.tmp } } class People{ state: State constructor(state: State){ this.state = state } } const state = new State() const people = new People(state) state.store = 1 console.log(people.state.store)
當然,如果一個數據接口既能記錄事件,又能記錄狀態,可以么?
這就是傳說中的響應式數據流,也就是大家平時使用的ReactiveX。
策略模式
重要程度:⭐⭐⭐ 難度:⭐⭐⭐⭐ 命名建議:xxxStratege
策略模式表示動態地修改行為,而行為有時候是一系列方法和對象的組合,與命令模式的區別也在這裡。
比如從中國到羅馬,可以如此封裝:
class Location{ position: string constructor(poosition: string){ this.position = position } } class Stratege{ locations: Location [] = [] constructor(...locations){ this.locations = locations console.log('路線經過了') this.locations.forEach(el=>{ console.log(el.position+',') }) } } class Move{ start: Location end: Location stratege: Stratege constructor(){ this.start = new Location('1 1') this.end = new Location('0 0') const sea = new Location('0 1') const land = new Location('1 0') this.stratege = new Stratege(this.start,sea,this.end) } }
設計模式根植於面向對象思想,也就是任何實現都要區分概念(類)和實例(對象),也就是要分清楚白馬和馬,這樣才能竟可能減輕擴展和團隊協作的負擔。
但是任何東西有利就有弊,揚長避短才是我們應該在意的方向。