跳到主要内容

13 篇博文 含有标签「设计模式」

设计模式

查看所有标签

装饰模式 (Decorator)

· 阅读需 4 分钟

装饰模式是一种结构型设计模式,通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为。

  • 无需创建新子类即可扩展对象的行为。
  • 可以在运行时添加或删除对象的功能。
  • 可以用多个装饰封装对象来组合几种行为。
  • 单一职责原则。可以将实现了许多不同行为的一个大类拆分为多个较小的类。

示例

写了一个数据读写的例子,定义一个 DataSource 接口规定读写操作。StringDataSource 类实现了该接口,可以读写字符串;定义装饰基类 DataSourceDecorator ,对将读写操作直接委派给被封装组件。

具体装饰类要继承装饰基类,然后扩展或改造读写方法,例如 EncryptionDecorator 类是加解密装饰类,基于装饰基类,改造了读写函数使之可以加密写入、解密读取。CompressDecorator 类同理,它封装了解压缩操作。

interface DataSource {
writeData(data: string): void;
readData(): string;
}

// 字符串内容读写类,实现了读写接口
class StringDataSource implements DataSource {
private content: string;

constructor(content?: string) {
this.content = content || '';
}

writeData(data: string): void {
this.content = data;
console.log(`Written ${data}.`);
}

readData(): string {
return this.content;
}
}

// 装饰基类
class DataSourceDecorator implements DataSource {
// 被封装的组件
protected source: DataSource;

constructor(dataSource: DataSource) {
this.source = dataSource;
}

// 装饰基类会直接将所有工作分派给被封装组件。具体装饰中则可以新增一些
// 额外的行为。
writeData(data: string): void {
this.source.writeData(data);
}
readData(): string {
return this.source.readData();
}
}

// 加解密装饰类,继承装饰基类,对读写数据函数进行了可加解密的改造
class EncryptionDecorator extends DataSourceDecorator {
writeData(data: string): void {
let chars = [];
for (let i = 0; i < data.length; i++) {
chars.push(String.fromCharCode(data.charCodeAt(i) * (i + 1)));
}
super.writeData(String.fromCharCode(6159) + chars.join(''));
}

readData(): string {
let str = super.readData();
if (str.startsWith(String.fromCharCode(6159))) {
let chars = [];
for (let i = 1; i < str.length; i++) {
chars.push(String.fromCharCode(str.charCodeAt(i) / i));
}
return chars.join('');
}
return str;
}
}

// 解压缩装饰类,继承装饰基类,对读写数据函数进行了可解压缩改造
class CompressDecorator extends DataSourceDecorator {
writeData(data: string): void {
super.writeData(`Compressed: ${data}`);
}
readData(): string {
return `Uncompressed: ${super.readData()}`;
}
}

// 字符串数据的读写
const strData = new StringDataSource('abcde');
strData.writeData('Hello'); // 写入
console.log(strData.readData()); // 读取

// 字符串数据的加解密读写,用加解密装饰对象包裹了字符串数据对象
const enc = new EncryptionDecorator(strData);
// 这里的加密写入,就是先加密,然后将加密好的内容委派由装饰基类 super.writeData() 处理,
// 基类该函数又将内容传给被封装的对象 source.writeData() 来处理
enc.writeData('Some secret.'); // 加密写入
console.log(enc.readData()); // 解密读取

输出:

Written Hello.
Hello

Written ᠏SÞŇƔ ʲ˃̘ЂϲӼȨ.
Some secret.

组合模式 (Composite)

· 阅读需 4 分钟

组合模式是一种结构型设计模式,它可以将对象组合成树状结构,并且能像使用独立对象一样使用它们。组合模式可以通过将同一抽象或接口类型的实例放入树状结构的行为方法来识别出。

  • 你可以利用多态和递归机制更方便地使用复杂树结构。
  • 开闭原则。无需更改现有代码,就可以在应用中添加新元素,使其成为对象树的一部分。
  • 对于功能差异较大的类,提供公共接口或许会有困难。在特定情况下,需要过度一般化组件接口,使其变得令人难以理解。

示例

Graphic 接口定义了一些图形应有的基础函数,绘制是否含有子项Dot 类和 Composite 类都实现该图形接口,Composite 类另外实现了添加和删除子项的函数,保存一个 children 数组,子项可以是简单的 Dot 对象,也可以是复杂的 Composite 对象,由此可以形成一个树状的结构。

客户端代码通过 Graphic 接口,来调用抽象的不具体的函数。

// 接口定义了图形应有的一些函数功能,画、有无子项
interface Graphic {
draw(): void;
isComposite(): boolean;
}

/**
* 该类代表了组合对象的末端,末端没有子项。
* 通常是该类完成实际的工作,组合对象只负责把工作委派给他们。
* 点类实现了图形接口,实现具体的画、有无子项函数
*/
class Dot implements Graphic {
protected x = 0;
protected y = 0;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
public draw(): void {
console.log('.', this.x, this.y);
}

// 可提供这个方法,让客户端代码确定某组件是否可以有子组件
public isComposite(): boolean {
return false;
}
}

/**
* 组合类实现了图形接口,它可以添加删除子项(简单项、复杂组合)
* 它把实际工作委派给子项,然后再收集汇总他们的结果
*/
class Composite implements Graphic {
private children: Graphic[] = [];

public add(graphic: Graphic) {
this.children.push(graphic);
}
public remove(graphic: Graphic) {
this.children.splice(this.children.indexOf(graphic), 1);
}
public isComposite() {
return true;
}
// 递归遍历子项,收集汇总它们的结果
public getChildren() {
const arr: any[] = [];
this.children.forEach((v) => {
if (v instanceof Composite) {
arr.push(v.getChildren());
} else {
arr.push(v);
}
});
return arr;
}
// 将子项所有结果收集汇总,并把最终结果输出
draw(): void {
console.log(this.getChildren());
}
}

// 客户端代码,通过基础接口,就可以与所有的简单项、复杂组合进行交互
function ClientCode(graphic: Graphic) {
graphic.draw();
}

const composite = new Composite();
const dot1 = new Dot(1, 1);
const dot2 = new Dot(2, 2);
const comp1 = new Composite();
comp1.add(dot1);
composite.add(comp1);
composite.add(dot2);

// [ [ Dot { x: 1, y: 1 } ], Dot { x: 2, y: 2 } ]
ClientCode(composite);

适配器模式 (Adapter)

· 阅读需 3 分钟

适配器模式是一种结构性设计模式,能够使不兼容的对象相互合作。可以担任两个对象之间的封装器,将一个对象的调用转换为另一个对象可识别的调用。

适配器模式允许你创建一个中间层类, 其可作为代码与遗留类、 第三方类或提供怪异接口的类之间的转换器。

特点:

  • 单一职责原则,可以将接口或数据转换代码从程序主要业务逻辑中分离。
  • 开闭原则,只要客户端代码通过客户端接口与适配器进行交互,你就能在不修改现有客户端代码的情况下在程序中添加新类型的适配器。
  • 代码整体复杂度增加,因为需要新增一系列接口和类。有时直接更改服务类使其与其他代码兼容会更简单。

示例

客户端要通过标准 Requestable 接口规范来请求,像 request('Hi') 这样调用。但一个重要的功能,调用方式并不规范,已经过时,但又不好重写,需要传两个参数 request('Hi', '') 这样来调用,这时就需要适配器来解决。

interface Requestable {
request(str: string): void;
}

// 适配器遵循客户端要求的接口,将其转换为符合遗留类要求的调用方式,对遗留类的函数进行调用。
class Adapter implements Requestable {
private adaptee: Adaptee;

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

// 符合规范的调用,内部转为符合遗留标准的调用
public request(str: string): void {
this.adaptee.wierdRequest(str, '');
}
}

// 遗留的功能类
class Adaptee {
public wierdRequest(str1: string, str2: string) {
console.log(str1 + str2);
}
}

// 客户端代码,只会调 request('Hi') 这样,不会传两个参数
function clientCode(target: Requestable) {
target.request('Hi');
}

clientCode(new Adapter(new Adaptee()));

生成器模式 (Builder)

· 阅读需 4 分钟

生成器模式也叫建造者模式,这种模式可以分步骤创建复杂对象。该模式允许你使用相同的创建代码生成不同类型和形式的对象。

  • 你可以分步创建对象
  • 生成不同形式的产品时,可以复用相同的制造代码
  • 单一职责原则,你可以将复杂构造代码从产品的业务逻辑中分离出来
  • 由于该模式需要新增多个类,因此代码整体复杂程度会有所增加

示例

下列代码,根据 Builder 生成器接口,实现了 CarBuilder 来造车,CarBuilder 提供了造车的一系列步骤函数,并提供自己的 getCar() 方法来获取结果产品。

可选的主管类 Director 用来指导造车,它提供一个 makeCar(builder: Builder) 方法来按照一定工艺流程来调用一系列造车函数。

在造完车之后,调用汽车生成器的 getCar() 方法获取成品。

// 生成器接口指定了创建产品对象不同部分的一系列方法
interface Builder {
setEngine(engine: string): void;
setGPS(gps: string): void;
setWheels(wheels: string): void;
reset(): void;
}

// 特定生成器类根据生成器接口,实现一系列创建产品的步骤方法。
// 可以根据该接口,实现多个不同的生成器变体,例如 TruckBuilder、BusBuilder
class CarBuilder implements Builder {
private car;

// 一个生成器实例,应该包含一个空的产品对象,未来用于进一步的组合
constructor() {
this.car = new Car();
}

reset(): void {
this.car = new Car();
}
setEngine(engine: string): void {
this.car.addItem(engine);
}
setGPS(gps: string): void {
this.car.addItem(gps);
}
setWheels(wheels: string): void {
this.car.addItem(wheels);
}

/**
* 具体的生成器提供自己的方法来获取结果,因为不同类型的生成器可能创建出完全
* 不同的产品而且不实现同一接口,因此这种方法不能被声明在基础生成器接口中。
* 通常,在把结果返回给客户端之后,生成器实例会准备进行下一个产品的创建。
* 这就是为什么一般要在 getProduct 函数的末尾调用 reset 函数。
* 但这种做法并不是强制的,也可以在合适的时间,在客户端或者主管类代码来一次 reset 调用。
*/
getCar() {
const result = this.car;
this.reset();
return result;
}
}

// 最好只在创建产品很复杂而且需要大量配置的时候用生成器模式。
class Car {
private items: any[] = [];

public addItem(item: any) {
this.items.push(item);
}

public listParts() {
return this.items;
}
}

/**
* 主管只负责按照特定顺序执行生成步骤。其在根据特定步骤或配置来生成产品时
* 会很有帮助。由于客户端可以直接控制生成器,所以严格意义上来说,主管类并
* 不是必需的。
*/
class Director {
makeCar(builder: Builder) {
builder.setEngine('Engine');
builder.setGPS('GPS');
builder.setWheels('Wheels');
}
}

function ClientCode() {
// 主管类指导造车
const director = new Director();
const carBuilder = new CarBuilder();
director.makeCar(carBuilder);
console.log(carBuilder.getCar().listParts()); // ['Engine', 'GPS', 'Wheels']

// 再造一辆,不用主管类
carBuilder.setEngine('SuperEngine');
carBuilder.setWheels('AntiBullet');
console.log(carBuilder.getCar().listParts()); // ['SuperEngine', 'AntiBullet']
}

ClientCode();

原型模式 (Prototype)

· 阅读需 2 分钟

原型模式是一种创建型设计模式,使你能够复制已有对象,而又无需使代码依赖它们所属的类。

它将克隆过程委派给被克隆的实际对象,这样可以解决一些私有变量无法被外部访问就无法在外部复制的问题,让自身提供克隆方法来复制自己,特征是一个类包含 clone 或者 copy 函数,来复制一份自身对象。

示例

下面是一个 Prototype 类,里面有 clone 函数,会复制一份自身对象并返回。一般也可以,定义一个 Cloneable 接口,接口里仅包含一个 clone 函数的定义,让其他类来实现这个接口。

class Prototype {
public primitive: any;
public component: object | null = null;
public circularReference: ComponentWithBackReference | null = null;

/**
* 像实现一个深拷贝,把自身的成员变量都给拷贝一遍,考虑基本类型、引用类型、
* 循环引用的情况、日期类型、正则对象等等
*/
public clone(): this {
const clone = Object.create(this);
// 假设 component 对象只有一层,可以这样拷贝
clone.component = Object.create(this.component);
// 处理循环引用,先用扩展运算符把基本类型的成员复制出来,再单独处理 prototype
clone.circularReference = {
...this.circularReference,
prototype: { ...this },
};
return clone;
}
}

// 循环引用的类,prototype 里有它,它里面有 prototype
class ComponentWithBackReference {
public prototype: Prototype;

constructor(proto: Prototype) {
this.prototype = proto;
}
}