Typescript 玩转设计模式 之 结构型模式(下)
继前文 Typescript 玩转设计模式 之 结构型模式(上) 之后,本周带来的是系列文章之三,讲解的是 3 种结构性模式:
- 外观
- 享元
- 代理
Facade(外观)
定义
为子系统中的一组接口提供一个一致的界面,Facade 模式定义一个高层接口,这个接口使得这个子系统更加容易使用。
结构
外观模式包含以下角色:
- Facade(外观角色):在客户端可以调用它的方法,在外观角色中可以知道相关的(一个或者多个)子系统的功能和责任;在正常情况下,它将所有从客户端发来的请求委派到相应的子系统去,传递给相应的子系统对象处理。
- SubSystem(子系统角色):在软件系统中可以有一个或者多个子系统角色,每一个子系统可以不是一个单独的类,而是一个类的集合,它实现子系统的功能;每一个子系统都可以被客户端直接调用,或者被外观角色调用,它处理由外观类传过来的请求;子系统并不知道外观的存在,对于子系统而言,外观角色仅仅是另外一个客户端而已。
示例
案例:领导提出要实现一个产品功能,但又不想了解其中的细节。
// 主意
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);
}
}
}
适用场景
使用享元模式需要符合以下条件:
- 一个应用需要使用大量对象;
- 完全由于使用大量的对象,造成很大的存储开销;
- 对象的大多数状态都可变为外部状态;
- 如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象;
优点
- 可以极大减少内存中对象的数量,使得相同或相似对象在内存中只保存一份,从而可以节约系统资源,提高系统性能;
- 享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享;
缺点
- 享元模式使得系统变得复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化;
注意点
- 删除外部状态。该模式的可用性很大程度上取决于是否容易识别外部状态并将它从共享对象中删除。如果不同种类的外部状态和共享前对象的书目相同的话,删除外部状态不会降低存储消耗。
- 管理共享对象。因为对象是共享的,用户不能直接对他进行实例化。需要有享元工厂帮助用户查找某个特定的享元对象。共享还意味着可以方便地进行引用计数和垃圾回收,当享元对象书目固定而且很小的时候,可以永久保存。
相关模式
- 享元模式通常和组合模式结合,用共享叶节点的有向无环图实现一个逻辑上的层次结构。
- 最好用享元实现状态和策略对象。
Proxy(代理)
定义
为其他对象提供一种代理以控制对这个对象的访问。
结构
代理模式包含以下角色:
- 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)对指向实际对象的引用计数,这样当该对象没有引用时,可以自动释放他;
- 当第一次引用一个持久对象时,将它装入内存;
- 在访问一个实际对象前,检查是否已经锁定了他,以确保其他对象不能改变他;
优点
代理模式公有优点:
- 能够协调调用者和被调用者,在一定程度上降低了系统的耦合度;
- 客户端可以针对抽象主题角色进行编程,增加和更换代理类无须修改源代码,符合开闭原则,系统具有较好的灵活性和可扩展性;
不同代理模式有各自的优点:
- 远程代理为位于两个不同地址空间对象的访问提供了一种实现机制,可以将一些消耗资源较多的对象和操作移至性能更好的计算机上,提高系统的整体运行效率;
- 虚代理通过一个消耗资源较少的对象来代表一个消耗资源较多的对象,可以在一定程度上节省系统的运行开销;
- 保护代理可以控制对一个对象的访问权限,为不同用户提供不同级别的使用权限;
缺点
- 实现代理模式需要额外的工作,而且有些代理模式的实现过程较为复杂,例如远程代理;
相关模式
- 适配器模式为他所适配的对象提供了一个不同的接口,相反,代理提供了与它的实体相同的接口。然而,用于访问保护的代理可能会拒绝执行实体会执行的操作,因此,它的接口实际上可能只是实体接口的一个子集。
- 尽管装饰器的实现部分与代理相似,但装饰器的目的不一样,装饰器为对象添加一个或多个功能,而代理则控制对对象的访问。保护代理实现可能与装饰器差不多,远程代理不包含对实体的直接引用,而只是一个间接引用,如“主机 ID,主机上的局部地址”。虚代理开始的时候使用一个间接引用,最终将获取并使用一个直接引用。
参考文档
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: Event Loop 的规范和实现
下一篇: WAF Bypass 介绍分析
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论