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…

相关文章
相关标签/搜索