# 设计模式解析

  1. 表单策略模式应用 (opens new window)

设计模式代表了最佳实践,通常被有经验的面向对象的软件开发人员所采用。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。

  • 有利于组织模块
  • 有利于设计沟通
  • 有利于代码质量

# 1、五大设计原则

# 1、开闭原则

对扩展开放,对修改关闭

  • 解析:要给扩展的接口,具体使用时无需修改源码即可扩展功能。

  • 应用:将通用方法提取成基类,通过继承基类,重写方法实现修改方法,而不是修改存量代码实现。

# 2、单一原则

模块只做一件事,模块的职责越单一越好。

//原功能:弹框和计算金额两个模块耦合
class PUBGManager {
  openDialog() {   // 弹框
    setPrice();    // 计算金额
  }
}
const game = new PUBGManager();
game.openDialog();

// 功能重构:弹框和计算金额解耦合
// 底层库
class PriceManager {
  setPrice(price) {
    // 配置金额……
  }
}

// 业务逻辑
class PUBGManager {
  constructor(command) {
    this.command = command;
  }
  openDialog(price) {
    this.command.setPrice(price); // 计算金额
  }
}

// main.js
const exe = new PriceManager();
const game = new PUBGManager(exe);
game.openDialog(15);
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

# 3、依赖倒置原则

上层不应依赖下层实现:上层模块依赖于下层接口抽象,不依赖于下层接口实现,接口定义不变,上层不受影响

//原功能1:Store中使用分享功能
class Store {
  constructor() {
    this.share = new Share();
  }
}
class Share {
  shareTo() {
    // 分享到不同平台
  }
}
const store = new Store();
store.share.shareTo('wx');

// 原功能2:添加评分功能,需修改constructor
class Share {
  shareTo() {
    // 分享到不同平台
  }
}
class Rate {
  star(stars) {
    // 评分
  }
}
class Store {
  constructor() {
    this.share = new Share();
    this.rate = new Rate();
  }
}
const store = new Store();
store.rate.stars('5');


// 重构:暴露挂载 => 动态挂载 
class Rate {
  init(store) {
    store.rate = this;
  }
  store(stars) {
    // 评分
  }
}
class Share {
  init(store) {
    store.share = this;
  }
  shareTo() {
    // 分享到不同平台
  }
}

class Store {
  // 维护模块名单
  static modules = new Map();
  constructor() {
    // 遍历名单做初始化挂载
    for (let module of Store.modules.values()) {
      module.init(this);
    }
  }
  // 注入功能模块
  static inject(module) {
    Store.modules.set(module.constructor.name, module);
  }
}


// 依次注册完所有模块
const rate = new Rate();
Store.inject(rate);

// 初始化商城
const store = new Store();
store.rate.star(4);
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

# 4、接口隔离原则

接口要细化,功能要单一,一个接口不要调用太多方法,使其能力单一

解析:多个专业接口比一个胖接口好用

//原功能:胖接口
class Game {
  constructor(name) {
    this.name = name;
  }
  run() {
    // 跑
  }
  shot() {
    // 开枪
  }
  mega() {
    // 开大
  }
}

class PUGB extends Game {
  constructor() {
    // pubg constructor
  }
}
pubg = new PUBG('pubg');
pubg.run();
pubg.shot();
pubg.mega();

// 重构功能:用多个接口替代他,每个接口服务于一个子模块
class Game {
  constructor(name) {
    this.name = name;
  }
  run() {
    // 跑
  }
}

class PUGB extends Game {
  constructor() {
    // pubg constructor
  }
  shot() {}
}

class LOL extends Game {
  constructor() {
    // lol constructor
  }
  mega() {}
}
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

# 5、里氏替换原则

子类可以扩展父类,不能改变父类

  • 解析:它主要关注于继承,它的意义是任何使用父类的地方都可以用子类去替换,直白的说我们子类继承父类的时候,我们的子类必须完全保证继承父类的属性和方法,这样的话父类使用的地方,子类可以进行替换
//原功能:sprint 1
class Game {
  start() {
    // 开机逻辑
  }
  shutdown() {
    // 关机
  }
  play() {
    // 游戏
  }
}
const game = new Game();
game.play();

//原功能:sprint 2
class MobileGame extends Game {
  tombStone() {
    // tombStone
  }
  play() { //重写play方法
    // 移动端游戏
  }
}
const mobile = new MobileGame();
mobile.play();

//重构功能:父类只包含公共方法,子类做扩展,不做重写
class Game {
  start() {
    // 开机逻辑
    console.log('start');
  }
  shutdown() {
    // 关机
    console.log('shutdown');
  }
}

class MobileGame extends Game {
  tombStone() {
    console.log('tombStone');
  }
  play() {
    console.log('playMobileGame');
  }
}

class PC extends Game {
  speed() {
    console.log('speed');
  }
  play() {
    console.log('playPCGAME');
  }
}
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

# 2、设计模式分类

# 1、创建型

  • 功能:创建元素

  • 目的:规范创建步骤

# 1、模式解析

# 1、工厂模式

生产同类型商品,隐藏创建过程、暴露共同接口

# 2、建造者模式

独立生产商品,每个模块独立解耦,而建造者负责创建串联正题系统

// 拆分简单模块、独立执行 => 注重过程与搭配
// 需求:优惠套餐单元,商品 + 皮肤 进行打折售卖
class Product {
  constructor(name) {
    this.name = name;
  }
  init() {
    console.log('Product init');
  }
}

class Skin {
  constructor(name) {
    this.name = name;
  }
  init() {
    console.log('Skin init');
  }
}

class Shop {
  constructor() {
    this.package = '';
  }
  create(name) {
    this.package = new PackageBuilder(name);
  }
  getGamePackage() {
    return this.package.getPackage();
  }
}

// 建造者模式:将皮肤和商品组合
class PackageBuilder {
  constructor(name) {
    this.game = new Product(name);
    this.skin = new Skin(name);
  }
  getPackage() {
    return this.game.init() + this.skin.init();
  }
}
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
# 3、单例模式

全局只有一个实例

class PlayStation {
  static instance = undefined;

  constructor() {
    this.state = 'off';
  }

  static getInstance() {
     return function() {
       if(!PlayStation.instance) {
         PlayStation.instance = new PlayStation();
       }
       return PlayStation.instance;
     }();
  }

  play() {
    if (this.state === 'on') {
      console.log('别闹,已经在happy了');
      return;
    }
    this.state = 'on';
    console.log('开始happy');
  }
  shutdown() {
    if (this.state === 'off') {
      console.log('已经关闭');
      return;
    }
    this.state = 'off';
    console.log('已经关机,请放心');
  }
  
}
const ps1 = PlayStation.getInstance();
ps1.play();

const ps2 = PlayStation.getInstance();
ps2.shutdown();
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

# 2、使用场景

  • 工厂模式:批量生产同类型应用来满足频繁使用同一种类型需求
  • 建造者模式:当我们需要模块化拆分一个大模块,同时使模块间独立解耦分工
  • 单例模式:全局只需要一个实例,注重统一一体化

# 3、实际应用

  • 工厂模式:Button Producer,生产不同类型的按钮 => 生产多个本质相同,利用传参区分不同属性的元素

  • 建造者模式:页头组件Header,包含了title、button、breadcum => 生产多重不同类型的元素

  • 单例模式:全局应用 router store => 只需要一个实例

# 2、结构型

功能: 优化结构的实现方式

# 1、模式解析

# 1、适配器模式

适配已有模块,保证模块间的独立解耦且连接兼容

// 需求: 买了一个港行PS,插座是国标
class HKDevice {
  getPlug() {
    return '港行双圆柱插头';
  }
}
//适配器类
class Target {
  constructor() {
    this.plug = new HKDevice();
  }
  getPlug() {
    return this.plug.getPlug() + '+港行双圆柱转换器';
  }
}

const target = new Target();
target.getPlug();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 2、装饰器模式

增强已有模块,动态将责任附加到对象上

// 设备升级
class Device {
  create() {
    console.log('PlayStation4');
  }
}
class Phone {
  create() {
    console.log('iphone18');
  }
}

//装饰器类:增加update功能
class Decorator {
  constructor(device) {
    this.device = device;
  }
  create() {
    this.device.create();
    this.update(device);
  }
  update(device) {
    console.log(device + 'pro');
  }
}

const device = new Device();
device.create();
const newDevice = new Decorator(device);
newDevice.create();
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
# 3、代理模式

使用代理人来替代原始对象,集约流程

// 游戏防沉迷
class Game {
  play() {
    return "playing";
  }
}

class Player {
  constructor(age) {
    this.age = age;
  }
}

//代理类,添加年龄控制
class GameProxy {
  constructor(player) {
    this.player = player;
  }
  play() {
    return (this.player.age < 16) ? "too young to play" : new Game().play();
  }
}

const player = new Player(18);
const game = new GameProxy(player);
game.play();
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

# 2、使用场景

  • 适配器模式:中间转换参数、保持模块间独立的时候

  • 装饰器模式:附着于多个组件上,批量动态赋予功能的时候

  • 代理模式:将代理对象与调用对象分离,不直接调用目标对象

# 3、实际应用

  • 适配器模式:两个模块为筛选器和表格,需要做一个联动,但筛选器的数据不能直接传入表格,需要做数据结构转换 => 模块之间独立,需要做数据结构转换

  • 装饰器模式:目前有按钮、title、icon三个组件,希望开发一个模块,让三个组件同时具备相同功能 => 套一层装甲对于每个组件有统一的能力提升,且可以动态添加功能进行拓展

  • 代理模式:ul中多个li,每个li上的点击事件 => 利用冒泡做委托,事件绑定在ul上

# 3、行为型

功能:模块行为总结

目的:不同的对象之间划分责任和算法的抽象化

# 1、模式解析

# 1、命令模式

包裹传输命令,请求以命令的形式包裹在对象中,并传给调用对象

// 接受者
class Receiver {
  execute() {
    console.log('角色开始奔跑');
  }
}

// 指令器
class Command {
  constructor(receiver) {
    this.receiver = receiver;
  }
  execute() {
    console.log('执行命令');
    this.receiver.execute();
  }
}

// 触发者:命令模式类,包裹命令,链接Receiver和Command
class Operator {
  constructor(command) {
    this.command = command;
  }
  run() {
    console.log('请给我爬');
    this.command.execute();
  }
}

const soldier = new Receiver();
const order = new Command(soldier);
const player = new Operator(order);
player.run();
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
# 2、模版模式

重编排、易拓展,在模板中,定义好每个方法的执行步骤,方法本身关注于自己的事情

// 想要成功吃个鸡,大概分几步
class Device {
  constructor(executePipeLine) {
    // executePipeLine……
  }
  powerOn() {
    console.log('打开电源');
  }
  login() {
    console.log('登录账号');
  }
  clickIcon() {
    console.log('点击开始游戏');
  }
  enterGame() {
    console.log('进入战场');
  }

  play() {
    //根据executePipeLine模版入参,编辑模版,组合调用上述方法
    this.powerOn();
    this.login();
    this.clickIcon();
    this.enterGame();
  }
}
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
# 3、观察者模式

也称发布订阅模式,模块间实时互通,当一个属性发生状态改变时,观察者会连续引发所有的相关状态改变

// 通过智能家居一键开始游戏
class MediaCenter {
  constructor() {
    this.state = '';
    this.observers = [];
  }
  attach(observer) {
    this.observers.push(observer);
  }
  getState() {
    return this.state;
  }
  setState(state) {
    this.state = state;
    this.notifyAllobservers();
  }
  notifyAllobservers() {
    this.observers.forEach(ob => {
      ob.update();
    })
  }
}

class observer {
  constructor(name, center) {
    this.name = name;
    this.center = center;
    this.center.attach(this);
  }
  update() {
    console.log(`${this.name} update, state: ${this.center.getState()}`);
  }
}

const center = new MediaCenter();
const ps = new Observer('ps', center);
const tv = new Observer('tv', center);

center.setState('on');
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
# 4、责任联模式

单项链执行,特点:1. 链式调用、2. 职责独立、3. 顺序执行

// 成年高质量男性想要打个游戏,在家里需要过几关
class Action {
  constructor(name) {
    this.name = name;
    this.nextAction = null;
  }
  setNextAction(action) {
    this.nextAction = action;
  }
  handle() {
    console.log(`${this.name}请审批,是否可以打游戏`);
    if (this.nextAction !== null) {
      this.nextAction.handle();
    }
  }
}

const dad = new Action('爸');
const mom = new Action('妈');
const wife = new Action('夫人');
dad.setNextAction(mom);
mom.setNextAction(wife);
dad.handle();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 2、使用场景

  • 命令模式:发出指令,中间层传递命令本身,命中包含执行对象

  • 模板模式:通过模板定义执行顺序,做独立操作

  • 观察者模式:通过观察者,可以让被观察值统一发生变化,触发相应依赖值的统一更新

  • 责任链模式:独立职责的单元通过链式执行,逐步操作流程

# 3、实际应用

  • 命令模式:调度器在接受到一组新的数据时候,解析数据,并且根据数据类型包裹在对象中传递到下级helper,helper再去执行相应操作

  • 模版模式:echart准备工作:canvas、config、init、draw(),规划顺序执行

  • 观察者模式:输入框输入的值去判断下拉框显示与否 => 观察input设置show

  • 责任链模式:提交表单进行表单逐行校验,链式调用validate,依次执行

# 3、面试题

某停车场,分3层,每层100车位,每个车位都能监控到车辆的驶入和离开,设计一套系统,要求:

  • 车辆进入前,显示每层的空余车位数量
  • 车辆进入时,摄像头可识别车牌号和时间
  • 车辆出来时,出口显示器显示车牌号和停车时长

# 1、类分析

  • 停车场:Park

    • 属性:floors停车层、camera摄像头、screen显示器、carList车辆信息
    • 方法:in车辆进、out车辆出、emptyNum获取车辆数目
  • 停车层:Floor

    • 属性:index层数,places车位数组
    • 方法:emptyPlaceNum,获取空余车位数量
  • 车位:Place

    • 属性:empty是否空
    • 方法:in、out,车辆进入和退出
  • 车辆:Car

    • 属性:num,车牌号
  • 显示器:Screen

    • 方法:show,显示车牌号和时长
  • 摄像头:Camera

    • 方法:shot,获取车牌号和时间

# 2、uml类图

# 3、代码实现

// 停车位
class Place{
  constructor(empty){
    this.empty = true
  }
  in(){
    this.empty = false
  }
  out(){
    this.empty = true
  }
}

// 停车层
class Floor{
  constructor(index, places){
    this.index = index
    this.places = places || []
  }
  emptyPlaceNum(){
    let num = 0
    this.places.forEach(p => {
      if(p.empty){
        num = num + 1
      }
    })
    return num
  }
}

// 车辆
class Car{
  constructor(num){
    this.num = num
  }
}

// 摄像机
class Camera{
  shot(car){
    return {
      num: car.num,
      inTime: Date.now()
    }
  }
}

// 显示器
class Screen{
  show(car, inTime){
    console.log('车牌号', car.num)
    console.log('停车时间', Date.now() - inTime)
  }
}

// 停车场
class Park{
  constructor(floors){
    this.floors = floors || []
    this.camera = new Camera()
    this.screen = new Screen()
    this.carList = {}  // 存储摄像头拍摄返回的车辆信息
  }
  //车辆进入
  in(car){
    // 通过摄像头获取信息
    const info = this.camera.shot(car)
    // 停到某个停车位
    const i = parseInt(Math.random() * 100 % 100)
    const place = this.floors[0].places[i] //停车位
    place.in() //停车
    info.place = place
    // 记录信息
    this.carList[car.num] = info
  }
  //车辆出
  out(car){
    // 获取信息
    const info = this.carList[car.num]
    // 将车位清空
    const place = info.place
    place.out()
    // 显示时间
    this.screen.show(car, info.inTime)
    // 清空记录
    delete this.carList[car.num]
  }
  emptyNum(){
    return this.floors.map(floor => {
      return `${floor.index} 层还有${floor.emptyPlaceNum()}个空闲车位}`
    }).join('\n')
  }
}

// 初始化停车场
const floors = []
for(let i = 0; i < 3; i++){
  const places = []
  for(let j = 0; j < 100; j++){
    places[j] = new Place()
  }
  floors[i] = new Floor(i + 1, places)
}
const park = new Park(floors)

// 初始化车辆
const car1 = new Car(100)
const car2 = new Car(200)
const car3 = new Car(300)
// 车辆进出
console.log('第一辆车进入')
console.log('空位数量', park.emptyNum())
park.in(car1)
console.log('第二辆车进入')
console.log('空位数量', park.emptyNum())
park.in(car2)
console.log('第一辆车离开')
park.out(car1)
console.log('第二辆车离开')
park.out(car2)
console.log('第三辆车进入')
console.log('空位数量', park.emptyNum())
park.in(car3)
console.log('第三辆车离开')
park.out(car3)
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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
Last Updated: 4/9/2022, 10:49:21 AM