作者簡介 joey 螞蟻金服·資料體驗技術團隊
繼前文Typescript玩轉設計模式 之 結構型模式(上)之後,本週帶來的是系列文章之三,講解的是4種結構性模式:
- 介面卡
- 橋接
- 組合
- 裝飾
結構性模式分為7種,本文先講解其中四種,剩餘3種下一篇文章再進行討論~
介面卡(Adapter)
定義
將一個類的介面轉換成客戶希望的另外一個介面。Adapter模式使得原本由於介面不相容而不能一起工作的那些類可以一起工作。
結構
介面卡模式由以下角色構成:
- Target(目標抽象類):目標抽象類定義客戶所需介面,可以是一個抽象類或介面,也可以是具體類。
- Adapter(介面卡類):介面卡可以呼叫另一個介面,作為一個轉換器,對Adaptee和Target進行適配,介面卡類是介面卡模式的核心,在物件介面卡中,它通過繼承Target並關聯一個Adaptee物件使二者產生聯絡。
- Adaptee(適配者類):適配者即被適配的角色,它定義了一個已經存在的介面,這個介面需要適配,適配者類一般是一個具體類,包含了客戶希望使用的業務方法,在某些情況下可能沒有適配者類的原始碼
物件介面卡
類介面卡
示例
電源插座是220V的,但手機充電時只需要5V,因此我們需要一個介面卡讓手機能在220V插座上充電。
// 介面卡有2種實現模式,類模式和物件模式
// 目標介面
interface Voltage5vInterface {
connect5v(): void;
}
// 被適配類
class Voltage220v {
connect220v() {
console.log('接通220V電源,');
}
}
// 客戶類,儲存對介面卡物件的引用關係,通過訪問介面卡物件來間接使用被適配物件
// 這裡,手機充電時只需要知道介面卡物件的5V介面就能呼叫被適配的220V插座來充電了
class Phone {
private voltage5v: Voltage5vInterface;
constructor(voltage5v: Voltage5vInterface) {
this.voltage5v = voltage5v;
}
charge(): void {
this.voltage5v.connect5v();
console.log('已經接通電源,手機開始充電');
}
}
// 類介面卡
// Voltage220v是被適配的類,介面跟最終要求不一致
// Voltage5vInterface包含想要提供的介面
// 因此"繼承"被適配的類,"實現"想要支援的介面
class ClassPowerAdapter extends Voltage220v implements Voltage5vInterface {
connect5v(): void {
this.connect220v();
console.log('將220V電源轉化為5v電源,');
}
}
function classAdapterDemo() {
const adapter = new ClassPowerAdapter();
const phone = new Phone(adapter);
phone.charge();
}
classAdapterDemo();
// 物件介面卡
// 介面卡中持有被適配類的物件的引用
class InstancePowerAdapter implements Voltage5vInterface {
private voltage220v: Voltage220v;
constructor(voltage220v: Voltage220v) {
this.voltage220v = voltage220v;
}
connect5v(): void {
this.voltage220v.connect220v();
console.log('將220V電源轉化為5v電源,');
}
}
function instanceAdapterDemo() {
const voltage220v = new Voltage220v();
const adapter = new InstancePowerAdapter(voltage220v);
const phone = new Phone(adapter);
phone.charge();
}
instanceAdapterDemo();
複製程式碼
同一個介面適配不同的類
電腦有個USB介面,可以插入華為手機或iphone的資料線
// 電腦的USB介面
interface ComputerInterface {
usb(): void;
}
// 華為手機,有自己的資料介面
class HuaweiPhone {
huaweiInterface(): void {
console.log('華為手機的資料介面');
}
}
// iphone,有自己的資料介面
class Iphone {
iphoneInterface(): void {
console.log('蘋果手機的資料介面');
}
}
// 華為手機資料線介面卡
class HuaweiDataWireAdapter extends HuaweiPhone implements ComputerInterface {
usb(): void {
console.log('使用華為資料線連線');
super.huaweiInterface();
}
}
// iphone手機資料線介面卡
class IphoneDataWireAdapter extends Iphone implements ComputerInterface {
usb(): void {
console.log('使用蘋果資料線連線');
super.iphoneInterface();
}
}
function commonAdapterDemo() {
const computer1 = new HuaweiDataWireAdapter();
computer1.usb();
const computer2 = new IphoneDataWireAdapter();
computer2.usb();
}
commonAdapterDemo();
複製程式碼
適用場景
- 你想使用一個已經存在的類,而他的介面不符合你的需求;
- 你想建立一個可以複用的類,該類可以與其他不相關的類或不可預見的類(即那些介面可能不一定相容的類)協同工作;
- (僅對於物件介面卡)你想使用一些已經存在的子類,但是不可能對每一個都進行子類化以匹配他們的介面。物件介面卡可以適配他的父類介面;
優點
- 將目標類和被適配類解耦,通過引入一個介面卡類來重用現有的被適配類,而無須修改原有程式碼;
- 增加了類的透明性和複用性,將具體的實現封裝在被適配類中,對於客戶端類來說是透明的,而且提高了被適配類的複用性;
- 類介面卡:由於介面卡類是被適配類的子類,因此可以在介面卡類中置換一些被適配類的方法,使得介面卡的靈活性更強;
- 物件介面卡:一個物件介面卡可以把多個不同的被適配類適配到同一個目標,也就是說,同一個介面卡可以把被適配類和它的子類都適配到目標介面;
缺點
- 類介面卡:單繼承機制使得同時只能適配一個被適配類;
- 物件介面卡:與類介面卡模式相比,要想置換被適配類的方法就不容易;
相關模式
- 橋接(Bridge)模式的結構與物件介面卡類似,但橋接模式的出發點不同:橋接目的是將介面部分和實現部分分離,從而使他們可以較為容易也相對獨立的加以改變。而介面卡則意味著改變一個已有物件的介面。
- 裝飾器(Decorator)模式增強了其他物件的功能而同時又不改變他的介面。因此裝飾器對應用程式的透明性比介面卡要好。裝飾器支援遞迴組合,而純粹使用介面卡是不可能實現這一點的。
- 代理(Proxy)模式在不改變他的介面的條件下,為另一個物件定義了一個代理。
橋接(Bridge)
定義
將抽象部分與他的實現部分分離,使他們都可以獨立地變化。
結構
橋接模式包含以下角色:
- Abstraction(抽象類):用於定義抽象類的介面,它一般是抽象類而不是介面,其中定義了一個Implementor(實現類介面)型別的物件並可以維護該物件,它與Implementor之間具有關聯關係,它既可以包含抽象業務方法,也可以包含具體業務方法。
- RefinedAbstraction(擴充抽象類):擴充由Abstraction定義的介面,通常情況下它不再是抽象類而是具體類,它實現了在Abstraction中宣告的抽象業務方法,在RefinedAbstraction中可以呼叫在Implementor中定義的業務方法。
- Implementor(實現類介面):定義實現類的介面,這個介面不一定要與Abstraction的介面完全一致,事實上這兩個介面可以完全不同,一般而言,Implementor介面僅提供基本操作,而Abstraction定義的介面可能會做更多更復雜的操作。Implementor介面對這些基本操作進行了宣告,而具體實現交給其子類。通過關聯關係,在Abstraction中不僅擁有自己的方法,還可以呼叫到Implementor中定義的方法,使用關聯關係來替代繼承關係。
- ConcreteImplementor(具體實現類):具體實現Implementor介面,在不同的ConcreteImplementor中提供基本操作的不同實現,在程式執行時,ConcreteImplementor物件將替換其父類物件,提供給抽象類具體的業務操作方法。
示例
// 汽車是一個維度,有多種不同的車型
abstract class AbstractCar {
abstract run(): void;
}
// 路是一個維度,有多種不同的路
abstract class AbstractRoad {
car: AbstractCar;
abstract snapshot(): void;
}
/**
* 汽車和路兩個維度
* 橋接就是一個維度的類中引用了另一個維度的物件,但只關心介面不關心是哪個具體的類
* 從而實現兩個維度獨立變化
*/
class SpeedRoad extends AbstractRoad {
constructor(car: AbstractCar) {
super();
this.car = car;
}
snapshot(): void {
console.log('在高速公路上');
this.car.run();
}
}
class Street extends AbstractRoad {
constructor(car: AbstractCar) {
super();
this.car = car;
}
snapshot(): void {
console.log('在市區街道上');
this.car.run();
}
}
class Car extends AbstractCar {
run(): void {
console.log('開著小汽車');
}
}
class Bus extends AbstractCar {
run(): void {
console.log('開著公共汽車');
}
}
function carRunOnRoadDemo(): void {
// 在高速公路上,開著小汽車
const car = new Car();
const speedRoad = new SpeedRoad(car);
speedRoad.snapshot();
// 在市區街道上,開著公共汽車
const bus = new Bus();
const street = new Street(bus);
street.snapshot();
}
carRunOnRoadDemo();
/**
* 人,汽車和路三個維度
*/
abstract class Person {
road: AbstractRoad;
abstract see(): void;
}
class Man extends Person {
constructor(road: AbstractRoad) {
super();
this.road = road;
}
see(): void {
console.log('男人看到');
this.road.snapshot();
}
}
class Woman extends Person {
constructor(road: AbstractRoad) {
super();
this.road = road;
}
see(): void {
console.log('女人看到');
this.road.snapshot();
}
}
function personSeeCarOnRoadDemo() {
// 男人看到 在市區街道上 開著小汽車
const car = new Car();
const street = new Street(car);
const man = new Man(street);
man.see();
}
personSeeCarOnRoadDemo();
複製程式碼
適用場景
- 如果一個系統需要在抽象化和具體化之間增加更多的靈活性,避免在兩個層次之間建立靜態的繼承關係,通過橋接模式可以使它們在抽象層建立一個關聯關係;
- “抽象部分”和“實現部分”可以以繼承的方式獨立擴充套件而互不影響,在程式執行時可以動態將一個抽象化子類的物件和一個實現化子類的物件進行組合,即系統需要對抽象化角色和實現化角色進行動態耦合;
- 一個類存在兩個(或多個)獨立變化的維度,且這兩個(或多個)維度都需要獨立進行擴充套件;
優點
- 分離介面及其實現部分;
- 提高可擴充性;
- 實現細節對客戶透明;
缺點
- 橋接模式的使用會增加系統的理解與設計難度,由於關聯關係建立在抽象層,要求開發者一開始就針對抽象層進行設計與程式設計;
- 橋接模式要求正確識別出系統中兩個獨立變化的維度,因此其使用範圍具有一定的侷限性,如何正確識別兩個獨立維度也需要一定的經驗積累;
相關模式
- 抽象工廠模式可以用來建立和配置一個特定的橋接模式。即一個維度的產品都由抽象工廠生成。
- 和介面卡模式的區別:介面卡模式用來幫助無關的類協同工作,他通常在系統設計完成後才會被使用;橋接模式是在系統開始時就被使用,他使得抽象介面和實現部分可以獨立進行改變。
組合(Composite)
意圖
將物件組合成樹形結構以表示“部分-整體”的層次結構。Composite使得使用者對單個物件和組合物件的使用具有一致性。
結構
組合模式包含以下角色:
- Component(抽象構件):它可以是介面或抽象類,為葉子構件和容器構件物件宣告介面,在該角色中可以包含所有子類共有行為的宣告和實現。在抽象構件中定義了訪問及管理它的子構件的方法,如增加子構件、刪除子構件、獲取子構件等。
- Leaf(葉子構件):它在組合結構中表示葉子節點物件,葉子節點沒有子節點,它實現了在抽象構件中定義的行為。對於那些訪問及管理子構件的方法,可以通過異常等方式進行處理。
- Composite(容器構件):它在組合結構中表示容器節點物件,容器節點包含子節點,其子節點可以是葉子節點,也可以是容器節點,它提供一個集合用於儲存子節點,實現了在抽象構件中定義的行為,包括那些訪問及管理子構件的方法,在其業務方法中可以遞迴呼叫其子節點的業務方法。
示例
// 抽象類 人,提供戰鬥介面
abstract class Human {
name: string;
constructor(name: string) {
this.name = name;
}
abstract fight(): void;
}
// 士兵類,戰鬥操作是自己加入戰鬥
class Soldier extends Human {
fight() {
console.log(`${this.name} 準備加入戰鬥`);
}
}
// 指揮官類,戰鬥操作是遞迴召集自己的下屬,集合部隊
class Commander extends Human {
soldiers: Set<Soldier>;
constructor(name: string) {
super(name);
this.soldiers = new Set<Soldier>();
}
add(soldier: Soldier) {
this.soldiers.add(soldier);
}
remove(soldier: Soldier) {
this.soldiers.delete(soldier);
}
fight() {
console.log(`${this.name} 開始召集屬下`);
this.soldiers.forEach(soldier => soldier.fight());
console.log(`${this.name} 部隊集結完畢`);
}
}
// 在使用組合模式時,所有物件都有'fight'方法,因此不需要關心物件是士兵還是指揮官,即不需要關心是單個物件還是組合物件
function battleDemo() {
const soldier1 = new Soldier('soldier1');
const soldier2 = new Soldier('soldier2');
const soldier3 = new Soldier('soldier3');
const soldier4 = new Soldier('soldier4');
const subCommander1 = new Commander('subCommander1');
subCommander1.add(soldier1);
subCommander1.add(soldier2);
const subCommander2 = new Commander('subCommander2');
subCommander2.add(soldier3);
subCommander2.add(soldier4);
const chiefCommander = new Commander('chiefCommander');
chiefCommander.add(subCommander1);
chiefCommander.add(subCommander2);
chiefCommander.fight();
}
battleDemo();
複製程式碼
適用場景
- 想表示物件的部分-整體層次結構;
- 希望使用者忽略組合物件與單個物件的不同,使用者將統一地使用組合結構中的所有物件;
優點
- 定義了包含基本物件和組合物件的類層次結構。基本物件可以被組合成更復雜的組合物件,而這個組合物件又可以被組合,不斷遞迴下去。客戶程式碼中,任何用到基本物件的地方都可以使用組合物件;
- 簡化客戶程式碼。客戶不需要關心處理的是一個葉節點還是枝節點;
- 更容易增加新型別的元件;
缺點
- 在增加新構件時很難對容器中的構件型別進行限制。有時候我們希望一個容器中只能有某些特定型別的物件,例如在某個資料夾中只能包含文字檔案,使用組合模式時,不能依賴型別系統來施加這些約束,因為它們都來自於相同的抽象層,在這種情況下,必須通過在執行時進行型別檢查來實現,這個實現過程較為複雜。
相關模式
- 部件-父部件連線用於職責鏈模式。
- 裝飾器模式經常與組合模式一起使用。當裝飾和組合一起使用時,他們通常有一個公共的父類。
- 享元模式讓你共享元件,但不能再引用他們的父部件。
- 迭代器可用來遍歷組合。
- 訪問者將本來應該分佈在枝類和葉子類中的操作和行為區域性化。
裝飾(Decorator)
意圖
動態地給一個物件新增一些額外的職責。就增加功能來說,Decorator模式相比生成子類更為靈活。
結構
裝飾模式包含以下角色:
- Component(抽象構件):它是具體構件和抽象裝飾類的共同父類,宣告瞭在具體構件中實現的業務方法,它的引入可以使客戶端以一致的方式處理未被裝飾的物件以及裝飾之後的物件,實現客戶端的透明操作。
- ConcreteComponent(具體構件):它是抽象構件類的子類,用於定義具體的構件物件,實現了在抽象構件中宣告的方法,裝飾器可以給它增加額外的職責(方法)。
- Decorator(抽象裝飾類):它也是抽象構件類的子類,用於給具體構件增加職責,但是具體職責在其子類中實現。它維護一個指向抽象構件物件的引用,通過該引用可以呼叫裝飾之前構件物件的方法,並通過其子類擴充套件該方法,以達到裝飾的目的。
- ConcreteDecorator(具體裝飾類):它是抽象裝飾類的子類,負責向構件新增新的職責。每一個具體裝飾類都定義了一些新的行為,它可以呼叫在抽象裝飾類中定義的方法,並可以增加新的方法用以擴充物件的行為。
示例
// 抽象構件——視覺化元件
class VisualComponent {
draw(): void {
console.log('繪製一個元件');
}
}
// 裝飾器基類,裝飾視覺化元件
class Decorator extends VisualComponent {
protected component: VisualComponent;
constructor(component: VisualComponent) {
super();
this.component = component;
}
draw(): void {
this.component.draw();
}
}
// 帶邊框的裝飾器
class BorderDecorator extends Decorator {
protected width: number;
constructor(component: VisualComponent, borderWidth: number) {
super(component);
this.width = borderWidth;
}
private drawBorder(): void {
console.log(`繪製寬度為${this.width}的邊框`);
}
draw() {
this.drawBorder();
this.component.draw();
}
}
// 帶滾動條的裝飾器
class ScrollDecorator extends Decorator {
private drawScrollBar(): void {
console.log('繪製滾動欄');
}
draw(): void {
this.drawScrollBar();
this.component.draw();
}
}
// 繪製一個帶滾動條和邊框的元件
function decoratorDemo() {
const component = new VisualComponent();
const finalComponent = new BorderDecorator(new ScrollDecorator(component), 1);
finalComponent.draw();
}
decoratorDemo();
複製程式碼
適用場景
- 在不影響其他物件的情況下,以動態、透明的方式給單個物件新增職責;
- 處理那些可以撤銷的職責;
- 當不能採用生成子類的方法進行擴充時。一般情況是,可能有大量獨立的擴充套件,為支援每一種組合將產生大量的子類,使得子類的書目呈爆炸性增長。另一種情況可能是因為類定義被隱藏,或類定義不能用於生成子類;
優點
- 對於擴充套件一個物件的功能,裝飾模式比繼承更加靈活性,不會導致類的個數急劇增加;
- 可以通過一種動態的方式來擴充套件一個物件的功能;
- 可以對一個物件進行多次裝飾,通過使用不同的具體裝飾類以及這些裝飾類的排列組合,可以創造出很多不同行為的組合,得到功能更為強大的物件;
- 具體構件類與具體裝飾類可以獨立變化,使用者可以根據需要增加新的具體構件類和具體裝飾類,原有類庫程式碼無須改變,符合“開閉原則”;
缺點
- 裝飾模式提供了一種比繼承更加靈活機動的解決方案,但同時也意味著比繼承更加易於出錯,排錯也很困難,對於多次裝飾的物件,除錯時尋找錯誤可能需要逐級排查,較為繁瑣。
- 使用裝飾時不應該依賴物件標識,
typeof
不能指向Component類
相關模式
- 介面卡模式:裝飾器模式不同於介面卡模式,因為裝飾僅改變物件的職責而不改變他的介面。而介面卡將給物件一個全新的介面。
- 組合模式:可以將裝飾器視為一個退化的,僅有一個元件的組合。然而,裝飾僅給物件新增一些額外的職責——他的目的不在於物件聚集。
- 策略模式:裝飾器你可以改變物件的外表,而策略模式使得你可以改變物件的核心,這是改變物件的兩種途徑。裝飾器是由外而內,策略是由內而外。當Component類很龐大時,使用裝飾器代價太高,策略模式相對更好一些。在策略模式中,元件將他的一些行為轉發給一個獨立的策略物件,我們可以替換策略物件,從而改變或擴充元件的功能。裝飾器模式,Component不需要知道外部的裝飾,而策略模式Component需要知道進行了哪些擴充。
參考文件
本文介紹了前4種結構型模式,對後續模式感興趣的同學可以關注專欄或者傳送簡歷至'chaofeng.lcf####alibaba-inc.com'.replace('####', '@'),歡迎有志之士加入~