设计模式

设计模式的优点

  1. 是被证实的解决方案,Patterns are proven solutions
  2. 容易复用,Patterns can be easily reused
  3. 自表达,Patterns can be expressive

设计模式的六大原则:SOLID原则

  1. 单一原则 (SRP): 实现类要职责单一,一个类只做一件事或者一类事,不要将功能无法划分为一类的揉到一起

  2. 里氏替换原则(LSP): 不要破坏继承体系,子类可以完全替换掉他们所继承的父类,可以理解为调用父类方法的地方换成子类也可以正常执行调用

  3. 依赖倒置原则(DIP):如果某套功能或者业务逻辑可能之后会出现并行的另外一种模式或者较大的调整,那不如把这部分逻辑抽象出来,创建一个包含相关方法的抽象类,而实现类继承这个抽象类来重写抽象类中的方法,完成具体的实现,调用这些功能方法的类不需要关心自己调用的这些个方法的具体实现,只管调用这些抽象类中定义好的形式上的方法即可,不与实际实现这些方法的类发生直接依赖关系,方便之后的实现逻辑的替换更改

  4. 接口隔离原则(ISP) : 在设计抽象类的时候要精简单一,白话说就是,A需要依赖B提供的一些方法,A我只用B的3个方法,B就尽量不要给A用不到的方法啦

  5. 迪米特法则(LoD): 降低耦合,尽量减少对象之间的直接的交互,如果其中一个类需要调用另一个类的某一个方法的话,可通过一个关系类发起这个调用,这样一个模块修改时,就可以最大程度的减少波及

  6. 开放-封闭原则(OCP): 对扩展开放,对修改关闭,你可以继承扩展我所有的能力,到你手里你想咋改咋改,但是,别动我本人好吗?好的

The main purpose of refactoring is to fight technical debt. It transforms a mess into clean code and simple design.

Clean Code

  1. clean code is obvious for other programmers
  2. clean code doesn’t contain duplication
  3. clean code contains a minimal number of classes and other moving parts
  4. clean code passes all tests
  5. clean code is easier and cheaper to maintain

Causes of Technical debt

  1. Business pressure
  2. lack of understanding of the consequences of technical debt
  3. failing to combat the strict coherence of components
  4. lack of tests
  5. lack of document
  6. lack of interaction between team members
  7. long-term simultaneous development in several branches
  8. delayed refactoring
  9. lack of compliance monitoring
  10. incompetence

When to refactor

  1. When you’re doing something for the first time, just get it done.
  2. When you’re doing something similar for the second time, cringe at having to repeat but do the same thing anyway.
  3. When you’re doing something for the third time, start refactoring.
  1. 创建型模式
    1. 工厂方法模式
    2. 抽象工厂模式
    3. 生成器模式
    4. 原型模式
    5. 单例模式
  2. 结构型模式
    1. 适配器模式
    2. 桥接模式
    3. 组合模式
    4. 装饰模式
    5. 外观模式
    6. 享元模式
    7. 代理模式
  3. 行为模式
    1. 责任链模式
    2. 命令模式
    3. 迭代器模式
    4. 中介者模式
    5. 备忘录模式
    6. 观察者模式
    7. 状态模式
    8. 策略模式
    9. 模板方法模式
    10. 访问者模式

Singleton 单例模式

单例模式是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。

使用场景:
  1. 如果程序中的某个类对于所有客户端只有一个可用的实例, 可以使用单例模式。
  2. 如果你需要更加严格地控制全局变量, 可以使用单例模式。
实现方式:
  1. 在类中添加一个私有静态成员变量用于保存单例实例。
  2. 声明一个公有静态构建方法用于获取单例实例。
  3. 在静态方法中实现”延迟初始化”。 该方法会在首次被调用时创建一个新对象, 并将其存储在静态成员变量中。 此后该方法每次被调用时都返回该实例。
  4. 将类的构造函数设为私有。 类的静态方法仍能调用构造函数, 但是其他对象不能调用。
  5. 检查客户端代码, 将对单例的构造函数的调用替换为对其静态构建方法的调用。
优点:
  1. 可以保证一个类只有一个实例。
  2. 获得了一个指向该实例的全局访问节点。
  3. 仅在首次请求单例对象时对其进行初始化。
缺点:
  1. 违反了单一职责原则。 该模式同时解决了两个问题。
  2. 单例模式可能掩盖不良设计, 比如程序各组件之间相互了解过多等。
  3. 该模式在多线程环境下需要进行特殊处理, 避免多个线程多次创建单例对象。
  4. 单例的客户端代码单元测试可能会比较困难, 因为许多测试框架以基于继承的方式创建模拟对象。 由于单例类的构造函数是私有的, 而且绝大部分语言无法重写静态方法, 所以你需要想出仔细考虑模拟单例的方法。 要么干脆不编写测试代码, 或者不使用单例模式。
1
2
3
4
5
6
function Person() {
if (typeof Person.instance === 'object')
return Person.instance;
Person.instance = this;
return this;
}
1
2
3
4
5
6
7
8
9
class Person {
constructor() {
if (typeof Person.instance === 'object') {
return Person.instance;
}
Person.instance = this;
return this;
}
}

Factory 工厂模式

分类:
  1. 简单工厂(Simple Factory)模式,又称静态工厂方法模式(Static Factory Method Pattern)
    • 违背OCP原则,每次新增新类创建过程,都要修改switch-case或if-else语句
    • 适用于创建对象较少的情况
  2. 工厂方法(Factory Method)模式,又称多态性工厂(Polymorphic Factory)模式或虚拟构造子(Virtual Constructor)模式
    • 工厂方法模式是一种创建型设计模式, 其在父类中提供一个创建对象的方法, 允许子类决定实例化对象的类型。
    • 注意, 并不一定每次调用工厂方法都会创建新的实例。 工厂方法也可以返回缓存、 对象池或其他来源的已有对象
  3. 抽象工厂(Abstract Factory)模式,又称工具箱(Kit 或Toolkit)模式
    • 在工厂方法模式中,其实我们有一个潜在意识的意识。那就是我们生产的都是同一类产品。抽象工厂模式是工厂方法的仅一步深化,在这个模式中的工厂类不单单可以创建一种产品,而是可以创建一组产品。
工厂方法使用场景:
  1. 当你在编写代码的过程中, 如果无法预知对象确切类别及其依赖关系时, 可使用工厂方法。
  2. 如果你希望用户能扩展你软件库或框架的内部组件, 可使用工厂方法。
  3. 如果你希望复用现有对象来节省系统资源, 而不是每次都重新创建对象, 可使用工厂方法。
工厂方法实现方式:
  1. 让所有产品都遵循同一接口。 该接口必须声明对所有产品都有意义的方法。
  2. 在创建类中添加一个空的工厂方法。 该方法的返回类型必须遵循通用的产品接口。
  3. 在创建者代码中找到对于产品构造函数的所有引用。 将它们依次替换为对于工厂方法的调用, 同时将创建产品的代码移入工厂方法。 你可能需要在工厂方法中添加临时参数来控制返回的产品类型。
  4. 现在, 为工厂方法中的每种产品编写一个创建者子类, 然后在子类中重写工厂方法, 并将基本方法中的相关创建代码移动到工厂方法中。
  5. 如果应用中的产品类型太多, 那么为每个产品创建子类并无太大必要, 这时你也可以在子类中复用基类中的控制参数。
  6. 如果代码经过上述移动后, 基础工厂方法中已经没有任何代码, 你可以将其转变为抽象类。 如果基础工厂方法中还有其他语句, 你可以将其设置为该方法的默认行为。
工厂方法优点:
  1. 可以避免创建者和具体产品之间的紧密耦合。
  2. 单一职责原则。 你可以将产品创建代码放在程序的单一位置, 从而使得代码更容易维护。
  3. 开闭原则。 无需更改现有客户端代码, 你就可以在程序中引入新的产品类型。
工厂方法缺点:
  1. 应用工厂方法模式需要引入许多新的子类, 代码可能会因此变得更复杂。 最好的情况是将该模式引入创建者类的现有层次结构中。
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 Shape {
draw(): void;
}

class Circle implements Shape {
draw() {
console.log("draw Circle");
}
}
class Rectangle implements Shape {
draw() {
console.log("draw Rectangle");
}
}

class ShapeFactory {
static getShape(shapeType: string) {
switch (shapeType) {
case "CIRCLE":
return new Circle();
case "RECTANGLE":
return new Rectangle();
default:
return null;
}
}
}

const circle = ShapeFactory.getShape("CIRCLE");
circle?.draw();
const rectangle = ShapeFactory.getShape("RECTANGLE");
rectangle?.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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// 工厂方法

class Button {
render() {
console.log("** Button render **");
}
onClick() {
console.log("** Button onClick **");
}
}

class WindowsButton extends Button {
render() {
console.log("** WindowsButton render **");
}
onClick() {
console.log("** WindowsButton onClick **");
}
}

class HTMLButton extends Button {
render() {
console.log("** HTMLButton render **");
}
onClick() {
console.log("** HTMLButton onClick **");
}
}

class Dialog {
button: Button;
createButton(): Button {
return new Button();
}
render() {
this.button = this.createButton();
this.button.render();
this.button.onClick();
}
}

class WindowsDialog extends Dialog {
createButton(): Button {
return new WindowsButton();
}
}

class WebDialog extends Dialog {
createButton(): Button {
return new HTMLButton();
}
}

class Application {
dialog: Dialog;
initialize(config:string) {// Windows Web
switch (config) {
case "Windows":
this.dialog = new WindowsDialog();
break;
case "Web":
this.dialog = new WebDialog();
break;
default:
throw new Error("UNKNOW");
}
}
render() {
this.dialog.render();
}
}

const app = new Application();
app.initialize('Windows');
app.render();
app.initialize('Web');
app.render();
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
57
58
59
60
61
62
63
64
65
66
67
68
69
// 抽象工厂
interface Gun {
shooting(): void;
}

interface Bullet {
load(): void;
}

interface AbstractFactory {
produceGun(): Gun;
produceBullet(): Bullet;
}

class AK implements Gun {
shooting() {
console.log("shooting with AK");
}
}

class M4A1 implements Gun {
shooting() {
console.log("shooting with M4A1");
}
}

class AKBullet implements Bullet {
load() {
console.log("load bullets with AK");
}
}

class M4A1Bullet implements Bullet {
load() {
console.log("load bullets with M4A1");
}
}

class AKFactory implements AbstractFactory {
produceGun() {
return new AK();
}

produceBullet() {
return new AKBullet();
}
}

class M4A1Factory implements AbstractFactory {
produceGun() {
return new M4A1();
}

produceBullet() {
return new M4A1Bullet();
}
}

const f1 = new AKFactory();
const b1 = f1.produceBullet();
b1.load();
const ak = f1.produceGun();
ak.shooting();
const f2 = new M4A1Factory();
f2.produceGun();
const b2 = f2.produceBullet();
b2.load();
const m4a1 = f1.produceGun();
m4a1.shooting();

Chain Of Resp 责任链模式

责任链按照顺序将请求动态传递给一系列的潜在接收者

使用场景:
  1. 支付逻辑
  2. 中间件
  3. 系列权限检测
  4. dom事件是特殊的责任链模式,一旦某个handler处理了event,就不继续往下传递request了
优点:
  1. 一个对象有多个处理方法,而且可以在运行时决定使用哪些handler
  2. 可以对handler增减个数或改变顺序
  3. 各个handler之间互不影响
缺点:
  1. 因是在运行时确定handler的数量和顺序,单元测试时很难预测结果
  2. 无法保证request一定会被处理
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
interface Next {
exec(products: number[]): number;
}

class ShoppingCart {
products: number[] = [];

addProduct(p: number) {
this.products.push(p);
}
}

class NumberDiscount {
next: Next;

setNext(fn: Next) {
this.next = fn;
}

exec(products: number[]) {
let result = 0;
if (products.length > 3) result = 0.05;

return result + this.next.exec(products);
}
}

class PriceDiscount {
next: Next;

setNext(fn: Next) {
this.next = fn;
}

exec(products: number[]) {
let result = 0;
const total = products.reduce((a, b) => a + b);

if (total >= 500) result = 0.1;

return result + this.next.exec(products);
}
}

class NoneDiscount {
exec() {
return 0;
}
}

class Discount {
calc(products: number[]) {
const ndiscount = new NumberDiscount();
const pdiscount = new PriceDiscount();
const none = new NoneDiscount();
ndiscount.setNext(pdiscount);
pdiscount.setNext(none);
return ndiscount.exec(products);
}
}

const discount = new Discount();

const sc = new ShoppingCart();
sc.addProduct(100);
sc.addProduct(100);
sc.addProduct(100);
console.log(discount.calc(sc.products)); // 0

sc.addProduct(100);
console.log(discount.calc(sc.products)); // 0.05

sc.addProduct(100);
console.log(discount.calc(sc.products)); // 0.15

Composite 组合模式

使用场景:
  1. 当系统的模型可以用tree来描述时可以使用组合模式,通过统一interface遍历所有子节点,不用care子节点是single节点还是composite节点
优点:
  1. 可以利用多态和递归机制更方便地使用复杂树结构
  2. 符合开闭原则。 无需更改现有代码, 你就可以在应用中添加新元素, 使其成为对象树的一部分
缺点:
  1. 对于功能差异较大的类, 提供公共接口或许会有困难。 在特定情况下, 你需要过度一般化组件接口, 使其变得令人难以理解。
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
interface Component {
execute(): number;
}

class Product implements Component {
price = 0;
constructor(price: number) {
this.price = price;
}
execute() {
return this.price;
}
}

class Box implements Component {
children: Component[] = [];

add(c: Component) {
this.children.push(c);
}

execute() {
return this.children.reduce((totalValue, currentCompoent) => {
totalValue += currentCompoent.execute();
return totalValue;
}, 0);
}
}

const phoneBox = new Box();
phoneBox.add(new Product(500));
phoneBox.add(new Product(30));
console.log(phoneBox.execute()); // 530

const penBox = new Box();
penBox.add(new Product(3));
penBox.add(new Product(3));
penBox.add(new Product(3));
console.log(penBox.execute()); // 9

const orderBox = new Box();
orderBox.add(phoneBox);
orderBox.add(penBox);
orderBox.add(new Product(3));
console.log(orderBox.execute()); // 542 = 590 + 9 +3

State 状态模式

使用场景:

一个由一个或多个动态变化的属性导致发生不同行为的对象,在与外部事件产生互动时,其内部状态就会改变,从而使得系统的行为也随之发生变化,那么这个对象,就是有状态的对象

代码中包含大量与对象状态有关的条件语句,像是if else或switch case语句,且这些条件执行与否依赖于该对象的状态。

如果场景符合上面两个条件,那我们就可以想象状态模式是不是可以帮忙了

优点:
  1. 一个状态状态对应行为,封装在一个类里,更直观清晰,增改方便
  2. 状态与状态间,行为与行为间彼此独立互不干扰
  3. 避免事物对象本身不断膨胀,条件判断语句过多
  4. 每次执行动作不用走很多不必要的判断语句,用哪个拿哪个
缺点:
  1. 需要将事物的不同状态以及对应的行为拆分出来,有时候会无法避免的拆分的很细,有的时候涉及业务逻辑,一个动作拆分出对应的两个状态,动作就拆不明白了,过度设计
  2. 必然会增加事物类和动作类的个数,有时候动作类再根据单一原则,按照功能拆成几个类,会反而使得代码混乱,可读性降低
State状态模式之一有限状态机模型一般都具有以下特点:
  1. 状态总数是有限的
  2. 任一时刻,只处在一种状态之中
  3. 某种条件触发后,会从一种状态转变到另一种状态
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
57
interface State {
name: string;
next(): State | void;
}

class LoadedSuccessState implements State {
name = "LoadedSuccess";
next() {
return;
}
}

class LoadedErrorState implements State {
name = "LoadedError";
next() {
return;
}
}

class LoadingState implements State {
name = "Loading";
next() {
const random = Math.random();
if (random >= 0.5) {
return new LoadedSuccessState();
} else {
return new LoadedErrorState();
}
}
}

class StartState implements State {
name = "Start";
next() {
return new LoadingState();
}
}

class Context {
currentState: State | void = new StartState();

nextState() {
if (this.currentState) {
this.currentState = this.currentState.next();
}
}

getState() {
return this.currentState ? this.currentState.name : "";
}
}

const ctx = new Context();
while (ctx.getState()) {
console.log(ctx.getState());
ctx.nextState();
}

Bridge 桥接模式

桥接模式是一种结构型设计模式,桥接模式通过将继承改为组合的方式将类在不同维度上扩展,具体来说, 就是抽取其中一个维度并使之成为独立的类层次, 这样就可以在初始类中引用这个新层次的对象, 从而使得一个类不必拥有所有的状态和行为。

桥接模式通常会于开发前期进行设计, 使你能够将程序的各个部分独立开来以便开发。

使用场景:
  1. 如果你想要拆分或重组一个具有多重功能的庞杂类 (例如能与多个数据库服务器进行交互的类), 可以使用桥接模式。桥接模式可以将庞杂类拆分为几个类层次结构。此后, 你可以修改任意一个类层次结构而不会影响到其他类层次结构。 这种方法可以简化代码的维护工作, 并将修改已有代码的风险降到最低。
  2. 如果你希望在几个独立维度上扩展一个类, 可使用该模式。
  3. 如果你需要在运行时切换不同实现方法, 可使用桥接模式。
优点:
  1. 可以创建与平台无关的类和程序
  2. 客户端代码仅与高层抽象部分进行互动, 不会接触到平台的详细信息
  3. 开闭原则。 你可以新增抽象部分和实现部分, 且它们之间不会相互影响
  4. 单一职责原则。 抽象部分专注于处理高层逻辑, 实现部分处理平台细节
缺点:
  1. 对高内聚的类使用该模式可能会让代码更加复杂
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
interface Device {
isEnable(): boolean;
enable(): void;
disable(): void;
setVolume(percent: number): void;
getVolume(): number;
}

class Remote {
device: Device;
constructor(device: Device) {
this.device = device;
}
togglePower() {
if (this.device.isEnable()) {
this.device.disable();
} else {
this.device.enable();
}
}
volumeUp() {
this.device.setVolume(Math.min(this.device.getVolume() + 2, 100));
}
volumeDown() {
this.device.setVolume(Math.max(this.device.getVolume() - 2, 0));
}
getVolume() {
return this.device.getVolume();
}
}

class Radio implements Device {
enableStatus = false;
volume = 0;

isEnable() {
return this.enableStatus;
}
enable() {
this.enableStatus = true;
}
disable() {
this.enableStatus = false;
}
setVolume(percent: number) {
this.volume = percent;
}
getVolume() {
return this.volume;
}
}

class TV implements Device {
enableStatus = false;
volume = 0;

isEnable() {
return this.enableStatus;
}
enable() {
this.enableStatus = true;
}
disable() {
this.enableStatus = false;
}
setVolume(percent: number) {
this.volume = percent;
}
getVolume() {
return this.volume;
}
}

const radioRemote = new Remote(new Radio());
radioRemote.togglePower();
radioRemote.volumeUp();
radioRemote.volumeUp();
console.log(radioRemote.getVolume());

const tvRemote = new Remote(new TV());
tvRemote.togglePower();
tvRemote.volumeUp();
tvRemote.volumeUp();
tvRemote.volumeDown();
console.log(tvRemote.getVolume());

Strategy 策略模式

策略模式是一种行为设计模式, 它能让你定义一系列算法, 并将每种算法分别放入独立的类中, 以使算法的对象能够相互替换。
名为上下文的原始类必须包含一个成员变量来存储对于每种策略的引用。 上下文并不执行任务, 而是将工作委派给已连接的策略对象。上下文不负责选择符合任务需要的算法——客户端会将所需策略传递给上下文。 实际上, 上下文并不十分了解策略, 它会通过同样的通用接口与所有策略进行交互, 而该接口只需暴露一个方法来触发所选策略中封装的算法即可。
因此, 上下文可独立于具体策略。 这样你就可在不修改上下文代码或其他策略的情况下添加新算法或修改已有算法了。

使用场景:
  1. 当你想使用对象中各种不同的算法变体, 并希望能在运行时切换算法时, 可使用策略模式。
  2. 当你有许多仅在执行某些行为时略有不同的相似类时, 可使用策略模式。将不同行为抽取到一个独立类层次结构中, 并将原始类组合成同一个, 从而减少重复代码。
  3. 如果算法在上下文的逻辑中不是特别重要, 使用该模式能将类的业务逻辑与其算法实现细节隔离开来。
  4. 当类中使用了复杂条件运算符以在同一算法的不同变体中切换时, 可使用该模式。
优点:
  1. 可以在运行时切换对象内的算法
  2. 可以将算法的实现和使用算法的代码隔离开来
  3. 可以使用组合来代替继承
  4. 开闭原则。 你无需对上下文进行修改就能够引入新的策略
缺点:
  1. 如果你的算法极少发生改变, 那么没有任何理由引入新的类和接口。 使用该模式只会让程序过于复杂。
  2. 客户端必须知晓策略间的不同——它需要选择合适的策略。
  3. 许多现代编程语言支持函数类型功能, 允许你在一组匿名函数中实现不同版本的算法。 这样, 你使用这些函数的方式就和使用策略对象时完全相同, 无需借助额外的类和接口来保持代码简洁。
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
interface RouteStrategy {
buildRoute(A: string, B: string): string;
}

class RoadStrategy implements RouteStrategy {
buildRoute(A: string, B: string) {
return A + " to " + B + " : 1 -> 2 -> 3";
}
}

class WalkingStrategy implements RouteStrategy {
buildRoute(A: string, B: string) {
return A + " to " + B + " : 1 -> 4 -> 3";
}
}

class PublicTransportStrategy implements RouteStrategy {
buildRoute(A: string, B: string) {
return A + " to " + B + " : 1 -> 5 -> 3";
}
}

class MapNavigator {
routeStrategy: RouteStrategy;
setRouteStrategy(routeStrategy: RouteStrategy) {
this.routeStrategy = routeStrategy;
}
}

const navi = new MapNavigator();
navi.setRouteStrategy(new RoadStrategy());
console.log(navi.routeStrategy.buildRoute("A", "B"));
navi.setRouteStrategy(new WalkingStrategy());
console.log(navi.routeStrategy.buildRoute("C", "D"));
navi.setRouteStrategy(new PublicTransportStrategy());
console.log(navi.routeStrategy.buildRoute("E", "F"));

Adapter 适配器模式

适配器模式是一种结构型设计模式,适配器模式通常在已有程序中使用,它能使接口不兼容的对象能够相互合作。

适配器能为被封装对象提供不同的接口,适配器模式会试图运用已有的接口。 适配器通常只封装一个对象,

使用场景:
  1. 当你希望使用某个类, 但是其接口与其他代码不兼容时, 可以使用适配器类
  2. 如果需要复用这样一些类, 他们处于同一个继承体系, 并且他们又有了额外的一些共同的方法, 但是这些共同的方法不是所有在这一继承体系中的子类所具有的共性,将缺失功能添加到一个适配器类中是一种优雅得多的解决方案。 然后你可以将缺少功能的对象封装在适配器中, 从而动态地获取所需功能。 如要这一点正常运作, 目标类必须要有通用接口, 适配器的成员变量应当遵循该通用接口。 这种方式同装饰模式非常相似。
优点:
  1. 单一职责原则,你可以将接口或数据转换代码从程序主要业务逻辑中分离。
  2. 开闭原则。 只要客户端代码通过客户端接口与适配器进行交互, 你就能在不修改现有客户端代码的情况下在程序中添加新类型的适配器。
缺点:
  1. 代码整体复杂度增加, 因为你需要新增一系列接口和类。 有时直接更改服务类使其与其他代码兼容会更简单。

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
57
58
59
60
61
62
interface Round {
radius: number;
getRadius(): number;
}

/* 圆钉 */
class RoundPeg implements Round {
radius: number;
constructor(radius: number) {
this.radius = radius;
}
getRadius() {
return this.radius;
}
}
/* 方钉 */
class SquarePeg {
width: number;
constructor(width: number) {
this.width = width;
}
getWidth() {
return this.width;
}
}

/* 圆孔 */
class RoundHole implements Round {
radius: number;
constructor(radius: number) {
this.radius = radius;
}
getRadius() {
return this.radius;
}
fits(peg: Round): boolean {
return peg.getRadius() <= this.radius;
}
}

/* 方钉适配器以适应圆孔 */
class SquarePegAdapter implements Round {
peg: SquarePeg;
radius: number;
constructor(peg: SquarePeg) {
this.peg = peg;
this.radius = (this.peg.getWidth() * Math.sqrt(2)) / 2;
}
getRadius(): number {
return this.radius;
}
}

const roundPeg = new RoundPeg(10);
console.log(roundPeg.getRadius());
const roundHole = new RoundHole(10);
console.log(roundHole.fits(roundPeg));

const squarePeg = new SquarePeg(14);
const squarePegAdapter = new SquarePegAdapter(squarePeg);
console.log(squarePegAdapter.getRadius());
console.log(roundHole.fits(squarePegAdapter));

Decorator 装饰模式

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

装饰模式还支持递归组合

装饰能为对象提供加强的接口

使用场景:
  1. 如果你希望在无需修改代码的情况下即可使用对象, 且希望在运行时为对象新增额外的行为, 可以使用装饰模式
  2. 如果用继承来扩展对象行为的方案难以实现或者根本不可行, 你可以使用该模式
优点:
  1. 无需创建新子类即可扩展对象的行为
  2. 可以在运行时添加或删除对象的功能
  3. 可以用多个装饰封装对象来组合几种行为
  4. 单一职责原则,可以将实现了许多不同行为的一个大类拆分为多个较小的类
缺点:
  1. 在封装器栈中删除特定封装器比较困难
  2. 实现行为不受装饰栈顺序影响的装饰比较困难
  3. 各层的初始化配置代码看上去可能会很糟糕
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
class Notifier {
send(msg: string) {
console.log("Notifier : " + msg);
}
}

class BaseDecorator implements Notifier {
wrapper: Notifier;
constructor(wrapper: Notifier) {
this.wrapper = wrapper;
}
send(msg: string) {
this.wrapper.send(msg);
}
}

class SMSDecorator extends BaseDecorator {
send(msg: string) {
super.send(msg);
console.log("SMSDecorator : " + msg);
}
}

class WechatDecorator extends BaseDecorator {
send(msg: string) {
super.send(msg);
console.log("WechatDecorator : " + msg);
}
}

class QQDecorator extends BaseDecorator {
send(msg: string) {
super.send(msg);
console.log("QQDecorator : " + msg);
}
}

let notifier = new Notifier();
notifier = new SMSDecorator(notifier);
notifier = new WechatDecorator(notifier);
notifier = new QQDecorator(notifier);
notifier.send("HELLO");

/*
'Notifier : HELLO'
'SMSDecorator : HELLO'
'WechatDecorator : HELLO'
'QQDecorator : HELLO'
*/

Proxy 代理模式

代理模式是一种结构型设计模式, 让你能够提供对象的替代品或其占位符。 代理控制着对于原对象的访问, 并允许在将请求提交给对象前后进行一些处理。

代理模式建议新建一个与原服务对象接口相同的代理类, 然后更新应用以将代理对象传递给所有原始对象客户端。 代理类接收到客户端请求后会创建实际的服务对象, 并将所有工作委派给它。

代理模式能为对象提供相同的接口

使用场景:
  1. 延迟初始化 (虚拟代理)。 如果你有一个偶尔使用的重量级服务对象, 一直保持该对象运行会消耗系统资源时, 可使用代理模式。无需在程序启动时就创建该对象, 可将对象的初始化延迟到真正有需要的时候。
  2. 访问控制 (保护代理)。 如果你只希望特定客户端使用服务对象, 这里的对象可以是操作系统中非常重要的部分, 而客户端则是各种已启动的程序 (包括恶意程序), 此时可使用代理模式。代理可仅在客户端凭据满足要求时将请求传递给服务对象。
  3. 本地执行远程服务 (远程代理)。 适用于服务对象位于远程服务器上的情形。在这种情形中, 代理通过网络传递客户端请求, 负责处理所有与网络相关的复杂细节。
  4. 记录日志请求 (日志记录代理)。 适用于当你需要保存对于服务对象的请求历史记录时。 代理可以在向服务传递请求前进行记录。
  5. 缓存请求结果 (缓存代理)。 适用于需要缓存客户请求结果并对缓存生命周期进行管理时, 特别是当返回结果的体积非常大时。
  6. 智能引用。 可在没有客户端使用某个重量级对象时立即销毁该对象。
优点:
  1. 可以在客户端毫无察觉的情况下控制服务对象。
  2. 如果客户端对服务对象的生命周期没有特殊要求, 你可以对生命周期进行管理。
  3. 即使服务对象还未准备好或不存在, 代理也可以正常工作。
  4. 开闭原则。 你可以在不对服务或客户端做出修改的情况下创建新代理。
缺点:
  1. 代码可能会变得复杂, 因为需要新建许多类。
  2. 服务响应可能会延迟。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface DB {
getData(): void;
}

class MySql implements DB {
getData() {
console.log("** getData **");
}
}

class MySqlProxy implements DB {
db: MySql;
constructor(db: MySql) {
this.db = db;
}
getData() {
console.log("** somthing done before **");
this.db.getData();
console.log("** somthing done after **");
}
}

const proxy = new MySqlProxy(new MySql());
proxy.getData();

Facade 外观模式

外观模式是一种结构型设计模式, 能为程序库、 框架或其他复杂类提供一个简单的接口。但是它不提供任何新功能。 子系统本身不会意识到外观的存在。 子系统中的对象可以直接进行交流。

外观模式为现有对象定义了一个新接口,外观通常会作用于整个对象子系统上。

使用场景:
  1. 如果你需要一个指向复杂子系统的直接接口, 且该接口的功能有限, 则可以使用外观模式。子系统通常会随着时间的推进变得越来越复杂。 即便是应用了设计模式, 通常你也会创建更多的类。 尽管在多种情形中子系统可能是更灵活或易于复用的, 但其所需的配置和样板代码数量将会增长得更快。 为了解决这个问题, 外观将会提供指向子系统中最常用功能的快捷方式, 能够满足客户端的大部分需求。
  2. 如果需要将子系统组织为多层结构, 可以使用外观。创建外观来定义子系统中各层次的入口。 你可以要求子系统仅使用外观来进行交互, 以减少子系统之间的耦合。
优点:
  1. 可以让自己的代码独立于复杂子系统。
缺点:
  1. 外观可能成为与程序中所有类都耦合的上帝对象。
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
class ShopFacade {
constructor() {
this.discount = new Discount();
this.shipping = new Shipping();
this.fees = new Fees();
}

calc(price) {
price = this.discount.calc(price);
price = this.fees.calc(price);
price += this.shipping.calc();
return price;
}
}

class Discount {

calc(value) {
return value * 0.9;
}
}

class Shipping {
calc() {
return 5;
}
}

class Fees {

calc(value) {
return value * 1.05;
}
}

Mediator 中介者模式

中介者模式是一种行为设计模式, 能让你减少对象之间混乱无序的依赖关系。 该模式会限制对象之间的直接交互, 迫使它们通过一个中介者对象进行合作。

使用场景:
  1. 当一些对象和其他对象紧密耦合以致难以对其进行修改时, 可使用中介者模式。
  2. 当组件因过于依赖其他组件而无法在不同应用中复用时, 可使用中介者模式。
  3. 如果为了能在不同情景下复用一些基本行为, 导致你需要被迫创建大量组件子类时, 可使用中介者模式。由于所有组件间关系都被包含在中介者中, 因此你无需修改组件就能方便地新建中介者类以定义新的组件合作方式。
优点:
  1. 单一职责原则。 你可以将多个组件间的交流抽取到同一位置, 使其更易于理解和维护
  2. 开闭原则。 你无需修改实际组件就能增加新的中介者
  3. 可以减轻应用中多个组件间的耦合情况
  4. 可以更方便地复用各个组件
缺点:
  1. 一段时间后, 中介者可能会演化成为上帝对象
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
class TrafficTower {
airplanes: Airplane[] = [];

requestPositions() {
return this.airplanes.map(airplane => {
return airplane.position;
});
}
}

class Airplane {
position: number;
trafficTower: TrafficTower;
constructor(position: number, trafficTower: TrafficTower) {
this.position = position;
this.trafficTower = trafficTower;
this.trafficTower.airplanes.push(this);
}

requestPositions() {
return this.trafficTower.requestPositions();
}
}

const trafficTower = new TrafficTower();
const boeing1 = new Airplane(10, trafficTower);
const boeing2 = new Airplane(15, trafficTower);
const boeing3 = new Airplane(55, trafficTower);
boeing1.requestPositions(); // [10, 15, 55]

Observer 观察者模式

观察者模式是一种行为设计模式, 允许你定义一种订阅机制, 可在对象事件发生时通知多个 “观察” 该对象的其他对象。

使用场景:
  1. 当一个对象状态的改变需要改变其他对象, 或实际对象是事先未知的或动态变化的时, 可使用观察者模式
  2. 当应用中的一些对象必须观察其他对象时, 可使用该模式。 但仅能在有限时间内或特定情况下使用
优点:
  1. 开闭原则。 你无需修改发布者代码就能引入新的订阅者类 (如果是发布者接口则可轻松引入发布者类)
  2. 可以在运行时建立对象之间的联系
缺点:
  1. 订阅者的通知顺序是随机的
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
interface Subscriber {
update(ctx: Publisher): void;
}

class Publisher {
state: string[] = [];
subscribers: Subscriber[] = [];
subscribe(s: Subscriber) {
if (!this.subscribers.find(subscribe => subscribe === s)) {
this.subscribers.push(s);
}
}
unsubscribe(s: Subscriber) {
this.subscribers = this.subscribers.filter(subscribe => subscribe !== s);
}
notify() {
this.subscribers.forEach(s => s.update(this));
}
stateChange() {
const newState = Math.random();
this.state.push(newState.toFixed(2));
this.notify();
}
}

class Subscriber1 implements Subscriber {
update(ctx: Publisher) {
console.log("Subscriber1 get : " + ctx.state);
}
}

class Subscriber2 implements Subscriber {
update(ctx: Publisher) {
console.log("Subscriber2 get : " + ctx.state);
}
}

const publisher = new Publisher();
const s1 = new Subscriber1();
publisher.subscribe(s1);
const s2 = new Subscriber2();
publisher.subscribe(s2);
publisher.stateChange();
publisher.unsubscribe(s2);
publisher.stateChange();

Command 命令模式

命令模式是一种行为设计模式, 它可将请求转换为一个包含与请求相关的所有信息的独立对象。 该转换让你能根据不同的请求将方法参数化、 延迟请求执行或将其放入队列中, 且能实现可撤销操作。

命令模式建议将请求的所有细节 (例如调用的对象、 方法名称和参数列表) 抽取出来组成命令类, 该类中仅包含一个用于触发请求的方法。

使用场景:
  1. 如果你需要通过操作来参数化对象, 可使用命令模式。命令模式可将特定的方法调用转化为独立对象。 这一改变也带来了许多有趣的应用: 你可以将命令作为方法的参数进行传递、 将命令保存在其他对象中, 或者在运行时切换已连接的命令等。
  2. 如果你想要将操作放入队列中、 操作的执行或者远程执行操作, 可使用命令模式。同其他对象一样, 命令也可以实现序列化 (序列化的意思是转化为字符串), 从而能方便地写入文件或数据库中。 一段时间后, 该字符串可被恢复成为最初的命令对象。 因此, 你可以延迟或计划命令的执行。 但其功能远不止如此! 使用同样的方式, 你还可以将命令放入队列、 记录命令或者通过网络发送命令。
  3. 如果你想要实现操作回滚功能, 可使用命令模式。
优点:
  1. 单一职责原则。 你可以解耦触发和执行操作的类。
  2. 开闭原则。 你可以在不修改已有客户端代码的情况下在程序中创建新的命令。
  3. 可以实现撤销和恢复功能。
  4. 可以实现操作的延迟执行。
  5. 可以将一组简单命令组合成一个复杂命令。
缺点:
  1. 代码可能会变得更加复杂, 因为你在发送者和接收者之间增加了一个全新的层次。
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
57
58
59
60
61
62
63
64
65
66
67
68
interface Command {
/* execute 无参数*/
execute(): void;
}

/** 具体命令 执行:委托业务类操作*/
class OnCommand implements Command {
turbine: Turbine;
argumentState = false;
constructor(turbine: Turbine, argumentState: boolean) {
this.turbine = turbine;
this.argumentState = argumentState;
}

execute() {
this.turbine.setState(this.argumentState);
}
}
class OffCommand implements Command {
turbine: Turbine;
argumentState = false;
constructor(turbine: Turbine, argumentState: boolean) {
this.turbine = turbine;
this.argumentState = argumentState;
}

execute() {
this.turbine.setState(this.argumentState);
}
}

/** 接收者:业务类 */
class Turbine {
state = false;
constructor() {
this.state = false;
}

setState(state: boolean) {
this.state = state;
console.log(this.state);
}
}

/** 发送者 */
class Cockpit {
command: Command;
constructor(command: Command) {
this.setCommand(command);
}

setCommand(command: Command) {
this.command = command;
}

executeCommand() {
this.command.execute();
}
}

/** 客户端 创建并配置具体命令*/
const turbine = new Turbine();
const onCommand = new OnCommand(turbine, true);
const cockpit = new Cockpit(onCommand);
cockpit.executeCommand();
const offCommand = new OffCommand(turbine, false);
cockpit.setCommand(offCommand);
cockpit.executeCommand();

Visitor 访问者模式

访问者模式是一种行为设计模式, 它能将算法与其所作用的对象隔离开来。

访问者模式建议将新行为放入一个名为访问者的独立类中, 而不是试图将其整合到已有类中。

使用场景:
  1. 如果你需要对一个复杂对象结构 (例如对象树) 中的所有元素执行某些操作, 可使用访问者模式。
  2. 可使用访问者模式来清理辅助行为的业务逻辑。该模式会将所有非主要的行为抽取到一组访问者类中, 使得程序的主要类能更专注于主要的工作。
  3. 当某个行为仅在类层次结构中的一些类中有意义, 而在其他类中没有意义时, 可使用该模式。
优点:
  1. 开闭原则。 你可以引入在不同类对象上执行的新行为, 且无需对这些类做出修改。
  2. 单一职责原则。 可将同一行为的不同版本移到同一个类中。
  3. 访问者对象可以在与各种对象交互时收集一些有用的信息。 当你想要遍历一些复杂的对象结构 (例如对象树), 并在结构中的每个对象上应用访问者时, 这些信息可能会有所帮助。
缺点:
  1. 每次在元素层次结构中添加或移除一个类时, 你都要更新所有的访问者。
  2. 在访问者同某个元素进行交互时, 它们可能没有访问元素私有成员变量和方法的必要权限。
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
interface Visitor {
visit(e: VisitorTarget): void;
}
interface VisitorTarget {
accept(v: Visitor): void;
}

class Employee implements VisitorTarget {
bonus = 0;
salary = 0;
constructor(salary: number) {
this.salary = salary;
}

accept(visitor: Visitor) {
visitor.visit(this);
}
}

class Manager extends Employee {
constructor(salary: number) {
super(salary);
}
}

class Developer extends Employee {
constructor(salary: number) {
super(salary);
}
}

class BonusVisitor implements Visitor {
visit(e: VisitorTarget) {
if (e instanceof Manager) {
e.bonus = e.salary * 2;
}
if (e instanceof Developer) {
e.bonus = e.salary;
}
}
}

const employees = [];
const john = new Developer(4000);
const maria = new Developer(4000);
const christian = new Manager(10000);
employees.push(john);
employees.push(maria);
employees.push(christian);

const bonusVisitor = new BonusVisitor();
employees.forEach(e => {
e.accept(bonusVisitor);
console.log(e.bonus);
});

Iterator 迭代器模式

迭代器模式是一种行为设计模式, 让你能在不暴露集合底层表现形式 (列表、 栈和树等) 的情况下遍历集合中所有的元素。

使用场景:
  1. 当集合背后为复杂的数据结构, 且你希望对客户端隐藏其复杂性时 (出于使用便利性或安全性的考虑), 可以使用迭代器模式。
  2. 使用该模式可以减少程序中重复的遍历代码。
  3. 如果你希望代码能够遍历不同的甚至是无法预知的数据结构, 可以使用迭代器模式。
优点:
  1. 单一职责原则。 通过将体积庞大的遍历算法代码抽取为独立的类, 你可对客户端代码和集合进行整理。
  2. 开闭原则。 你可实现新型的集合和迭代器并将其传递给现有代码, 无需修改现有代码。
  3. 你可以并行遍历同一集合, 因为每个迭代器对象都包含其自身的遍历状态。
  4. 相似的, 你可以暂停遍历并在需要时继续。
缺点:
  1. 如果你的程序只与简单的集合进行交互, 应用该模式可能会矫枉过正。
  2. 对于某些特殊集合, 使用迭代器可能比直接遍历的效率低。
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
interface CIterator {
next(): number;
hasNext(): boolean;
}

interface CIterableCollection {
createIterator(): CIterator;
}

class CArray implements CIterableCollection {
elements: number[] = [];

constructor(elements: number[]) {
this.elements = elements;
}

createIterator() {
let index = 0
return {
next: () => {
return this.elements[index++];
},
hasNext: () => {
return index < this.elements.length;
}
};
}
}

const arr = new CArray([1, 2, 3, 4, 5]);
const iterator = arr.createIterator();
console.log(iterator.next());

console.log('******');

const iterator2= arr.createIterator();
while (iterator2.hasNext()) {
console.log(iterator2.next());
}

Memento 备忘录模式

备忘录模式是一种行为设计模式, 允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态。

备忘录模式将创建状态快照 (Snapshot) 的工作委派给实际状态的拥有者原发器 (Originator) 对象。 这样其他对象就不再需要从 “外部” 复制编辑器状态了, 编辑器类拥有其状态的完全访问权, 因此可以自行生成快照。

模式建议将对象状态的副本存储在一个名为备忘录 (Memento) 的特殊对象中。 除了创建备忘录的对象外, 任何对象都不能访问备忘录的内容。 其他对象必须使用受限接口与备忘录进行交互, 它们可以获取快照的元数据 (创建时间和操作名称等), 但不能获取快照中原始对象的状态。

这种限制策略允许你将备忘录保存在通常被称为负责人 (Caretakers) 的对象中。 由于负责人仅通过受限接口与备忘录互动, 故其无法修改存储在备忘录内部的状态。 同时, 原发器拥有对备忘录所有成员的访问权限, 从而能随时恢复其以前的状态。

使用场景:
  1. 当你需要创建对象状态快照来恢复其之前的状态时, 可以使用备忘录模式。
  2. 当直接访问对象的成员变量、 获取器或设置器将导致封装被突破时, 可以使用该模式。备忘录让对象自行负责创建其状态的快照。 任何其他对象都不能读取快照, 这有效地保障了数据的安全性。
优点:
  1. 可以在不破坏对象封装情况的前提下创建对象状态快照。
  2. 可以通过让负责人维护原发器状态历史记录来简化原发器代码。
缺点:
  1. 如果客户端过于频繁地创建备忘录, 程序将消耗大量内存。
  2. 负责人必须完整跟踪原发器的生命周期, 这样才能销毁弃用的备忘录。
  3. 绝大部分动态编程语言 (例如 PHP、 Python 和 JavaScript) 不能确保备忘录中的状态不被修改。
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
interface MementoState {
from: number;
to: number;
}

class Memento {
state: MementoState;
constructor(state: MementoState) {
this.state = state;
}
getState() {
return this.state;
}
}

class Originator {
state: MementoState = { from: 0, to: 1 };

store() {
return new Memento(this.state);
}

restore(m: Memento | undefined) {
if (!m) return;
this.state = m.getState();
}
}

class Caretaker {
originator: Originator;
history: Memento[] = [];

constructor(originator: Originator) {
this.originator = originator;
this.save();
}

save() {
const m = this.originator.store();
this.history.push(m);
}

doSomething() {
this.originator.state = {
from: Math.floor(Math.random() * 100),
to: Math.floor(Math.random() * 100)
};
this.save();
}

undo() {
const m = this.history.pop();
this.originator.restore(m);
}
}

const c = new Caretaker(new Originator());
console.log("origin", c.originator.state);
c.doSomething();
console.log("save", c.originator.state);
c.doSomething();
console.log("save", c.originator.state);
c.undo();
console.log("undo", c.originator.state);
c.undo();
console.log("undo", c.originator.state);
c.undo();
console.log("undo", c.originator.state);
c.undo();
console.log("undo", c.originator.state);
命令模式 + 备忘录模式
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
interface MementoState {
from: number;
to: number;
}

class Memento {
state: MementoState;
constructor(state: MementoState) {
this.state = state;
}
getState() {
return this.state;
}
}

class Originator {
state: MementoState = { from: 0, to: 1 };

store() {
return new Memento(this.state);
}

restore(m: Memento | undefined) {
if (!m) return;
this.state = m.getState();
}
}

interface Command {
execute(): void;
}

class SaveCommand implements Command {
c: Caretaker;
constructor(c: Caretaker) {
this.c = c;
}
execute() {
const m = this.c.originator.store();
this.c.history.push(m);
}
}

class UndoCommand implements Command {
c: Caretaker;
constructor(c: Caretaker) {
this.c = c;
}
execute() {
const m = this.c.history.pop();
this.c.originator.restore(m);
}
}

class Caretaker {
originator: Originator;
history: Memento[] = [];

constructor(originator: Originator) {
this.originator = originator;
}

doSomething() {
this.originator.state = {
from: Math.floor(Math.random() * 100),
to: Math.floor(Math.random() * 100)
};
}

executeCommand(command: Command) {
command.execute();
}
}

const c = new Caretaker(new Originator());
const saveCommand = new SaveCommand(c);
const undoCommand = new UndoCommand(c);
c.executeCommand(saveCommand);
console.log("origin", c.originator.state);
c.doSomething();
c.executeCommand(saveCommand);
console.log("save", c.originator.state);
c.doSomething();
c.executeCommand(saveCommand);
console.log("save", c.originator.state);
c.executeCommand(undoCommand);
console.log("undo", c.originator.state);
c.executeCommand(undoCommand);
console.log("undo", c.originator.state);
c.executeCommand(undoCommand);
console.log("undo", c.originator.state);
c.executeCommand(undoCommand);
console.log("undo", c.originator.state);

Prototype 原型模式

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

原型模式将克隆过程委派给被克隆的实际对象。 模式为所有支持克隆的对象声明了一个通用接口, 该接口让你能够克隆对象, 同时又无需将代码和对象所属类耦合。 通常情况下, 这样的接口中仅包含一个 克隆方法。

使用场景:
  1. 如果你需要复制一些对象, 同时又希望代码独立于这些对象所属的具体类, 可以使用原型模式。
  2. 如果子类的区别仅在于其对象的初始化方式, 那么你可以使用该模式来减少子类的数量。 别人创建这些子类的目的可能是为了创建特定类型的对象。
优点:
  1. 可以克隆对象, 而无需与它们所属的具体类相耦合。
  2. 可以克隆预生成原型, 避免反复运行初始化代码。
  3. 可以更方便地生成复杂对象。
  4. 可以用继承以外的方式来处理复杂对象的不同配置。
缺点:
  1. 克隆包含循环引用的复杂对象可能会非常麻烦。
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
interface Prototype {
clone(): Prototype;
}

class Sheep implements Prototype {
name: string;
tags: number[];

constructor(name: string, tags: number[]) {
this.name = name;
this.tags = tags;
}

clone() {
return new Sheep(this.name, this.tags);
}
}

const sheep = new Sheep("dolly", [1, 2, 3]);
const dolly = sheep.clone();
console.log(dolly.name === sheep.name); // true
console.log(dolly.tags === sheep.tags); // true
sheep.tags.push(4);
console.log(dolly.tags) // [1, 2, 3, 4]
sheep.tags = [];
console.log(dolly.tags) // [1, 2, 3, 4]
console.log(dolly instanceof Sheep); // true
console.log(dolly === sheep); // false

Builder 生成器模式

生成器模式是一种创建型设计模式, 使你能够分步骤创建复杂对象。 该模式允许你使用相同的创建代码生成不同类型和形式的对象。

生成器模式建议将对象构造代码从产品类中抽取出来, 并将其放在一个名为生成器的独立对象中。

该模式会将对象构造过程划分为一组步骤。 每次创建对象时, 你都需要通过生成器对象执行一系列步骤。 重点在于你无需调用所有步骤, 而只需调用创建特定对象配置所需的那些步骤即可。

当你需要创建不同形式的产品时, 其中的一些构造步骤可能需要不同的实现。在这种情况下, 你可以创建多个不同的生成器, 用不同方式实现一组相同的创建步骤。 然后你就可以在创建过程中使用这些生成器 (例如按顺序调用多个构造步骤) 来生成不同类型的对象。

基本生成器接口中定义了所有可能的制造步骤, 具体生成器将实现这些步骤来制造特定形式的产品。 同时, 主管类将负责管理制造步骤的顺序。

使用场景:
  1. 使用生成器模式可避免 “重叠构造函数 (telescopic constructor)” 的出现。
  2. 当你希望使用代码创建不同形式的产品 (例如石头或木头房屋) 时, 可使用生成器模式。
  3. 使用生成器构造组合树或其他复杂对象。
优点:
  1. 可以分步创建对象, 暂缓创建步骤或递归运行创建步骤。
  2. 生成不同形式的产品时, 你可以复用相同的制造代码。
  3. 单一职责原则。 你可以将复杂构造代码从产品的业务逻辑中分离出来。
缺点:
  1. 由于该模式需要新增多个类, 因此代码整体复杂程度会有所增加。
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
class HTTPRequest {
url = "";
method = "";
payload: {} = {};
}

class RequestBuilder {
request: HTTPRequest;
constructor() {
this.request = new HTTPRequest();
}

forUrl(url: string) {
this.request.url = url;
return this;
}

useMethod(method: string) {
this.request.method = method;
return this;
}

payload(payload: {}) {
this.request.payload = payload;
return this;
}

build() {
return this.request;
}
}

const requestBuilder = new RequestBuilder();
const url = "http://something/users";
const method = "GET";
const request = requestBuilder
.forUrl(url)
.useMethod(method)
.payload({ a: 1 })
.build();

Flyweight 享元模式

享元模式是一种结构型设计模式, 它摒弃了在每个对象中保存所有数据的方式, 通过共享多个对象所共有的相同状态, 让你能在有限的内存容量中载入更多对象。

使用场景:
  1. 仅在程序必须支持大量对象且没有足够的内存容量时使用享元模式。
优点:
  1. 如果程序中有很多相似对象, 那么你将可以节省大量内存。
缺点:
  1. 可能需要牺牲执行速度来换取内存, 因为他人每次调用享元方法时都需要重新计算部分情景数据。
  2. 代码会变得更加复杂。 团队中的新成员总是会问: ​ “为什么要像这样拆分一个实体的状态?”。
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
class Color {
name: string;
constructor(name: string) {
this.name = name;
}
}

class ColorFactory {
colors: { [prop: string]: Color };
constructor() {
this.colors = {};
}
create(name: string) {
const color = this.colors[name];
if (color) return color;
this.colors[name] = new Color(name);
return this.colors[name];
}
}

const cf = new ColorFactory();
cf.create("RED");
cf.create("RED");
cf.create("RED");
cf.create("YELLOW");
cf.create("YELLOW");
console.log(cf.colors); // { RED: Color { name: 'RED' }, YELLOW: Color { name: 'YELLOW' } }

Template Method 模板方法模式

模板方法模式是一种行为设计模式, 它在超类中定义了一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤。

模板方法模式建议将算法分解为一系列步骤, 然后将这些步骤改写为方法, 最后在 “模板方法” 中依次调用这些方法。 步骤可以是 抽象的, 也可以有一些默认的实现。 为了能够使用算法, 客户端需要自行提供子类并实现所有的抽象步骤。 如有必要还需重写一些步骤 (但这一步中不包括模板方法自身)。

使用场景:
  1. 当你只希望客户端扩展某个特定算法步骤, 而不是整个算法或其结构时, 可使用模板方法模式。
  2. 当多个类的算法除一些细微不同之外几乎完全一样时, 你可使用该模式。 但其后果就是, 只要算法发生变化, 你就可能需要修改所有的类。
优点:
  1. 可仅允许客户端重写一个大型算法中的特定部分, 使得算法其他部分修改对其所造成的影响减小。
  2. 可将重复代码提取到一个超类中。
缺点:
  1. 部分客户端可能会受到算法框架的限制。
  2. 通过子类抑制默认步骤实现可能会导致违反里氏替换原则
  3. 模板方法中的步骤越多, 其维护工作就可能会越困难。
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
class Tax {
value: number;
calc(value: number) {
if (value >= 1000) value = this.overThousand(value);

return this.complementaryFee(value);
}

complementaryFee(value: number) {
return value + 10;
}

overThousand(value: number) {
return value;
}
}

class Tax1 extends Tax {
constructor() {
super();
}

overThousand(value: number) {
return value * 1.1;
}
}

class Tax2 extends Tax {
constructor() {
super();
}

overThousand(value: number) {
return value * 1.2;
}
}

const tax1 = new Tax1();
const tax2 = new Tax2();

console.log(tax1.calc(1000)); // 1110
console.log(tax2.calc(1000)); // 1210
console.log(tax2.calc(100)); // 110
References:
  1. https://addyosmani.com/resources/essentialjsdesignpatterns/book/#introduction
  2. http://www.code4human.com/state-pattern-javascript-example/
  3. https://segmentfault.com/a/1190000015050674
  4. https://refactoring.guru/design-patterns/