提升开发思维的设计模式(下)
上期回顾
提升开发思维的设计模式(上)
2. 设计模式分类(23种设计模式)
2.13 组合模式(Composite Pattern)
-
将对象组合成树形结构,以表示“整体-部分”的层次结构。
-
通过对象的多态表现,使得用户对单个对象和组合对象的使用具有一致性。
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()
场景
-
表示对象-整体层次结构
-
希望用户忽略组合对象和单个对象的不同,用户将统一地使用组合结构中的所有对象(方法)
缺点
如果通过组合模式创建了太多的对象,那么这些对象可能会让系统负担不起。
2.14 原型模式(Prototype Pattern)
原型模式(prototype)是指用原型实例指向创建对象的种类,并且通过拷贝这些原型创建新的对象。
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()
原型模式,就是创建一个共享的原型,通过拷贝这个原型来创建新的类,用于创建重复的对象,带来性能上的提升。
2.15 策略模式(Strategy Pattern)
定义一系列的算法,把它们一个个封装起来,并且使它们可以互相替换
<html>
<head><title>策略模式-校验表单</title><meta content="text/html; charset=utf-8" http-equiv="Content-Type">
</head>
<body><form id = "registerForm" method="post" action="http://xxxx.com/api/register">用户名:<input type="text" name="userName">密码:<input type="text" name="password">手机号码:<input type="text" name="phoneNumber"><button type="submit">提交</button></form><script type="text/javascript">// 策略对象const strategies = {isNoEmpty: function (value, errorMsg) {if (value === '') {return errorMsg;}},isNoSpace: function (value, errorMsg) {if (value.trim() === '') {return errorMsg;}},minLength: function (value, length, errorMsg) {if (value.trim().length < length) {return errorMsg;}},maxLength: function (value, length, errorMsg) {if (value.length > length) {return errorMsg;}},isMobile: function (value, errorMsg) {if (!/^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|17[7]|18[0|1|2|3|5|6|7|8|9])\d{8}$/.test(value)) {return errorMsg;} }}// 验证类class Validator {constructor() {this.cache = []}add(dom, rules) {for(let i = 0, rule; rule = rules[i++];) {let strategyAry = rule.strategy.split(':')let errorMsg = rule.errorMsgthis.cache.push(() => {let strategy = strategyAry.shift()strategyAry.unshift(dom.value)strategyAry.push(errorMsg)return strategies[strategy].apply(dom, strategyAry)})}}start() {for(let i = 0, validatorFunc; validatorFunc = this.cache[i++];) {let errorMsg = validatorFunc()if (errorMsg) {return errorMsg}}}}// 调用代码let registerForm = document.getElementById('registerForm')let validataFunc = function() {let validator = new Validator()validator.add(registerForm.userName, [{strategy: 'isNoEmpty',errorMsg: '用户名不可为空'}, {strategy: 'isNoSpace',errorMsg: '不允许以空白字符命名'}, {strategy: 'minLength:2',errorMsg: '用户名长度不能小于2位'}])validator.add(registerForm.password, [ {strategy: 'minLength:6',errorMsg: '密码长度不能小于6位'}])validator.add(registerForm.phoneNumber, [{strategy: 'isMobile',errorMsg: '请输入正确的手机号码格式'}])return validator.start()}registerForm.onsubmit = function() {let errorMsg = validataFunc()if (errorMsg) {alert(errorMsg)return false}}</script>
</body>
</html>
场景例子
-
如果在一个系统里面有许多类,它们之间的区别仅在于它们的'行为',那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
-
一个系统需要动态地在几种算法中选择一种。
-
表单验证
优点
-
利用组合、委托、多态等技术和思想,可以有效的避免多重条件选择语句
-
提供了对开放-封闭原则的完美支持,将算法封装在独立的strategy中,使得它们易于切换,理解,易于扩展
-
利用组合和委托来让Context拥有执行算法的能力,这也是继承的一种更轻便的代替方案
缺点
-
会在程序中增加许多策略类或者策略对象
-
要使用策略模式,必须了解所有的strategy,必须了解各个strategy之间的不同点,这样才能选择一个合适的strategy
2.16 享元模式(Flyweight Pattern)
运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式,它是一种对象结构型模式
let examCarNum = 0 // 驾考车总数
/* 驾考车对象 */
class ExamCar {constructor(carType) {examCarNum++this.carId = examCarNumthis.carType = carType ? '手动档' : '自动档'this.usingState = false // 是否正在使用}/* 在本车上考试 */examine(candidateId) {return new Promise((resolve => {this.usingState = trueconsole.log(`考生- ${ candidateId } 开始在${ this.carType }驾考车- ${ this.carId } 上考试`)setTimeout(() => {this.usingState = falseconsole.log(`%c考生- ${ candidateId } 在${ this.carType }驾考车- ${ this.carId } 上考试完毕`, 'color:#f40')resolve() // 0~2秒后考试完毕}, Math.random() * 2000)}))}
}/* 手动档汽车对象池 */
ManualExamCarPool = {_pool: [], // 驾考车对象池_candidateQueue: [], // 考生队列/* 注册考生 ID 列表 */registCandidates(candidateList) {candidateList.forEach(candidateId => this.registCandidate(candidateId))},/* 注册手动档考生 */registCandidate(candidateId) {const examCar = this.getManualExamCar() // 找一个未被占用的手动档驾考车if (examCar) {examCar.examine(candidateId) // 开始考试,考完了让队列中的下一个考生开始考试.then(() => {const nextCandidateId = this._candidateQueue.length && this._candidateQueue.shift()nextCandidateId && this.registCandidate(nextCandidateId)})} else this._candidateQueue.push(candidateId)},/* 注册手动档车 */initManualExamCar(manualExamCarNum) {for (let i = 1; i <= manualExamCarNum; i++) {this._pool.push(new ExamCar(true))}},/* 获取状态为未被占用的手动档车 */getManualExamCar() {return this._pool.find(car => !car.usingState)}
}ManualExamCarPool.initManualExamCar(3) // 一共有3个驾考车
ManualExamCarPool.registCandidates([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) // 10个考生来考试
场景例子
-
文件上传需要创建多个文件实例的时候
-
如果一个应用程序使用了大量的对象,而这些大量的对象造成了很大的存储开销时就应该考虑使用享元模式
优点
-
大大减少对象的创建,降低系统的内存,使效率提高。
缺点
-
提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质, 不应该随着内部状态的变化而变化,否则会造成系统的混乱
2.17 模板方法模式(Template Method Pattern)
模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法和封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。
class Beverage {constructor({brewDrink, addCondiment}) {this.brewDrink = brewDrinkthis.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()
场景例子
-
一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现
-
子类中公共的行为应被提取出来并集中到一个公共父类中的避免代码重复
优点
-
提取了公共代码部分,易于维护
缺点
-
增加了系统复杂度,主要是增加了的抽象类和类间联系
2.18 责任链模式(Chain of Responsibility)
使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止
// 请假审批,需要组长审批、经理审批、总监审批
class Action {constructor(name) {this.name = namethis.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()
场景例子
-
JS 中的事件冒泡
-
作用域链
-
原型链
优点
-
降低耦合度。它将请求的发送者和接收者解耦。
-
简化了对象。使得对象不需要知道链的结构
-
增强给对象指派职责的灵活性。通过改变链内的成员或者调动它们的次序,允许动态地新增或者删除责任
-
增加新的请求处理类很方便。
缺点
-
不能保证某个请求一定会被链中的节点处理,这种情况可以在链尾增加一个保底的接受者节点来处理这种即将离开链尾的请求。
-
使程序中多了很多节点对象,可能再一次请求的过程中,大部分的节点并没有起到实质性的作用。他们的作用仅仅是让请求传递下去,从性能当面考虑,要避免过长的职责链到来的性能损耗。
2.19 命令模式(Command)
将一个请求封装成一个对象,从而让你使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。
// 接收者类
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()
优点
-
对命令进行封装,使命令易于扩展和修改
-
命令发出者和接受者解耦,使发出者不需要知道命令的具体执行过程即可执行
缺点
-
使用命令模式可能会导致某些系统有过多的具体命令类。
2.20 备忘录模式(Memento)
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到保存的状态。
//备忘类
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()) //444
editor.getContentFromMemento(careTaker.get(1))
console.log(editor.getContent()) //333editor.getContentFromMemento(careTaker.get(0))
console.log(editor.getContent()) //222
场景例子
-
分页控件
-
撤销组件
优点
-
给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态
缺点
-
消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。
2.21 中介者模式(Mediator)
解除对象与对象之间的紧耦合关系。增加一个中介者对象后,所有的 相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知 中介者对象即可。中介者使各对象之间耦合松散,而且可以独立地改变它们之间的交互。中介者 模式使网状的多对多关系变成了相对简单的一对多关系(类似于观察者模式,但是单向的,由中介者统一管理。)
class A {constructor() {this.number = 0}setNumber(num, m) {this.number = numif (m) {m.setB()}}
}
class B {constructor() {this.number = 0}setNumber(num, m) {this.number = numif (m) {m.setA()}}
}
class Mediator {constructor(a, b) {this.a = athis.b = b}setA() {let number = this.b.numberthis.a.setNumber(number * 10)}setB() {let number = this.a.numberthis.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)
场景例子
-
系统中对象之间存在比较复杂的引用关系,导致它们之间的依赖关系结构混乱而且难以复用该对象
-
想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。
优点
-
使各对象之间耦合松散,而且可以独立地改变它们之间的交互
-
中介者和对象一对多的关系取代了对象之间的网状多对多的关系
-
如果对象之间的复杂耦合度导致维护很困难,而且耦合度随项目变化增速很快,就需要中介者重构代码
缺点
-
系统中会新增一个中介者对象,因 为对象之间交互的复杂性,转移成了中介者对象的复杂性,使得中介者对象经常是巨大的。中介 者对象自身往往就是一个难以维护的对象。
2.22 解释器模式(Interpreter)
给定一个语言, 定义它的文法的一种表示,并定义一个解释器, 该解释器使用该表示来解释语言中的句子。
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);
优点
-
易于改变和扩展文法。
-
由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法
缺点
-
执行效率较低,在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度慢
-
对于复杂的文法比较难维护
2.23 访问者模式(Visitor)
表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
// 访问者
class Visitor {constructor() {}visitConcreteElement(ConcreteElement) {ConcreteElement.operation()}
}
// 元素类
class ConcreteElement{constructor() {}operation() {console.log("ConcreteElement.operation invoked"); }accept(visitor) {visitor.visitConcreteElement(this)}
}
// client
let visitor = new Visitor()
let element = new ConcreteElement()
element.accept(visitor)
场景例子
-
对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作
-
需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,也不希望在增加新操作时修改这些类。
优点
-
符合单一职责原则
-
优秀的扩展性
-
灵活性
缺点
-
具体元素对访问者公布细节,违反了迪米特原则
-
违反了依赖倒置原则,依赖了具体类,没有依赖抽象。
-
具体元素变更比较困难
3. 总结
设计模式是为了可复用、可拓展、高性能软件,前人给我们总结的宝贵经验。
设计模式(Design Pattern)是前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。
当然,软件设计模式只是一个引导,在实际的软件开发中,必须根据具体的需求来选择。
4. 团队介绍
「三翼鸟数字化技术平台-ToC服务平台」以用户行为数据为基础,利用推荐引擎为用户提供“千人千面”的个性化推荐服务,改善用户体验,持续提升核心业务指标。通过构建高效、智能的线上运营系统,全面整合数据资产,实现数据分析-人群圈选-用户触达-后效分析-策略优化的运营闭环,并提供可视化报表,一站式操作提升数字化运营效率。