看看用TypeScript怎樣實現常見的設計模式,順便復習一下。
學模式最重要的不是記UML,而是知道什么模式可以解決什么樣的問題,在做項目時碰到問題可以想到用哪個模式可以解決,UML忘了可以查,思想記住就好。
這里盡量用原創的,實際中能碰到的例子來說明模式的特點和用處。
裝飾模式 Decorator
特點:在不改變接口的情況下,裝飾器通過組合方式引用對象,並由此在保持對象原有功能的基礎上給對象加上新功能。
用處:當需要不影響現有類並增加新的功能時,可以考慮裝飾模式,它可以動態透明的給對象增加功能。
注意:與繼承的優劣。
下面用TypeScript簡單實現一下裝飾模式:
現在有一輛小轎車,加速到100km/h需要10秒:
interface Movable{
accelerate();
}
class Car implements Movable{
accelerate(){
console.log('加速到100km/h需要10秒');
}
}
現在想改裝下,提高加速度,加個渦輪增壓器。
class TurboCharger{
use(){
console.log('使用渦輪增壓');
}
}
class RefittedCar implements Movable{
construct(private car: Car){
}
turboCharger = new TurboCharger();
accelerate(){
this.car.accelerate();
this.turboCharger.use();
console.log('加速到100km/h需要5秒');
}
}
let refitterCar: Movable = new RefittedCar(new Car());
refitterCar.accelerate();
//輸出:
加速到100km/h需要10秒
使用渦輪增壓
加速到100km/h需要5秒
這樣改裝后的車用的還是原來的接口,但新對象卻可以在保持原對象的功能基礎上添加新的加速功能。
代碼實現的特點主要還是在於使用同樣接口,並在新對象里有對原對象的引用,這樣才能使用原對象的功能。
上面的功能其實通過繼承也很容易做到:
class TurboCharger{
use(){
console.log('使用渦輪增壓');
}
}
class RefittedCar extends Car{
turboCharger = new TurboCharger();
accelerate(){
super.accelerate();
this.turboCharger.use();
console.log('加速到100km/h需要5秒');
}
}
let refitterCar: Movable = new RefittedCar();
refitterCar.accelerate();
//輸出同樣是:
加速到100km/h需要10秒
使用渦輪增壓
加速到100km/h需要5秒
用哪個好呢?對比看下各自的優缺點:(裝飾器用的是組合)
繼承的優點是 直觀,容易理解,缺點是繼承鏈太長的話將很難維護,並且耦合度較高,相對死板,不夠靈活。
組合的優點是 靈活,但如果裝飾器本身也比較多且復雜時,代碼的復雜度也會增加不少。
就我個人來說在這種場景下還是用組合比較舒服,不是很喜歡在已經使用的類上繼承出子類。
接上面的例子,有個皮卡也要增加加速功能,繼承的話又得再實現一個皮卡子類,而用裝飾模式的話使用時傳進來就好了,本身不需要做任何更改。
當然,如果是從新設計,改下接口的話可能會選繼承,畢竟直觀很多,以后的需求有改動時再重構就好了,代碼保持簡單,怎么簡單怎么來,所謂組合優於繼承也要根據實際情況來定奪。
代理模式 Proxy
特點:在對象前面加了一層,外部需要通過這層代理來操作原對象,代理可以加一些控制來過濾或簡化操作。
用處:當對象不希望被外部直接訪問時可以考慮代理模式,典型的有遠程代理、保護代理、透明代理、虛擬代理等。
注意:與外觀、裝飾器模式的區別。
遠程代理:在Visual Studio里很容易用到,Web Reference,直接把web service當本地引用使用。
保護代理:通常用來對一個對象做權限控制。
虛擬代理:做web頁面時對圖像經常使用虛擬代理,看不到的圖像先用個統一的圖像替代,頁面滑到哪就加載到哪,省資源。
下面用TypeScript簡單實現一下遠程代理模式:
數據接口:
interface DataService{
getData(): string | Promise<string>;
}
在server端的遠程服務:
class RemoteService implements DataService{
getData(): string{
return 'get remote data';
}
}
假設一個Reqeuster類,可以取server端數據:
class Requester{
Request(): Promise<string>{
return Promise.resolve(new RemoteService().getData()); //這里本來應該從網絡取,現在只是演示一下
}
}
本地代理:
class DataProxy implements DataService{
async getData(): string{
return await new Requester().Request();
}
}
function isPromise(p: string | Promise<string>): p is Promise<string> { //用來判斷是否是promise
return (<Promise<string>>p).then !== undefined;
}
let dataService: DataService = new DataProxy();
let data = dataService.getData();
if(isPromise(data)){
data.then(o=>console.log(o));
} else {
console.log(data);
}
//輸出
get remote data
DataProxy和遠程的RemoteService使用同樣的接口,client像調用本地功能一樣通過DataProxy使用遠程功能。
代理模式與外觀模式的差別在於:
代理和被代理使用同一抽象,並且代理着重於訪問控制。
外觀則着重於簡化原本復雜的操作,並在此基礎上提取新抽象。
代理模式與裝飾器模式的差別在於:
代理的目的一般不是為了增加新功能而在於訪問控制,同時代理通常是自己來創建被代理對象。
裝飾器則着重於增加新功能,且被裝飾對象通常是作為引用傳給裝飾器的。