原文链接:https://juejin.im/post/5e021eb96fb9a01628014095
设计模式简介 设计模式代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。 设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。
设计模式原则
S – Single Responsibility Principle 单一职责原则
一个程序只做好一件事
如果功能过于复杂就拆分开,每个部分保持独立
O – OpenClosed Principle 开放/封闭原则
对扩展开放,对修改封闭
增加需求时,扩展新代码,而非修改已有代码
L – Liskov Substitution Principle 里氏替换原则
I – Interface Segregation Principle 接口隔离原则
保持接口的单一独立
类似单一职责原则,这里更关注接口
D – Dependency Inversion Principle 依赖倒转原则
面向接口编程,依赖于抽象而不依赖于具体
使用方只关注接口而不关注具体类的实现
SO 体现较多,举个栗子:(比如 Promise)
单一职责原则:每个 then 中的逻辑只做好一件事
开放封闭原则(对扩展开放,对修改封闭):如果新增需求,扩展 then
再举个栗子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 let checkType = function (str, type ) { switch (type) { case 'email' : return /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/ .test(str); case 'mobile' : return /^1[3|4|5|7|8][0-9]{9}$/ .test(str); case 'tel' : return /^(0\d{2,3}-\d{7,8})(-\d{1,4})?$/ .test(str); default : return true ; } };
有以下两个问题:
如果想添加其他规则就得在函数里面增加 case 。添加一个规则就修改一次!这样违反了开放-封闭原则(对扩展开放,对修改关闭)。而且这样也会导致整个 API 变得臃肿,难维护。
比如 A 页面需要添加一个金额的校验,B 页面需要一个日期的校验,但是金额的校验只在 A 页面需要,日期的校验只在 B 页面需要。如果一直添加 case 。就是导致 A 页面把只在 B 页面需要的校验规则也添加进去,造成不必要的开销。B 页面也同理。
建议的方式是给这个 API 增加一个扩展的接口:
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 let checkType = (function ( ) { let rules = { email (str ) { return /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/ .test(str); }, mobile (str ) { return /^1[3|4|5|7|8][0-9]{9}$/ .test(str); }, }; return { check (str, type ) { return rules[type] ? rules[type](str) : false ; }, addRule (type, fn ) { rules[type] = fn; }, }; })(); console .log(checkType.check('188170239' , 'mobile' ));checkType.addRule('money' , function (str ) { return /^[0-9]+(.[0-9]{2})?$/ .test(str); }); console .log(checkType.check('18.36' , 'money' ));
设计模式分类 创建型 单例模式 一个类只有一个实例,并提供一个访问它的全局访问点,一般登录、购物车等都是一个单例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class SingleObject { login ( ) {} } SingleObject.getInstance = (function ( ) { let instance; return function ( ) { if (!instance) { instance = new SingleObject(); } return instance; }; })(); const obj1 = SingleObject.getInstance();const obj2 = SingleObject.getInstance();console .log(obj1 === obj2);
优点
划分命名空间,减少全局变量
增强模块性,把自己的代码组织在一个全局变量名下,放在单一位置,便于维护
且只会实例化一次。简化了代码的调试和维护
缺点
由于单例模式提供的是一种单点访问,所以它有可能导致模块间的强耦合,从而不利于单元测试。无法单独测试一个调用了来自单例的方法的类,而只能把它与那个单例作为一个单元一起测试。
应用场景
定义命名空间和实现分支型方法
登录框
vuex 和 redux 中的 store
原型模式 原型模式(prototype)是指用原型实例指向创建对象的种类,并且通过拷贝这些原型创建新的对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Person { constructor (name ) { this .name = name; } getName ( ) { return this .name; } } class Student extends Person { constructor (name ) { super (name); } sayHello ( ) { console .log(`Hello, My name is ${this .name} ` ); } } let student = new Student('xiaoming' );student.sayHello();
原型模式,就是创建一个共享的原型,通过拷贝这个原型来创建新的类,用于创建重复的对象,带来性能上的提升。
工厂模式 工厂模式定义一个用于创建对象的接口,这个接口由子类决定实例化哪一个类。该模式使一个类的实例化延迟到了子类。而子类可以重写接口方法以便创建的时候指定自己的对象类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Product { constructor (name ) { this .name = name; } init ( ) { console .log('init' ); } fun ( ) { console .log('fun' ); } } class Factory { create (name ) { return new Product(name); } } let factory = new Factory();let p = factory.create('p1' );p.init(); p.fun();
优点
创建对象的过程可能很复杂,但我们只需要关心创建结果。
构造函数和创建者分离, 符合“开闭原则”
一个调用者想创建一个对象,只要知道其名称就可以了。
扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
缺点
添加新产品时,需要编写新的具体产品类,一定程度上增加了系统的复杂度
考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度
适用场景
如果你不想让某个子系统与较大的那个对象之间形成强耦合,而是想运行时从许多子系统中进行挑选的话,那么工厂模式是一个理想的选择
将 new 操作简单封装,遇到 new 的时候就应该考虑是否用工厂模式;
需要依赖具体环境创建不同实例,这些实例都有相同的行为,这时候我们可以使用工厂模式,简化实现的过程,同时也可以减少每种对象所需的代码量,有利于消除对象间的耦合,提供更大的灵活性
应用场景
JQuery 中的 $
Vue.component 异步组件
React.createElement
抽象工厂模式 抽象工厂其实是实现子类继承父类的方法,只是一个方法。抽象工厂模式一般用在多人协作的超大型项目中,并且严格的要求项目以面向对象的思想进行完成。
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 var abstractFactory = function (subType, superType ) { if (typeof abstractFactory[superType] === 'function' ) { function F ( ) {} F.prototype = new abstractFactory[superType](); subType.constructor = subType; subType.prototype = new F(); } else { throw new Error ('未创建该抽象类' ); } }; abstractFactory.Car = function ( ) { this .type = 'car' ; }; abstractFactory.Car.prototype = { getPrice1: function ( ) { return '基类函数' ; }, getSpeed1: function ( ) { return '基类函数' ; }, }; var MyCar = function (price, speed ) { this .price = price; this .speed = speed; }; abstractFactory(MyCar, 'Car' ); MyCar.prototype.getPrice = function ( ) { return this .price; }; MyCar.prototype.getSpeed = function ( ) { return this .speed; }; var bmw = new MyCar(11000 , 200 );var speed = bmw.getSpeed();var price = bmw.getPrice();var speed1 = bmw.getSpeed1();var price1 = bmw.getPrice1();console .log(speed);console .log(price);console .log(speed1);console .log(price1);
建造者模式 建造者模式(Builder pattern),将一个复杂对象的构建层与其表示层相互分离,使得同样的构建过程(某一算法)可以采用不同的表示。
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 const Human = function (param ) { this .skill = param && param.skill; this .hobby = param && param.hobby; }; Human.prototype = { getSkill: function ( ) { return this .skill; }, getHobby: function ( ) { return this .hobby; }, }; const Named = function (named ) { (function (named, that ) { that.wholeName = named; if (named.includes(' ' )) { that.FirstName = named.slice(0 , named.indexOf(' ' )); that.SecondeName = named.slice(named.indexOf(' ' )); } })(named, this ); }; const Work = function (work ) { (function (work, that ) { switch (work) { case 'FE' : that.work = '工程师' ; that.workDesc = '每天沉迷于编程' ; break ; case 'UI' : that.work = '设计师' ; that.workDesc = '设计更像一种艺术' ; break ; default : that.work = work; that.workDesc = '对不起,我们不清楚您所选择职位的描述' ; } })(work, this ); }; Work.prototype.changeWork = function (work ) { this .work = work; }; const Person = function (param, name, work ) { const _person = new Human(param); _person.named = new Named(name); _person.work = new Work(work); return _person; }; const xiaoming = new Person({ skill : '耍帅' , hobby : '装逼' }, 'xiao ming' , 'code' );xiaoming.skill; xiaoming.FirstName; xiaoming.work;
优点
分布创建一个复杂的对象。
解耦封装过程和具体创建的组件。
无需关心组件如何组装。
注意点
一定要一个稳定的算法进行支持。
加工工艺是暴露的。
结构型 装饰器模式
动态地给某个对象添加一些额外的职责,是一种实现继承的替代方案
在不改变原对象的基础上,通过对其进行包装扩展,使原有对象可以满足用户的更复杂需求,而不会影响从这个类中派生的其他对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Cellphone { create ( ) { console .log('生成一个手机' ); } } class Decorator { constructor (cellphone ) { this .cellphone = cellphone; } create ( ) { this .cellphone.create(); this .createShell(cellphone); } createShell ( ) { console .log('生成手机壳' ); } } let cellphone = new Cellphone();cellphone.create(); console .log('------------' );let dec = new Decorator(cellphone);dec.create();
优点
装饰类和被装饰类都只关心自身的核心业务,实现了解耦。
方便动态的扩展功能,且提供了比继承更多的灵活性。
缺点
多层装饰比较复杂。
常常会引入许多小对象,看起来比较相似,实际功能大相径庭,从而使得我们的应用程序架构变得复杂起来
场景例子
比如现在有 4 种型号的自行车,我们为每种自行车都定义了一个单独的类。现在要给每种自行车都装上前灯、尾灯和铃铛这 3 种配件。如果使用继承的方式来给 每种自行车创建子类,则需要 4×3 = 12 个子类。但是如果把前灯、尾灯、铃铛这些对象动态组合到自行车上面,则只需要额外增加 3 个类
ES7 Decorator 阮一峰
core-decorators
适配器模式 将一个类的接口转化为另外一个接口,以满足用户需求,使类之间接口不兼容问题通过适配器得以解决。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Plug { getName ( ) { return 'iphone充电头' ; } } class Target { constructor ( ) { this .plug = new Plug(); } getName ( ) { return this .plug.getName() + ' 适配器Type-c充电头' ; } } let target = new Target();target.getName();
优点
可以让任何两个没有关联的类一起运行。
提高了类的复用。
适配对象,适配库,适配数据
缺点
额外对象的创建,非直接调用,存在一定的开销(且不像代理模式在某些功能点上可实现性能优化)
如果没必要使用适配器模式的话,可以考虑重构,如果使用的话,尽量把文档完善
场景
整合第三方 SDK
封装旧接口
vue 的 computed
适配器与代理模式相似
适配器模式: 提供一个不同的接口(如不同版本的插头)
代理模式: 提供一模一样的接口
代理模式 为其他对象提供一种代理,便以控制对这个对象的访问,不能直接访问目标对象。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 class Flower {}class Jack { constructor (target ) { this .target = target; } sendFlower (target ) { const flower = new Flower(); this .target.receiveFlower(flower); } } class Rose { receiveFlower (flower ) { console .log('收到花: ' + flower); } } class ProxyObj { constructor ( ) { this .target = new Rose(); } receiveFlower (flower ) { this .sendFlower(flower); } sendFlower (flower ) { this .target.receiveFlower(flower); } } const proxyObj = new ProxyObj();const jack = new Jack(proxyObj);jack.sendFlower(proxyObj);
优点
代理模式能将代理对象与被调用对象分离,降低了系统的耦合度。代理模式在客户端和目标对象之间起到一个中介作用,这样可以起到保护目标对象的作用
代理对象可以扩展目标对象的功能;通过修改代理对象就可以了,符合开闭原则;
缺点
不同点
装饰者模式实现上和代理模式类似
装饰者模式: 扩展功能,原有功能不变且可直接使用
代理模式: 显示原有功能,但是经过限制之后的
应用场景
ES6 Proxy
Vuex 中对于 getters 访问
图片预加载
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 var myImage = (function ( ) { var imgNode = document .createElement('img' ); document .body.appendChild(imgNode); return { setSrc: function (src ) { imgNode.src = src; }, }; })(); var proxyImage = (function ( ) { var img = new Image(); img.onload = function ( ) { myImage.setSrc(this .src); }; return { setSrc: function (src ) { myImage.setSrc('file:// /C:/Users/svenzeng/Desktop/loading.gif' ); img.src = src; }, }; })(); proxyImage.setSrc('http:// image.qq.com/music/photo/k/000GGDys0yA0Nk.jpg' );
外观模式 为一组复杂的子系统接口提供一个更高级的统一接口,通过这个接口使得对子系统接口的访问更容易,不符合单一职责原则和开放封闭原则。
兼容浏览器事件绑定
1 2 3 4 5 6 7 8 9 let addMyEvent = function (el, ev, fn ) { if (el.addEventListener) { el.addEventListener(ev, fn, false ); } else if (el.attachEvent) { el.attachEvent('on' + ev, fn); } else { el['on' + ev] = fn; } };
封装接口
1 2 3 4 5 6 7 let myEvent = { stop: (e ) => { e.stopPropagation(); e.preventDefault(); }, };
优点
缺点
不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。
场景
设计初期,应该要有意识地将不同的两个层分离,比如经典的三层结构,在数据访问层和业务逻辑层、业务逻辑层和表示层之间建立外观 Facade
在开发阶段,子系统往往因为不断的重构演化而变得越来越复杂,增加外观 Facade 可以提供一个简单的接口,减少他们之间的依赖。
在维护一个遗留的大型系统时,可能这个系统已经很难维护了,这时候使用外观 Facade 也是非常合适的,为系系统开发一个外观 Facade 类,为设计粗糙和高度复杂的遗留代码提供比较清晰的接口,让新系统和 Facade 对象交互,Facade 与遗留代码交互所有的复杂工作。
桥接模式 桥接模式(Bridge)将抽象部分与它的实现部分分离,使它们都可以独立地变化。
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 class Speed { constructor (x, y ) { this .x = x; this .y = y; } run ( ) { console .log(`运动起来 ${this .x} + ${this .y} ` ); } } class Color { constructor (cl ) { this .color = cl; } draw ( ) { console .log(`绘制颜色 ${this .color} ` ); } } class Speak { constructor (wd ) { this .word = wd; } say ( ) { console .log(`说话 ${this .word} ` ); } } class Ball { constructor (x, y, cl ) { this .speed = new Speed(x, y); this .color = new Color(cl); } init ( ) { this .speed.run(); this .color.draw(); } } class Man { constructor (x, y, wd ) { this .speed = new Speed(x, y); this .speak = new Speak(wd); } init ( ) { this .speed.run(); this .speak.say(); } } const man = new Man(1 , 2 , 'hehe?' );man.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 class TrainOrder { create ( ) { console .log('创建火车票订单' ); } } class HotelOrder { create ( ) { console .log('创建酒店订单' ); } } class TotalOrder { constructor ( ) { this .orderList = []; } addOrder (order ) { this .orderList.push(order); return this ; } create ( ) { this .orderList.forEach((item ) => { item.create(); }); return this ; } } let train = new TrainOrder();let hotel = new HotelOrder();let total = new TotalOrder();total.addOrder(train).addOrder(hotel).create();
缺点
如果通过组合模式创建了太多的对象,那么这些对象可能会让系统负担不起
场景
表示对象-整体层次结构
希望用户忽略组合对象和单个对象的不同,用户将统一地使用组合结构中的所有对象(方法)
虚拟 dom
享元模式 运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式。
一个程序中使用了大量的相似对象,造成等很大的内存开销,且对象的大部分属性都可以变成外部状态。主要是区分外部状态和内部状态,剥离外部状态,保存在其他地方,在合适的时候再把外部状态组装进共享对象
重点:区分内部状态或外部状态
内部状态存储于对象的内部
内部状态可以被一些对象共享
内部状态独立于具体的场景,通常不会改变
外部状态取决于具体的场景,并根据场景而变化,外部状态不能被共享
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 class Modal { constructor (id, gender ) { this .gender = gender; this .name = `张${gender} ${id} ` ; } } class ModalFactory { static create (id, gender ) { if (this [gender]) { return this [gender]; } return (this [gender] = new Modal(id, gender)); } } class TakeClothesManager { static addClothes (id, gender, clothes ) { const modal = ModalFactory.create(id, gender); this [id] = { clothes, modal, }; } static takePhoto (id ) { const obj = this [id]; console .log(`${obj.modal.gender} 模特${obj.modal.name} 穿${obj.clothes} 拍了张照` ); } } for (let i = 0 ; i < 50 ; i++) { TakeClothesManager.addClothes(i, '男' , `服装${i} ` ); TakeClothesManager.takePhoto(i); } for (let i = 50 ; i < 100 ; i++) { const { addClothes, takePhoto } = TakeClothesManager; TakeClothesManager.addClothes(i, '女' , `服装${i} ` ); TakeClothesManager.takePhoto(i); }
优点
缺点
提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质, 不应该随着内部状态的变化而变化,否则会造成系统的混乱
场景例子
文件上传需要创建多个文件实例的时候
如果一个应用程序使用了大量的对象,而这些大量的对象造成了很大的存储开销时就应该考虑使用享元模式
行为型 观察者模式 定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使它们能够自动更新自己,当一个对象的改变需要同时改变其它对象,并且它不知道具体有多少对象需要改变的时候,就应该考虑使用观察者模式。
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 class Subject { constructor ( ) { this .state = 0 ; this .observers = []; } getState ( ) { return this .state; } setState (state ) { this .state = state; this .notifyAllObservers(); } notifyAllObservers ( ) { this .observers.forEach((observer ) => { observer.update(); }); } attach (observer ) { this .observers.push(observer); } } class Observer { constructor (name, subject ) { this .name = name; this .subject = subject; this .subject.attach(this ); } update ( ) { console .log(`${this .name} update, state: ${this .subject.getState()} ` ); } } let s = new Subject();let o1 = new Observer('o1' , s);let o2 = new Observer('02' , s);s.setState(12 );
优点
支持简单的广播通信,自动通知所有已经订阅过的对象
目标对象与观察者之间的抽象耦合关系能单独扩展以及重用
增加了灵活性
观察者模式所做的工作就是在解耦,让耦合的双方都依赖于抽象,而不是依赖于具体。从而使得各自的变化都不会影响到另一边的变化。
缺点
过度使用会导致对象与对象之间的联系弱化,会导致程序难以跟踪维护和理解
应用场景
迭代器模式 提供一种方法顺序一个聚合对象中各个元素,而又不暴露该对象的内部表示。
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 Iterator { constructor (conatiner ) { this .list = conatiner.list; this .index = 0 ; } next ( ) { if (this .hasNext()) { return this .list[this .index++]; } return null ; } hasNext ( ) { if (this .index >= this .list.length) { return false ; } return true ; } } class Container { constructor (list ) { this .list = list; } getIterator ( ) { return new Iterator(this ); } } let container = new Container([1 , 2 , 3 , 4 , 5 ]);let iterator = container.getIterator();while (iterator.hasNext()) { console .log(iterator.next()); }
特点
访问一个聚合对象的内容而无需暴露它的内部表示。
为遍历不同的集合结构提供一个统一的接口,从而支持同样的算法在不同的集合结构上进行操作.
对于集合内部结果常常变化各异,不想暴露其内部结构的话,但又想让客户代码透明的访问其中的元素,可以使用迭代器模式
场景例子
Array.prototype.forEach
jQuery 中的$.each()
ES6 Iterator
策略模式 实现一种功能有多种选择方案,将算法的使用和实现分离开来。定义一系列的算法,把他们封装起来,并且使他们可以相互替换。可用于替换 if else。
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 Strategy1 { calculate (salary ) { return salary * 2 ; } } class Strategy2 { calculate (salary ) { return salary * 3 ; } } class Bonus { constructor ( ) { this .salary = null ; this .strategy = null ; } setSalary (salary ) { this .salary = salary; } setStrategy (strategy ) { this .strategy = strategy; } getBonus ( ) { return this .strategy.calculate(this .salary); } } const bonus = new Bonus();bonus.setSalary(1000 ); bonus.setStrategy(new Strategy1()); console .log(bonus.getBonus());bonus.setStrategy(new Strategy2()); console .log(bonus.getBonus());
优点
利用组合、委托、多态等技术和思想,可以有效的避免多重条件选择语句
提供了对开放-封闭原则的完美支持,将算法封装在独立的 strategy 中,使得它们易于切换,理解,易于扩展
利用组合和委托来让 Context 拥有执行算法的能力,这也是继承的一种更轻便的代替方案
缺点
会在程序中增加许多策略类或者策略对象
要使用策略模式,必须了解所有的 strategy,必须了解各个 strategy 之间的不同点,这样才能选择一个合适的 strategy
场景例子
如果在一个系统里面有许多类,它们之间的区别仅在于它们的’行为’,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
一个系统需要动态地在几种算法中选择一种。
表单验证
模板方法模式 模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法和封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。
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 class Beverage { constructor ({ brewDrink, addCondiment } ) { this .brewDrink = brewDrink; this .addCondiment = addCondiment; } boilWater ( ) { console .log('水已经煮沸=== 共用' ); } pourCup ( ) { console .log('倒进杯子里===共用' ); } init ( ) { this .boilWater(); this .brewDrink(); this .pourCup(); this .addCondiment(); } } const coffee = new Beverage({ brewDrink: function ( ) { console .log('冲泡咖啡' ); }, addCondiment: function ( ) { console .log('加点奶和糖' ); }, }); coffee.init();
优点
缺点
场景例子
一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现
子类中公共的行为应被提取出来并集中到一个公共父类中的避免代码重复
职责链模式 使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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(); } } } let a1 = new Action('组长' );let a2 = new Action('经理' );let a3 = new Action('总监' );a1.setNextAction(a2); a2.setNextAction(a3); a1.handle();
优点
降低耦合度。它将请求的发送者和接收者解耦。
简化了对象。使得对象不需要知道链的结构。
增强给对象指派职责的灵活性。通过改变链内的成员或者调动它们的次序,允许动态地新增或者删除责任。
增加新的请求处理类很方便。
缺点
不能保证某个请求一定会被链中的节点处理,这种情况可以在链尾增加一个保底的接受者节点来处理这种即将离开链尾的请求。
使程序中多了很多节点对象,可能再一次请求的过程中,大部分的节点并没有起到实质性的作用。他们的作用仅仅是让请求传递下去,从性能当面考虑,要避免过长的职责链到来的性能损耗。
场景例子
命令模式 将一个请求封装成一个对象,从而让你使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。
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 class Receiver { execute ( ) { console .log('接收者执行请求' ); } } class Command { constructor (receiver ) { this .receiver = receiver; } execute ( ) { console .log('命令' ); this .receiver.execute(); } } class Invoker { constructor (command ) { this .command = command; } invoke ( ) { console .log('开始' ); this .command.execute(); } } const warehouse = new Receiver();const order = new Command(warehouse);const client = new Invoker(order);client.invoke();
优点
对命令进行封装,使命令易于扩展和修改。
命令发出者和接受者解耦,使发出者不需要知道命令的具体执行过程即可执行。
缺点
使用命令模式可能会导致某些系统有过多的具体命令类。
备忘录模式 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到保存的状态。
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 class Memento { constructor (content ) { this .content = content; } getContent ( ) { return this .content; } } class CareTaker { constructor ( ) { this .list = []; } add (memento ) { this .list.push(memento); } get (index ) { return this .list[index]; } } class Editor { constructor ( ) { this .content = null ; } setContent (content ) { this .content = content; } getContent ( ) { return this .content; } saveContentToMemento ( ) { return new Memento(this .content); } getContentFromMemento (memento ) { this .content = memento.getContent(); } } let editor = new Editor();let careTaker = new CareTaker();editor.setContent('111' ); editor.setContent('222' ); careTaker.add(editor.saveContentToMemento()); editor.setContent('333' ); careTaker.add(editor.saveContentToMemento()); editor.setContent('444' ); console .log(editor.getContent()); editor.getContentFromMemento(careTaker.get(1 )); console .log(editor.getContent()); editor.getContentFromMemento(careTaker.get(0 )); console .log(editor.getContent());
优点
给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态。
缺点
消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。
场景例子
状态模式 允许一个对象在其内部状态改变的时候改变它的行为,对象看起来似乎修改了它的类
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 class State { constructor (state ) { this .state = state; } handle (context ) { console .log(`this is ${this .state} light` ); context.setState(this ); } } class Context { constructor ( ) { this .state = null ; } getState ( ) { return this .state; } setState (state ) { this .state = state; } } let context = new Context();let weak = new State('weak' );let strong = new State('strong' );let off = new State('off' );weak.handle(context); console .log(context.getState());strong.handle(context); console .log(context.getState());strong.handle(context); console .log(context.getState());
优点
定义了状态与行为之间的关系,封装在一个类里,更直观清晰,增改方便
状态与状态间,行为与行为间彼此独立互不干扰
用对象代替字符串来记录当前状态,使得状态的切换更加一目了然
缺点
场景
一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为
一个操作中含有大量的分支语句,而且这些分支语句依赖于该对象的状态
访问者模式 表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Visitor { constructor ( ) {} visitConcreteElement (ConcreteElement ) { ConcreteElement.operation(); } } class ConcreteElement { constructor ( ) {} operation ( ) { console .log('ConcreteElement.operation invoked' ); } accept (visitor ) { visitor.visitConcreteElement(this ); } } let visitor = new Visitor();let element = new ConcreteElement();elementA.accept(visitor);
优点
缺点
具体元素对访问者公布细节,违反了迪米特原则
违反了依赖倒置原则,依赖了具体类,没有依赖抽象。
具体元素变更比较困难
场景例子
对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作
需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作”污染”这些对象的类,也不希望在增加新操作时修改这些类。
中介者模式 解除对象与对象之间的紧耦合关系。增加一个中介者对象后,所有的相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知中介者对象即可。中介者使各对象之间耦合松散,而且可以独立地改变它们之间的交互。中介者模式使网状的多对多关系变成了相对简单的一对多关系(类似于观察者模式,但是单向的,由中介者统一管理。)
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 class A { constructor ( ) { this .number = 0 ; } setNumber (num, m ) { this .number = num; if (m) { m.setB(); } } } class B { constructor ( ) { this .number = 0 ; } setNumber (num, m ) { this .number = num; if (m) { m.setA(); } } } class Mediator { constructor (a, b ) { this .a = a; this .b = b; } setA ( ) { let number = this .b.number; this .a.setNumber(number * 10 ); } setB ( ) { let number = this .a.number; this .b.setNumber(number / 10 ); } } let a = new A();let b = new B();let m = new Mediator(a, b);a.setNumber(10 , m); console .log(a.number, b.number);b.setNumber(10 , m); console .log(a.number, b.number);
优点
使各对象之间耦合松散,而且可以独立地改变它们之间的交互。
中介者和对象一对多的关系取代了对象之间的网状多对多的关系。
如果对象之间的复杂耦合度导致维护很困难,而且耦合度随项目变化增速很快,就需要中介者重构代码。
缺点
系统中会新增一个中介者对象,因 为对象之间交互的复杂性,转移成了中介者对象的复杂性,使得中介者对象经常是巨大的。中介 者对象自身往往就是一个难以维护的对象。
场景例子
系统中对象之间存在比较复杂的引用关系,导致它们之间的依赖关系结构混乱而且难以复用该对象想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。
解释器模式 给定一个语言, 定义它的文法的一种表示,并定义一个解释器, 该解释器使用该表示来解释语言中的句子。
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 class Context { constructor ( ) { this ._list = []; this ._sum = 0 ; } get sum () { return this ._sum; } set sum (newValue ) { this ._sum = newValue; } add (expression ) { this ._list.push(expression); } get list () { return [...this._list]; } } class PlusExpression { interpret (context ) { if (!(context instanceof Context)) { throw new Error ('TypeError' ); } context.sum = ++context.sum; } } class MinusExpression { interpret (context ) { if (!(context instanceof Context)) { throw new Error ('TypeError' ); } context.sum = --context.sum; } } const context = new Context();context.add(new PlusExpression()); context.add(new PlusExpression()); context.add(new MinusExpression()); context.list.forEach((expression ) => expression.interpret(context)); console .log(context.sum);
优点
易于改变和扩展文法。
由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法
缺点
执行效率较低,在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度慢。
对于复杂的文法比较难维护。