Typescript玩轉設計模式 之 結構型模式(上)

做者簡介 joey 螞蟻金服·數據體驗技術團隊html

繼前文Typescript玩轉設計模式 之 結構型模式(上)以後,本週帶來的是系列文章之三,講解的是4種結構性模式:java

  • 適配器
  • 橋接
  • 組合
  • 裝飾

結構性模式分爲7種,本文先講解其中四種,剩餘3種下一篇文章再進行討論~git

適配器(Adapter)

定義

將一個類的接口轉換成客戶但願的另一個接口。Adapter模式使得本來因爲接口不兼容而不能一塊兒工做的那些類能夠一塊兒工做。github

結構

適配器模式由如下角色構成:編程

  • 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();
複製代碼

同一個接口適配不一樣的類bash

電腦有個USB接口,能夠插入華爲手機或iphone的數據線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)

定義

將抽象部分與他的實現部分分離,使他們均可以獨立地變化。post

結構

橋接模式包含如下角色:this

  • 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('####', '@'),歡迎有志之士加入~

原文地址:github.com/ProtoTeam/b…

相關文章
相關標籤/搜索