Typescript 重走常用设计模式(二)
2019-03-22 #Coding

本节主要涉及创造型模式:单例模式、工厂模式,结构型模式:适配器模式、装饰模式、代理模式、外观模式

创造型模式

单例模式 Singleton

概念

单例模式确保一个类只有一个实例,并提供一个访问它的全局访问点。

示例 UML 类图

示例代码实现

Singleton中声明了一个getInstance的静态方法用于返回自身所属类的一个相同实例,如果实例未被创建则创建,如已被创建的直接返回已存在的实例,保持唯一性。

由于 Javascript 内没有privatestatic等修饰符,之前的instance基本都被用闭包来实现存储,类构造函数也无法约束被调用。

1
2
3
4
5
6
7
8
9
10
11
12
function Singleton() {
// ...
}
Singleton.getInstance = (function(){
let instance = null
return function() {
if (instance === null) {
instance = new Singleton()
}
return instance
}
})()

进入 ES6 之后,可以通过 Symbol 这个新增的原始类型来保证instance的唯一性

1
2
3
4
5
6
7
8
9
10
11
12
const instance = Symbol()
class Singleton {
constructor() {
// ...
}
static getInstance() {
if(!this[instance]) {
this[instance] = new Singleton()
}
return this[instance]
}
}

得益于 Typescipt 终于可以实现对构造方法的私有化,防止外部调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Singleton {
private static instance: Singleton;
private constructor() {
// ..
}

public static getInstance(): Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton()
}

return Singleton.instance;
}

someMethod() {}
}

function client() {
// 直接实例化会 Typescript 会报错
// Constructor of class 'Singleton' is private and only accessible within the class declaration.
const singleton = new Singleton()
const instacneA = Singleton.getInstance()
const instacneB = Singleton.getInstance()
if (instacneA === instacneB) {
console.log('他们存储着相同的实例')
}
}
client()
// console output
// 他们存储着相同的实例

适用场景

单例模式的应用场景在前端开发中相当常见,例如一些需要保持唯一性的组件类:全局遮罩、联合登录模块、购物车等,所以符合以下要求的场景就可以考虑单例模式:

  • 类只能有一个实例,而且可以通过一个周知的访问点访问他

分析

  • 单例模式可以保证只有一个实例,在特定场景保证稳定性
  • 内存中也只存在一个实例化对象,节省空间消耗
  • 其不太符合单一职责原则,因为其在其内部完成了两件事,但是相比于其解决的问题还是值得使用

工厂模式 Factory Method

概念

工厂方法模式定义一个用于创建对象的接口,这个接口由子类决定实例化哪一个类。

工厂分离出了类实例化的过程,让客户程序使用时不需要关心实例化的过程,并且可以在实例化过程时进行额外的操作,如根据不同的运行环境等返回不同的对象实例等。

示例 UML 类图

示例代码实现

Creator 是工厂的抽象方法,也叫创建者,工厂子类通过继承实现具体的创建产品方法,同时这里可以创建一些工厂通用方法被子类调用;

CreatorA… 具体的工厂实现,内部可以在根据不同参数等返回不同的产品实例;

Product 产品接口,用于声明产品内需要实现的方法,对于所有工厂和产品通用;

ProductA… 具体的产品实例;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// Product 接口
interface Product {
productFn(): string
}

class ProductA implements Product {
productFn() {
return `这是:可口可乐`
}
}

class ProductB implements Product {
productFn() {
return `这是:百事可乐`
}
}

// Creator 抽象类,定义了一个 someStuff() 来执行一些公共业务
abstract class Creator {
protected someStuff() {
console.log('进行一些公共业务')
}
public abstract create(): Product
}

// CreatorA 工厂A 只生产 可口可乐
class CreatorA extends Creator {
create() {
super.someStuff()
return new ProductA()
}
}

// CreatorB 工厂B 只生产 百事可乐
class CreatorB extends Creator {
create() {
super.someStuff()
return new ProductB()
}
}
// 实例化工厂
const creatorA = new CreatorA()
const creatorB = new CreatorB()
// 生产
const productA = creatorA.create()
const productB = creatorB.create()
console.log(productA.productFn())
console.log(productB.productFn())

// console output
// 进行一些公共业务
// 进行一些公共业务
// 这是:可口可乐
// 这是:百事可乐

同时还有一种简单工厂模式,并不在GOF 23种设计模式之中,区别就在于工厂是一个具体类或者静态方法,直接创造具体的产品对象实例,多用于根据不同参数或者环境等返回不同的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Product 接口
interface Product {
productFn(): string
}

// CarA
class CarA implements Product {
productFn() {
return `这是:阿特兹`
}
}

// CarB
class CarB implements Product {
productFn() {
return `这是:新款的阿特兹`
}
}

// Creator
class Creator {
static create(year: number): Product {
// 根据不同参数返回不同的实例
if (year < 2018) {
return new CarA()
} else {
return new CarB()
}
}
}

const car = Creator.create(2019)
console.log(car.productFn())

// console output
// 这是:新款的阿特兹

适用场景

  • 对象构建十分复杂时
  • 需要根据不同的情况、环境来创建不同的实例

分析

  • 符合单一职责原则,产品和创建产品的代码相互独立,只负责自己的职责
  • 符合开放封闭原则,客户程序使用产品时,无需修改客户程序代码即可在工厂中增量增加产品类型

  • 工厂生产的产品较多时,可能导致工厂内部的逻辑过于复杂

结构型模式

适配器模式 Adapter

概念

适配器模式用于将类、对象的接口(方法或属性)转换为客户程序期望的另一个接口(方法或属性),使原本不兼容的类、对象可以在一起工作。

就比如港版 PS4 默认的插头为英制插头,无法直接插到国内的电源接口,需要一个转接头进行转换,而这个转接头就是适配器。

示例 UML 类图

示例代码实现

Target 是客户程序 client 所期待的接口,其可以是接口或者类,这里用的是类,其内部有一个request()

Adaptee 是已存在需要适配的接口(这里也用类),其内部包含接口的特殊请求方法specificRequest(),但是 client 还无法正常使用;

Adapter 是适配器,Adapter 依赖 Adaptee 并继承 Target将 Adaptee 中无法兼容的specificRequest()改造为request()`,以供 client 使用;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// client 所需要的接口
class Target {
public request(): string {
return '中国标准的插头'
}
}
// 不兼容的接口,没有 request 方法
class Adaptee {
public specificRequest(): string {
return '港版英国标准的插头'
}
}
// Adapter 适配器
class Adapter extends Target {
private adaptee: Adaptee

constructor(adaptee: Adaptee) {
super()
this.adaptee = adaptee
}

public request(): string {
return `适配器转换后的 ${this.adaptee.specificRequest()}`;
}
}

// client 客户程序
function client(target: Target) {
console.log(`我是中国标准电源接口,插入的是 ${target.request()}`)
}

// 实例化不兼容的接口
const adaptee = new Adaptee()
// client(adaptee) // 直接给 client 使用 Typescript 会直接报错
// 使用适配器来转换接口
const adapter = new Adapter(adaptee)
client(adapter)

// console output
// 我是中国标准电源接口,插入的是 适配器转换后的 港版英国标准的插头

适用场景

  • 项目中存在一些遗留代码,想使用其中一些类,但其接口不符合使用需求
  • 想要复用一些类,这个类可能与其他接口可能不兼容的类一起工作

分析

  • 符合单一职责原则,接口、适配器、旧的接口代码都相互分离没有干扰,职责专一
  • 符合开放封闭原则,需要新增其他兼容接口时,只需要增量添加新的适配器即可,不需要修改客户程序代码
  • 存在大量适配器的话,代码复杂度会增加

装饰模式 Decorator

概念

装饰器模式是动态的给对象添加新的职责,在添加新的功能时相比于继承更加弹性灵活,不会影响其派生出的其他对象。每个装饰器不但包含着上一层装饰器的状态、方法而且还能对自己进行扩展,同时通过调用超类上的方法形成一条装饰链。

示例 UML 类图

示例代码实现

Shape 是一个基础接口,其声明了实现其的类中需要 draw() 这个具体方法;

Circle 和 Square 为具体形状类,实现了 Shape 接口;

ShapeDecorator 为基础装饰类,也实现了 Shape 基础接口,同时拥有一个指向其实现后实例化对象的引用成员变量;

RedBorderDecorator 和 BlueBackgroundDecorator 为具体装饰类,重写基类的 draw() 方法,然后在其内部调用父类之前的方法进行一些额外的行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 基础接口
interface Shape {
draw(): void
}
// 具体形状类
class Circle implements Shape {
public draw() {
console.log('画一个圆形')
}
}
// 具体形状类
class Square implements Shape {
public draw() {
console.log('画一个正方形')
}
}
// 基础装饰类
class ShapeDecorator implements Shape {
protected shape: Shape
constructor(shape: Shape) {
this.shape = shape
}
public draw() {
this.shape.draw()
}
}

// 具体装饰类
class RedBorderDecorator extends ShapeDecorator {
public draw() {
super.draw()
console.log('添加红色的边框')
}
}
class BlueBackgroundDecorator extends ShapeDecorator {
public draw() {
super.draw()
console.log('添加蓝色的背景')
}
}

// 客户程序 接受 Shape
function client(shape: Shape) {
shape.draw()
}

const circle = new Circle()
const redBorderDecorator = new RedBorderDecorator(circle)
const blueBackgroundDecorator = new BlueBackgroundDecorator(redBorderDecorator)
// 传入装饰过的 Circle
client(blueBackgroundDecorator)

// console output
// 画一个圆形
// 添加红色的边框
// 添加蓝色的背景

ES7 中新增的 decorator 也是一种装饰器模式的思路,可以在不修改原有代码的情况下,动态的添加方法、修改属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function log(target, name, descriptor){
// descriptor.value 为 object 数据描述符 和 writable 平级
const oldFn = descriptor.value
descriptor.value = function() {
console.log(`调用了 ${name} 方法,传入参数为 `, arguments)
// 仍执行原来的方法 this 也原封不动传递过去
return oldFn.apply(this, arguments)
}
// 返回修改过的 函数对象
return descriptor
}

class Math {
@log
add(a, b) {
return a + b
}
}

const math = new Math()
const result = math.add(1, 2)
console.log(result)
// 调用了 add 方法,传入参数为 Arguments(2) [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]
// 3

适用场景

  • 想不修改原有代码,并在运行时为对象增加额外的行为
  • 难以通过继承来扩展对象时
  • ES7 新增的 decorator,通过 @decorator 类似的关键字给类和方法添加装饰器

分析

  • 符合单一职责原则,额外添加的装饰器都保持完成自身一项职责
  • 符合开放封闭原则,无需修改原有代码,增量增加装饰器即可
  • 可以用多个装饰器实现链式调用,动态组合行为
  • 缺点是增加了系统的复杂度,如果嵌套调用出现异常时需要链式逐步排查

代理模式 Proxy

概念

代理模式是为某个对象提供一个代理,并由这个代理对象控制对原对象的访问,允许将访问提交给原对象前后进行一些处理。

示例 UML 类图

示例代码实现

Subject 声明了抽象主题,其为 Proxy 和 RealSubject 的共同接口;

RealSubject 为真实主题,内部包含一个真实的request()方法进行一些实际的业务操作;

Proxy 为代理主题,包含了对 RealSubject 的引用成员变量,Proxy 也实现了 Subject 接口,所以可以随时替换掉 RealSubject 供 Client 使用。同时在其内部可以在进行 RealSubject 前后进行一些逻辑处理,如访问控制、记录日志等,这里做了一个访问控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
interface Subject {
request(): void
}

class RealSubject implements Subject {
public request() {
console.log('RealSubject: 发出真实的请求')
}
}

class Proxy implements Subject {
private realsubject: RealSubject
constructor(realsubject: RealSubject) {
this.realsubject = realsubject
}
private checkBeforeRequest() {
console.log('Proxy: 访问控制,验证是否能进行请求')
return true
}
public request() {
if (this.checkBeforeRequest()) {
console.log('Proxy: 访问控制,验证通过')
this.realsubject.request()
}
}
}
// client 客户程序接受实现 Subject 的类的实例化对象
function client(subject: Subject) {
subject.request()
}
const realSubject = new RealSubject()
const proxy = new Proxy(realSubject)
client(proxy)

另外 ES6 中的 Proxy 也是代理模式的一种思想实现,实现对对象上内部变量变化之前进行一些诸如访问控制之类的逻辑处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 反诬不动产
const apartment = {
apartmentName: 'XXX公寓',
price: 2000000,
phone: '业主电话:18288882222'
}

// 代理房产中介
const agent = new Proxy(apartment, {
/**
* @param {star} target 被代理的目标对象
* @param {string} key 目标对象的 key
*/
get: function(target, key) {
if (key === 'phone') {
// 不能直接给业主电话
return '房产中介电话:13989930022'
}
if (key === 'price') {
// 报价加上税费等
return target[key] + target[key] * 0.02
}
// 没命中 返回目标对象对应 key 的 value
return target[key]
},
set: function(target, key, value) {
if (key === 'offerPrice') {
if (value < 2000000) {
console.log('报价太低了!')
} else {
target[key] = value
}
}
}
})

console.log(agent.apartmentName) // 公寓房产名称可以给对方
console.log(agent.phone) // 手机号不能返回
console.log(agent.price) // 价格需要进行额外处理
agent.offerPrice = 1800000 // 客户报价

// console output
// XXX公寓
// 房产中介电话:13989930022
// 2040000
// 报价太低了!

适用场景

  • 保护代理,进行访问控制,防止对对象进行权限以外的操作等
  • 虚拟代理,建立一个补偿用的大开销对象,使用代理延时化创建
  • 日志代理,在请求时通过代理进行一些日志操作
  • 远程代理,通过代理处理一些额外的网络传输相关的操作,并与远程服务器通信
  • 智能代理,通过代理在进行对象访问时进行一些附加处理

分析

  • 符合开放闭合原则,增量添加代理无需修改原有代码
  • 协调客户程序和目标对象之间的关系,一定程度上降低系统耦合度
  • 缺点是代理模式增加了处理路径长度,降低处理速度

外观模式 Facade

概念

外观模式是为子系统、库等中的一组接口提供一个统一的高层接口,让其更加容易使用。

示例 UML 类图

示例代码实现

Facade 外观类,集合了子系统中的各个实例,根据需要在内部方法中处理对应的子系统操作,再暴露出供客户端进行使用。

SubSystem 子系统类,代表诸多的子系统可以是类的集合,独自处理响应的业务,外观类对与子系统就是个客户端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class Facade {
public operation() {
console.log('Facade: 根据需要调用子系统')
const subSystemA = new SubSystemA()
const subSystemB = new SubSystemB()
subSystemA.operation1()
subSystemA.operationN()
subSystemB.operationN()
console.log('Facade: 执行完毕')
}
}

class SubSystemA {
public operation1() {
console.log('SubSystemA: 处理相关业务1 - H')
}
public operationN() {
console.log('SubSystemA: 处理相关业务N - O')
}
}

class SubSystemB {
public operation1() {
console.log('SubSystemB: 处理相关业务1')
}
public operationN() {
console.log('SubSystemB: 处理相关业务N - T')
}
}
// 客户程序 只需要关心外观 不需要关心子系统
function client(facade: Facade) {
facade.operation()
}
const facade = new Facade()
client(facade)

// console output
// Facade: 根据需要调用子系统
// SubSystemA: 处理相关业务1 - H
// SubSystemA: 处理相关业务N - O
// SubSystemB: 处理相关业务N - T
// Facade: 执行完毕

适用场景

外观模式在业务中应用相当广泛,大部分情况客户程序都不需要了解复杂的内部系统实现和关系,诸如复杂的类库 API 二次封装内部方法对外暴露等。

  • 在维护一个负责的子系统时,需要提供一个简单的接口供客户程序调用。随着时间的推移,子系统往往变的越来越复杂,使用外观模式可以快速的指向需要的子系统功能,满足客户程序的需求。
  • 分层设计时,可以要求子系统应用外观模式定义其各层次的入口,让他们通过外观来进行交互,减少耦合性。

分析

  • 降低客户程序可以和子系统之间关联,适当的进行解耦
  • 设计不当可能让外观类与相当多的类耦合在一块,逐步成为上帝对象

参考

相关文章