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

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

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

  • 外觀
  • 享元
  • 代理

Facade(外觀)

定義

爲子系統中的一組接口提供一個一致的界面,Facade模式定義一個高層接口,這個接口使得這個子系統更加容易使用。git

結構

外觀模式包含如下角色:程序員

  • Facade(外觀角色):在客戶端能夠調用它的方法,在外觀角色中能夠知道相關的(一個或者多個)子系統的功能和責任;在正常狀況下,它將全部從客戶端發來的請求委派到相應的子系統去,傳遞給相應的子系統對象處理。
  • SubSystem(子系統角色):在軟件系統中能夠有一個或者多個子系統角色,每個子系統能夠不是一個單獨的類,而是一個類的集合,它實現子系統的功能;每個子系統均可以被客戶端直接調用,或者被外觀角色調用,它處理由外觀類傳過來的請求;子系統並不知道外觀的存在,對於子系統而言,外觀角色僅僅是另一個客戶端而已。

示例

案例:領導提出要實現一個產品功能,但又不想了解其中的細節。github

// 主意
class Idea {};
// 需求
class Requirement {};
// 開發包
class Development {};
// 發佈包
class Release {};

// 產品經理
class PD {
  analyze(idea: Idea) {
    console.log('PD 開始需求');
    return new Requirement();
  }
}

// 開發者
class Developer {
  develop(requirement: Requirement) {
    console.log('程序員開始開發');
    return new Development();
  }
}

// 測試者
class Tester {
  test(develop: Development) {
    return new Release();
  }
}

// 外觀方法,領導不須要關注具體的開發流程,只要說出本身的想法便可
// 而不用外觀方法的話,也能夠訪問到子系統,只是須要瞭解其中的細節
function addNewFunction(idea: Idea) {
  const pd = new PD();
  const developer = new Developer();
  const tester = new Tester();
  const requirement = pd.analyze(idea);
  const development = developer.develop(requirement);
  const release = tester.test(development);
  console.log('發佈');
}

// 領導
class Leader {
  haveAGoodIdea() {
    const idea = new Idea();
    addNewFunction(idea);
  }
}

function facadeDemo() {
  const leader = new Leader();
  leader.haveAGoodIdea();
}
facadeDemo();
複製代碼

適用場景

  • 當你要爲一個複雜子系統提供一個簡單接口時。子系統每每因爲不斷演化而變得愈來愈複雜。大多數模式使用時都會產生更多更小的類。這使得子系統更具可重用性,也更容易對子系統進行定製,但這也給那些不須要定製子系統的用戶帶來一些使用上的困難。外觀能夠提供一個簡單的默認接口,這一接口對於大多數用戶來講已經足夠,而那些須要更多的可定製性的用戶能夠越過外觀層。
  • 客戶程序與抽象類的實現部分之間存在着很大的依賴性。外觀模式分離子系統,提升子系統的獨立性和可移植性。
  • 當你須要構建一個層次結構的子系統時,使用外觀模式定義子系統的入口點。讓子系統間經過外觀進行通信,簡化互相之間的依賴關係。

優勢

  • 對客戶程序屏蔽了子系統組件。經過引入外觀模式,客戶端代碼將變得很簡單,與之關聯的對象也不多;
  • 實現了子系統和客戶程序的鬆耦合關係;
  • 一個子系統的修改對其餘子系統沒有任何影響,並且子系統內部變化也不會影響到外觀對象;

缺點

  • 不能很好地限制客戶端直接使用子系統類,若是對客戶端訪問子系統類作太多的限制則減小了可變性和靈活性;
  • 若是設計不當,增長新的子系統可能須要修改外觀類的源代碼,違背了開閉原則;

相關模式

  • 抽象工廠模式能夠與外觀模式一塊兒使用以提供一個接口,這一接口可用來以一種子系統獨立的方式建立子系統對象。抽象工廠也能夠代替外觀模式隱藏哪些與平臺相關的類。
  • 中介者模式與外觀模式的類似之處是,他抽象了一些已有的類的功能。然而,中介者的目的是對同事之間的任意通信進行抽象,一般集中部署域任何單個對象的功能。中介者的同事對象知道中介者並與他通訊,而不是直接與其餘同類對象通訊。相對而言,外觀模式僅對子系統對象的接口進行抽象,從而使他們更容易使用,他並不定義新功能,子系統也不知道外觀的存在。
  • 外觀對象經常屬於單例模式。

Flyweight(享元)

定義

運用共享技術有效地支持大量細粒度的對象。編程

結構

享元模式包含如下角色:設計模式

  • Flyweight(抽象享元類):一般是一個接口或抽象類,在抽象享元類中聲明瞭具體享元類公共的方法,這些方法能夠向外界提供享元對象的內部數據(內部狀態),同時也能夠經過這些方法來設置外部數據(外部狀態)。
  • ConcreteFlyweight(具體享元類):它實現了抽象享元類,其實例稱爲享元對象;在具體享元類中爲內部狀態提供了存儲空間。一般咱們能夠結合單例模式來設計具體享元類,爲每個具體享元類提供惟一的享元對象。
  • UnsharedConcreteFlyweight(非共享具體享元類):並非全部的抽象享元類的子類都須要被共享,不能被共享的子類可設計爲非共享具體享元類;當須要一個非共享具體享元類的對象時能夠直接經過實例化建立。
  • FlyweightFactory(享元工廠類):享元工廠類用於建立並管理享元對象,它針對抽象享元類編程,將各類類型的具體享元對象存儲在一個享元池中,享元池通常設計爲一個存儲「鍵值對」的集合(也能夠是其餘類型的集合),能夠結合工廠模式進行設計;當用戶請求一個具體享元對象時,享元工廠提供一個存儲在享元池中已建立的實例或者建立一個新的實例(若是不存在的話),返回新建立的實例並將其存儲在享元池中。

示例

// 書籍類,書的基本信息和借閱信息都是屬性
// 但同一本書能夠被屢次借出,對借閱記錄來講,同一本書的屢次借閱記錄裏存儲的書的信息是冗餘的
class OriginBookRecord {
  // 書的基本信息
  ISBN: string;
  title: string;
  // 借閱信息
  id: string;
  time: string;
  constructor(ISBN: string, title: string, id: string, time: string) {
    this.ISBN = ISBN;
    this.title = title;
    this.id = id;
    this.time = time;
  }

  checkout(time: string) {
    this.time = time;
  }
}

// 書籍管理者
class OriginBookRecordManager {
  books: Map<string, OriginBookRecord>;
  add(ISBN: string, id: string, title: string, time: string) {
    const book = new OriginBookRecord(ISBN, title, id, time);
    this.books.set(id, book);
  }

  checkout(id: string, time: string): void {
    const book = this.books.get(id);
    if (book) {
      book.checkout(time);
    }
  }
}

// 享元模式,分離內部狀態和外部狀態,將能共享的部分分離出來
// 本案例中,書的基本信息和借閱信息分離開來,同一本書能夠有多條借閱記錄
class LibraryBook {
  ISBN: string;
  title: string;
  constructor(ISBN: string, title: string) {
    this.ISBN = ISBN;
    this.title = title;
  }
}

// 享元工廠
class LibraryBookFactory {
  books: Map<string, LibraryBook>;
  createBook(ISBN: string, title: string): LibraryBook {
    let book = this.books.get(ISBN);
    if (!book) {
      book = new LibraryBook(ISBN, title);
      this.books.set(ISBN, book);
    }
    return book;
  }
}
// 將享元工廠實現爲單例
const libraryBookFactory = new LibraryBookFactory();

// 借閱記錄,此時記錄對象不須要保存書的屬性,只須要保存一個書的引用,減小了存儲空間
class BookRecord {
  book: LibraryBook;
  id: string;
  time: string;
  constructor(id: string, book: LibraryBook, time: string) {
    this.book = book;
    this.time = time;
    this.id = id;
  }
  checkout(time: string) {
    this.time = time;
  }
}

class BookRecordManager {
  bookRecords: Map<string, BookRecord>;
  add(id: string, ISBN: string, title: string, time: string): void {
    const book = libraryBookFactory.createBook(ISBN, title);
    const bookRecord = new BookRecord(id, book, time);
    this.bookRecords.set(id, bookRecord);
  }
  checkout(id: string, time: string) {
    const bookRecord = this.bookRecords.get(id);
    if (bookRecord) {
      bookRecord.checkout(time);
    }
  }
}
複製代碼

適用場景

使用享元模式須要符合如下條件:bash

  • 一個應用須要使用大量對象;
  • 徹底因爲使用大量的對象,形成很大的存儲開銷;
  • 對象的大多數狀態均可變爲外部狀態;
  • 若是刪除對象的外部狀態,那麼能夠用相對較少的共享對象取代不少組對象;

優勢

  • 能夠極大減小內存中對象的數量,使得相同或類似對象在內存中只保存一份,從而能夠節約系統資源,提升系統性能;
  • 享元模式的外部狀態相對獨立,並且不會影響其內部狀態,從而使得享元對象能夠在不一樣的環境中被共享;

缺點

  • 享元模式使得系統變得複雜,須要分離出內部狀態和外部狀態,這使得程序的邏輯複雜化;

注意點

  • 刪除外部狀態。該模式的可用性很大程度上取決因而否容易識別外部狀態並將它從共享對象中刪除。若是不一樣種類的外部狀態和共享前對象的書目相同的話,刪除外部狀態不會下降存儲消耗。
  • 管理共享對象。由於對象是共享的,用戶不能直接對他進行實例化。須要有享元工廠幫助用戶查找某個特定的享元對象。共享還意味着能夠方便地進行引用計數和垃圾回收,當享元對象書目固定並且很小的時候,能夠永久保存。

相關模式

  • 享元模式一般和組合模式結合,用共享葉節點的有向無環圖實現一個邏輯上的層次結構。
  • 最好用享元實現狀態和策略對象。

Proxy(代理)

定義

爲其餘對象提供一種代理以控制對這個對象的訪問。ide

結構

代理模式包含如下角色:post

  • Subject(抽象主題角色):它聲明瞭真實主題和代理主題的共同接口,這樣一來在任何使用真實主題的地方均可以使用代理主題,客戶端一般須要針對抽象主題角色進行編程。
  • Proxy(代理主題角色):它包含了對真實主題的引用,從而能夠在任什麼時候候操做真實主題對象;在代理主題角色中提供一個與真實主題角色相同的接口,以便在任什麼時候候均可以替代真實主題;代理主題角色還能夠控制對真實主題的使用,負責在須要的時候建立和刪除真實主題對象,並對真實主題對象的使用加以約束。一般,在代理主題角色中,客戶端在調用所引用的真實主題操做以前或以後還須要執行其餘操做,而不只僅是單純調用真實主題對象中的操做。
  • RealSubject(真實主題角色):它定義了代理角色所表明的真實對象,在真實主題角色中實現了真實的業務操做,客戶端能夠經過代理主題角色間接調用真實主題角色中定義的操做。

示例

遠程代理

爲一個對象在不一樣的地址空間提供局部表明,延遲獲取遠程對象。

class RemoteResource {
  getContent(): string {
    return '讀取遠程文件內容';
  }
}

class RemoteRecourceProxy {
  getContent() {
    const resource = this.request();
    return resource.getContent();
  }

  request(): RemoteResource {
    console.log('千辛萬苦從遠程拿到了文件')
    return new RemoteResource();
  }
}

function remoteProxyDemo() {
  const resource = new RemoteRecourceProxy();
  const content = resource.getContent();
  console.log(content);
}
remoteProxyDemo();
複製代碼

虛代理

若是須要建立一個資源消耗較大的對象,先建立一個消耗相對較小的對象,真實對象只在須要時纔會被真正建立。

// 大圖片,繪製會消耗較多資源
  class BigImage {
    private name: string;
    constructor(name: string) {
      this.name = name;
      this.draw();
    }
    // 繪製
    draw(): void {
      console.log('繪製 ${this.name},須要消耗大量資源');
    }
    // 預覽
    preview(): void {
      console.log(`展現 ${this.name} 的預覽效果`);
    }
    getName(): string {
      return this.name;
    }
  }

  class VirutalBigImageProxy {
    private image: BigImage;
    private name: string;
    // 虛代理先建立一個大圖片的代理,而不真正建立實際對象
    constructor(name: string) {
      this.name = name;
    }
    // 只有在要預覽時,才真正繪製圖像
    preview(): void {
      if (!this.image) {
        this.image = new BigImage(this.name);
      }
      this.image.preview();
    }
    getName(): string {
      if (!this.image) {
        console.log('返回虛代理裏保存的名稱');
        return this.name;
      }
      console.log('實際圖片已經被建立,返回實際圖片的名稱');
      return this.image.getName();
    }
  }

  function virutalProxyDemo() {
    const image1 = new VirutalBigImageProxy('圖1');
    const image2 = new VirutalBigImageProxy('圖2');
    // 讀取圖1的名稱,此時不須要真正繪製大圖片,只須要返回虛代理裏存儲的數據便可,減少開銷
    console.log(image1.getName());
    // 只有在真正須要使用大圖片時,才建立大圖片對象
    image2.preview();
  }
  virutalProxyDemo();
複製代碼

保護代理

控制對原始對象的訪問,保護代理用戶對象應該有不一樣的訪問權限的時候。

class SecretDoc {
    read(): string {
      return '機密文件內容';
    }
  }

  class ProtectionSecretDocProxy {
    private name: string;
    private  doc: SecretDoc;
    constructor(name: string) {
      this.name = name;
      this.doc = new SecretDoc();
    }
    // 提供相同的方法名,可是加了權限控制的代碼
    read(): string {
      if (this.name === '遠峯') {
        const content = this.doc.read();
        return content;
      }
      return '';
    }
  }

  function protectionProxyDemo() {
    const doc1 = new ProtectionSecretDocProxy('遠峯');
    console.log(`遠峯讀出了: ${doc1.read()}`);
    const doc2 = new ProtectionSecretDocProxy('其餘人');
    console.log(`其餘人讀出了: ${doc2.read()}`);
  }
  protectionProxyDemo();
複製代碼

智能代理

在訪問對象時執行一些附加的操做。

class Resource {
  content: string;
  constructor(content: string) {
    this.content = content;
  }
  read(): string {
    return this.content;
  }
  write(content: string): Promise<null> {
    return new Promise(resolve => {
      setTimeout(() => {
        this.content = content;
        resolve();
      }, 1000);
    })
  }
}

// 智能代理,多了一個是否上鎖的屬性,以及相關對鎖的操做
class SmartResourceProxy {
  lock: boolean;
  resource: Resource;
  constructor() {
    this.resource = new Resource('文件內容');
  }
  read(): string|Error {
    if (this.lock) { return new Error('別人正在寫'); }
    console.log('正在讀');
    return this.resource.read();
  }
  write(content: string) {
    console.log('正在寫')
    this.lock = true;
    this.resource.write(content)
      .then(() => {
        this.lock = false;
      });
  }
}

function smartProxyDemo() {
  const resource = new SmartResourceProxy();
  // 能讀到內容
  console.log(resource.read());

  resource.write('新的文件內容');
  // 因爲別人正在寫,讀不到內容
  try {
    resource.read();
  } catch (e) {
    console.error(e);
  }
}
smartProxyDemo();

複製代碼

適用場景

  • 遠程代理。爲一個對象在不一樣的地址空間提供局部表明。
  • 虛代理。根據須要建立開銷很大的對象。
  • 保護代理。控制對原始對象的訪問,保護代理用於對象應該有不一樣的訪問權限的時候。
  • 智能指引。在訪問對象時執行一些附加操做,如:
    1)對指向實際對象的引用計數,這樣當該對象沒有引用時,能夠自動釋放他;
  1. 當第一次引用一個持久對象時,將它裝入內存;
  2. 在訪問一個實際對象前,檢查是否已經鎖定了他,以確保其餘對象不能改變他;

優勢

代理模式公有優勢:

  • 可以協調調用者和被調用者,在必定程度上下降了系統的耦合度;
  • 客戶端能夠針對抽象主題角色進行編程,增長和更換代理類無須修改源代碼,符合開閉原則,系統具備較好的靈活性和可擴展性;

不一樣代理模式有各自的優勢:

  • 遠程代理爲位於兩個不一樣地址空間對象的訪問提供了一種實現機制,能夠將一些消耗資源較多的對象和操做移至性能更好的計算機上,提升系統的總體運行效率;
  • 虛代理經過一個消耗資源較少的對象來表明一個消耗資源較多的對象,能夠在必定程度上節省系統的運行開銷;
  • 保護代理能夠控制對一個對象的訪問權限,爲不一樣用戶提供不一樣級別的使用權限;

缺點

  • 實現代理模式須要額外的工做,並且有些代理模式的實現過程較爲複雜,例如遠程代理;

相關模式

  • 適配器模式爲他所適配的對象提供了一個不一樣的接口,相反,代理提供了與它的實體相同的接口。然而,用於訪問保護的代理可能會拒絕執行實體會執行的操做,所以,它的接口實際上可能只是實體接口的一個子集。
  • 儘管裝飾器的實現部分與代理類似,但裝飾器的目的不同,裝飾器爲對象添加一個或多個功能,而代理則控制對對象的訪問。保護代理實現可能與裝飾器差很少,遠程代理不包含對實體的直接引用,而只是一個間接引用,如「主機ID,主機上的局部地址」。虛代理開始的時候使用一個間接引用,最終將獲取並使用一個直接引用。

參考文檔

本文介紹了前4種結構型模式,對後續模式感興趣的同窗能夠關注專欄或者發送簡歷至'tao.qit####alibaba-inc.com'.replace('####', '@'),歡迎有志之士加入~

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

相關文章
相關標籤/搜索