设计模式六个基本原则
1.单一职责
2.开闭原则
3.里氏替换
4.依赖倒转
5.接口隔离
6.迪米特法则(最少知识原则)
设计模式分类
一、创建型模式
1.单例
2.建造者
3.原型
4.工厂方法
简单工厂:
/**
* 抽象咖啡类
*/
abstract class Coffee {
constructor(public name: string) { }
}
class LatteCoffee extends Coffee { }
class MachaCoffee extends Coffee { }
class AmericanoCoffee extends Coffee { }
// 简单工厂实现
class CoffeFactory {
static buy(name: string) {
switch (name) {
case 'LatteCoffee':
return new LatteCoffee('拿铁咖啡');
case 'MachaCoffee':
return new MachaCoffee('摩卡咖啡');
case 'AmericanoCoffee':
return new AmericanoCoffee('美式咖啡');
default:
throw new Error('没有您需要的咖啡')
}
}
}
console.log(CoffeFactory.buy('LatteCoffee'));
console.log(CoffeFactory.buy('MachaCoffee'));
console.log(CoffeFactory.buy('AmericanoCoffee'));
工厂方法:
/**
* 抽象咖啡类
*/
abstract class Coffee {
constructor(public name: string) { }
}
class LatteCoffee extends Coffee { }
class MachaCoffee extends Coffee { }
class AmericanoCoffee extends Coffee { }
// 抽象咖啡工厂类
abstract class CoffeeFactory {
constructor() { }
}
class LatteCoffeeFactory extends CoffeeFactory {
createCoffe() {
console.log('您创建了一份拿铁咖啡');
}
}
class MachaCoffeeFactory extends CoffeeFactory {
createCoffe() {
console.log('您创建了一份摩卡咖啡');
}
}
class AmericanoCoffeeFactory extends CoffeeFactory {
createCoffe() {
console.log('您创建了一份美式咖啡');
}
}
// 在工厂方法里,不再由 Factory 来创建产品,而是先创建了具体的工厂,然后具体的工厂创建产品
class Factory {
static buy(name: string) {
switch (name) {
case 'LatteCoffee':
// 先创建了拿铁咖啡具体的工厂,然后再由具体的工厂再创建产品
return new LatteCoffeeFactory().createCoffe();
case 'MachaCoffee':
// 先创建了摩卡咖啡具体的工厂,然后再由具体的工厂再创建产品
return new MachaCoffeeFactory().createCoffe();
case 'AmericanoCoffee':
// 先创建了美式咖啡具体的工厂,然后再由具体的工厂再创建产品
return new AmericanoCoffeeFactory().createCoffe();
default:
throw new Error('没有您需要的咖啡')
}
}
}
console.log(Factory.buy('LatteCoffee'));
console.log(Factory.buy('MachaCoffee'));
console.log(Factory.buy('AmericanoCoffee'));
5.抽象工厂
/**
* 产品抽象类或接口
*/
abstract class MacProdoct { }
abstract class IWatchProdoct { }
abstract class PhoneProdoct { }
/**
* 具体产品实现类
*/
class AppleMacProdoct extends MacProdoct { }
class HuaweiMacProdoct extends MacProdoct { }
class AppleIWatchProdoct extends IWatchProdoct { }
class HuaweiIWatchProdoct extends IWatchProdoct { }
class ApplePhoneProdoct extends PhoneProdoct { }
class HuaweiPhoneProdoct extends PhoneProdoct { }
/**
* 抽象工厂
*/
abstract class AbstractProdoctFactory {
abstract createMacProdoct(): MacProdoct;
abstract createIWatchProdoct(): IWatchProdoct;
abstract createPhoneProdoct(): PhoneProdoct;
}
/**
* 具体产品工厂 - 苹果
*/
class AppleProdoct extends AbstractProdoctFactory {
createMacProdoct() {
return new AppleMacProdoct();
}
createIWatchProdoct() {
return new AppleIWatchProdoct();
}
createPhoneProdoct() {
return new ApplePhoneProdoct();
}
}
/**
* 具体产品工厂 - 华为
*/
class HuaweiProdoct extends AbstractProdoctFactory {
createMacProdoct() {
return new HuaweiMacProdoct();
}
createIWatchProdoct() {
return new HuaweiIWatchProdoct();
}
createPhoneProdoct() {
return new HuaweiPhoneProdoct();
}
}
let huaweiProduct = new HuaweiProdoct();
console.log(huaweiProduct.createMacProdoct()); // 购买华为电脑
console.log(huaweiProduct.createIWatchProdoct()); // 购买华为手表
console.log(huaweiProduct.createPhoneProdoct()); // 购买华为手机
简单工厂:
唯一工厂类,一个产品抽象类,工厂类的创建方法依据入参判断并创建具体产品对象。
适用情况包括:工厂类负责创建的对象比较少;客户端只知道传入工厂类的参数,对于如何创建对象不关心。
工厂方法:
多个工厂类,一个产品抽象类,利用多态创建不同的产品对象,避免了大量的 if-else 判断。
适用情况包括:一个类不知道它所需要的对象的类;一个类通过其子类来指定创建哪个对象;将创建对象的任务委托给多个工厂子类中的某一个,客户端在使用时可以无须关心是哪一个工厂子类创建产品子类,需要时再动态指定。
抽象工厂:
多个工厂类,多个产品抽象类,产品子类分组,同一个工厂实现类创建同组中的不同产品,减少了工厂子类的数量。
适用情况包括:一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节;系统中有多于一个的产品族,而每次只使用其中某一产品族;属于同一个产品族的产品将在一起使用;系统提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于具体实现。
二、结构型模式
1.代理
包装对象,为了增强原对象功能,但又不允许直接修改原对象时,使用代理模式。
应用:修改原对象的属性时,修改之前和修改之后都需要加上打印日志,但不允许直接修改原对象。
const obj = {
name: "john",
age: 20
};
const proxyObj = new Proxy(obj, {
get: (target, key) => {
return target[key];
},
set: (target, key, value) => {
if (key === "age") {
console.log("age before change===", target[key]);
target[key] = value;
console.log("age after change===", value);
return;
}
target[key] = value;
}
});
proxyObj.age = 18;
输出结果:
age before change=== 20
age after change=== 18
2.装饰器
包装对象,可以不侵入原有代码内部的情况下修改类代码的行为,处理一些与具体业务无关的公共功能,常见:日志,异常,埋码等。
举例:ES7中的decorator
// log()函数接受Calculator类作为参数,并返回一个新函数替换 Calculator类的构造函数。
function log(name) {
return function decorator(Class) {
return (...args) => {
console.log(`Arguments for ${name}: ${args}`);
return new Class(...args);
};
}
}
@log('Multiply')
class Calculator {
constructor (x,y) { }
}
calculator = new Calculator(10,10);
// Arguments for Multiply: [10, 10]console.log(calculator);
// Calculator {}
3.适配器
包装对象,为了解决两个对象之间不匹配的问题,而原对象又不适合直接修改,此时可以使用适配器模式进行一层转换。
应用:
- 使用一个已经存在的对象,但其方法或属性不符合我们的要求。
- 统一多个类的接口设计。
- 适配不同格式的数据。
- 兼容老版本的接口。
举例:vue的计算属性
4.外观(门面)
包装对象,提供一个统一的接口去访问多个子系统的多个不同的接口,为子系统中的一组接口提供统一的高层接口。使得子系统更容易使用,不仅简化类中的接口,而且实现调用者和接口的解耦。
与适配器模式的区别
适配器模式是将一个对象包装起来以改变其接口,而外观模式是将一群对象包装起来以简化其接口。
适配器是将接口转换为不同接口,而外观模式是提供一个统一的接口来简化接口。
举例:
// a.js
export default {
getA (params) {
// do something...
}
}
// b.js
export default {
getB (params) {
// do something...
}
}
// app.js 外观模式为子系统提供同一的高层接口
import A from './a'
import B from './b'
export default {
A,
B
}
// 通过同一接口调用子系统
import app from './app'
app.A.getA(params);
app.B.getB(params);
5.组合
组合对象,将一组相关的对象,组合为‘部分-整体’的结构。
举例:文件夹扫描
// 树对象 - 文件目录
class CFolder {
constructor(name) {
this.name = name;
this.files = [];
}
add(file) {
this.files.push(file);
}
scan() {
for (let file of this.files) {
file.scan();
}
}
}
// 叶对象 - 文件
class CFile {
constructor(name) {
this.name = name;
}
add(file) {
throw new Error('文件下面不能再添加文件');
}
scan() {
console.log(`开始扫描文件:${this.name}`);
}
}
let mediaFolder = new CFolder('娱乐');
let movieFolder = new CFolder('电影');
let musicFolder = new CFolder('音乐');
let file1 = new CFile('钢铁侠.mp4');
let file2 = new CFile('再谈记忆.mp3');
movieFolder.add(file1);
musicFolder.add(file2);
mediaFolder.add(movieFolder);
mediaFolder.add(musicFolder);
mediaFolder.scan();
/* 输出:
开始扫描文件:钢铁侠.mp4
开始扫描文件:再谈记忆.mp3
*/
举例:万能遥控器
// 创建一个宏命令
var MacroCommand = function(){
return {
// 宏命令的子命令列表
commandsList: [],
// 添加命令到子命令列表
add: function( command ){
this.commandsList.push( command );
},
// 依次执行子命令列表里面的命令
execute: function(){
for ( var i = 0, command; command = this.commandsList[ i++ ]; ){
command.execute();
}
}
}
};
<!--打开空调命令-->
var openAcCommand = {
execute: function(){
console.log( '打开空调' );
}
};
<!--打开电视和音响-->
var openTvCommand = {
execute: function(){
console.log( '打开电视' );
}
};
var openSoundCommand = {
execute: function(){
console.log( '打开音响' );
}
};
//创建一个宏命令
var macroCommand1 = MacroCommand();
//把打开电视装进这个宏命令里
macroCommand1.add(openTvCommand)
//把打开音响装进这个宏命令里
macroCommand1.add(openSoundCommand)
<!--关门、打开电脑和打登录QQ的命令-->
var closeDoorCommand = {
execute: function(){
console.log( '关门' );
}
};
var openPcCommand = {
execute: function(){
console.log( '开电脑' );
}
};
var openQQCommand = {
execute: function(){
console.log( '登录QQ' );
}
};
//创建一个宏命令
var macroCommand2 = MacroCommand();
//把关门命令装进这个宏命令里
macroCommand2.add( closeDoorCommand );
//把开电脑命令装进这个宏命令里
macroCommand2.add( openPcCommand );
//把登录QQ命令装进这个宏命令里
macroCommand2.add( openQQCommand );
<!--把各宏命令装进一个超级命令中去-->
var macroCommand = MacroCommand();
macroCommand.add( openAcCommand );
macroCommand.add( macroCommand1 );
macroCommand.add( macroCommand2 );
6.享元
组合对象,将一群类似的对象抽离出内部状态和外部状态,内部状态为共享,外部状态根据实际情况和内部状态进行连接。
举例:一个工厂需要 20 个男模特和 20 个女模特穿上 40 件新款衣服拍照做宣传。
不使用享元模式:
let Model = function(sex, underwear) {
this.sex = sex;
this.underwear = underwear;
}
Model.prototype.takePhoto = function () {
console.log('sex=' + this.sex + 'underwear = ' + this.underwear);
}
for(let i = 0; i < 20 ; i++){
new Model('男', 'underwear' + i).takePhoto();
}
for(let i = 0; i < 20 ; i++){
new Model('女', 'underwear' + i).takePhoto();
}
使用享元模式重构:
let ModelR = function( sex ) {
this.sex = sex;
}
let ModelF = new ModelR( '女' );
let ModelM = new ModelR('男');
ModelR.prototype.takePhoto = function () {
console.log('sex=' + this.sex + 'underwear = ' + this.underwear);
}
for(let i = 0; i < 20 ; i++) {
ModelF.underwear = 'underwear' + i;
ModelF.takePhoto();
}
for(let i = 0; i < 20 ; i++) {
ModelM.underwear = 'underwear' + i;
ModelM.takePhoto();
}
分析:利用所有对象相同的属性来初始化创建对象,上述例子中利用人的性别这个属性来创建对象,而性别这个属性只有男女这两种,因此我们只需要创建两个对象,将衣服作为其他不同的属性添加到对象中便完成了对象的替换,相当于拥有 40 个不同的对象,但是实际只创建了两个。
7.桥接
组合对象,分离抽象部分和实现部分,通过桥接的方式连接对象;沿着多个维度变化不会增加系统的复杂度,还可以达到抽象和实现解耦的目的。
举例:页面有 Toast、Message 两种形态的弹窗,弹窗都有出现和隐藏等行为,这些行为可以使用不同风格的动画。
function Toast(node, animation) {
this.node = node
this.animation = animation
}
//调用 animation 的show方法
Toast.prototype.show = function() {
this.animation.show(this.node)
}
//调用 animation 的hide方法
Toast.prototype.hide = function() {
this.animation.hide(this.node)
}
function Message(node, animation) {
this.node = node
this.animation = animation
}
//调用 animation 的show方法
Message.prototype.show = function() {
this.animation.show(this.node)
}
//调用 animation 的hide方法
Message.prototype.hide = function() {
this.animation.hide(this.node)
}
const Animations = {
bounce: {
show: function(node) { console.log(node + '弹跳着出现') }
hide: function(node) { console.log(node + '弹跳着消失') }
},
slide: {
show: function(node) { console.log(node + '滑着出现') }
hide: function(node) { console.log(node + '滑着消失') }
}
}
let toast = new Toast('元素1', Animations.bounce )
toast.show()
let messageBox = new Message('元素2', Animations.slide)
messageBox.hide()
三、行为型模式
观察者、模板方法、迭代器、责任链、策略、状态、备忘录、解释器、命令、访问者、中介者
一、单个对象行为封装:
1.策略
受外部状态影响改变对象行为;封装不同的对象行为,返回一个行为接口 。
应用:重构大量if-else逻辑或switch-case逻辑。
2.状态
受内部状态影响改变对象行为;封装不同的状态行为(包含对象行为)。
应用:重构大量if-else逻辑或switch-case逻辑,且分支逻辑中修改同一个状态。
举例:手写Promise
3.模板方法
将不同功能组合在一起,只提供框架,具体实现还需要调用者传进来。
举例:把东西放进冰箱
// 把东西放进冰箱
function putIntoIcebox(openFunc, pickFunc, putFunc){
openFunc()
pickFunc()
putFunc()
}
// 打开冰箱
function openIcebox(){}
// 抱起大象
function pickElephant(){}
// 放大象
function putElephant(){}
// 把大象放进冰箱
putIntoIcebox(openIcebox, pickElephant, putElephant)
// 抱起老虎
function pickTiger(){}
// 放老虎
function putTiger(){}
// 把老虎放进冰箱
putIntoIcebox(openIcebox, pickTiger, putTiger)
4.迭代器
提供一个接口,访问一个对象内的所有行为(属性),接口可提供多种访问顺序。
应用:数组遍历、对象遍历。
5.备忘录
为对象行为提供一个快照功能,能随时提供返回。
应用:状态管理数据持久化和还原,保证刷新页面应用状态不丢失。
二、依赖对象行为封装:
6.命令
对象行为一对一,分离请求和接收对象行为的分离,并可在其中添加其他操作,因为命令对象已经持有接受者的引用。
7.发布订阅(广义观察者)
对象行为一对多(发散:多个对象行为无关联),分离发布和多个订阅对象的行为。
应用:对象通信解耦。
举例:
export class EventBus {
static map = new Map();
static publish(key, data) {
const funcArr = this.map.get(key) || [];
for (let index = 0; index < funcArr.length; index++) {
funcArr[index](data);
}
}
static subscribe(key, callback) {
const funcArr = this.map.get(key) || [];
this.map.set(key, funcArr.push(callback));
}
}
8.职责链
对象行为一对多(串行:多个对象行为有关联),分离请求和多个接收对象的行为。
应用:try-catch的处理、作用域链、原型链、DOM节点中的事件冒泡。
class Chain {
construct (fn) {
this.fn = fn
this.successor = null
}
setNextSuccessor (successor) {
return this.successor = successor
}
next () {
return this.successor && this.successor.passRequest.apply(this.successor, arguments)
}
passRequest () {
const res = this.fn.apply(this, arguments)
if (res === 'nextSuccessor') {
return this.successor && this.successor.passRequest.apply(this.successor, arguments)
}
return res
}
}
看一个异步使用的例子:
const fn1 = new Chain(function () {
console.log(1)
return 'nextSuccessor'
})
const fn1 = new Chain(function () {
console.log(2)
setTimeout(() => {
this.next()
}, 1000)
})
const fn3 = new Chain(function () {
console.log(3)
})
fn1.setNextSuccessor(fn2).setNextSuccessor(fn3)
fn1.passRequest()
职责链优点:
- 符合单一职责,使每个方法中都只有一个职责。
- 符合开放封闭原则,在需求增加时可以很方便的扩充新的责任。
- 使用时候不需要知道谁才是真正处理方法,减少大量的
if
或switch
语法。
职责链缺点:
- 团队成员需要对责任链存在共识,否则当看到一个方法莫名其妙的返回一个 next 时一定会很奇怪。
- 出错时不好排查问题,因为不知道到底在哪个责任中出的错,需要从链头开始往后找。
- 就算是不需要做任何处理的方法也会执行到,因为它在同一个链中,如果有异步请求的话,执行时间也许就会比较长。
9.中介者
对象行为多对多,使网状的多对多关系变成了相对简单的一对多关系,本质上是对多个对象模块之间复杂交互的封装。
封装子系统间各模块之间的直接交互,松散模块间的耦合。
class A {
constructor () {
this.number = 0
}
setNumber (num, middleman) {
this.number = num
if (middleman) {
middleman.setB()
}
}
}
class B {
constructor () {
this.number = 0
}
setNumber (num, middleman) {
this.number = num
if (middleman) {
middleman.setA()
}
}
}
class Middleman {
constructor (a, b) {
this.a = a
this.b = b
}
setA () {
let number = this.b.number
this.a.setNumber(number * 100)
}
setB () {
let number = this.a.number
this.b.setNumber(number / 100)
}
}
const buyer = new A()
const seller = new B()
const middleman = new Middleman(buyer, seller)
buyer.setNumber(100, middleman)
console.log(buyer.number, seller.number) // 100 1
seller.setNumber(100, middleman)
console.log(buyer.number, seller.number) //10000 100
10.访问者
访问者模式先把一些可复用的行为抽象到一个函数(对象)里,这个函数我们就称为访问者(Visitor)。如果另外一些对象要调用这个函数,只需要把那些对象当作参数传给这个函数,在js里我们经常通过call或者apply的方式传递 this对象给一个Visitor函数。
// 接收者类,拥有accept方法
class Employee {
constructor(name, salary) {
this.name = name;
this.salary = salary;
}
getSalary() {
return this.salary;
}
setSalary(salary) {
this.salary = salary;
}
// accept方法接收访问者对象,将自身引用传递给visitor
accept(visitor) {
visitor.visit(this);
}
}
// 访问者类,具有visit方法,在接收者的accpet方法被调用
class Visitor {
constructor(times) {
this.times = times;
}
visit(employee) {
employee.setSalary(employee.getSalary() * this.times);
}
}
const emp = new Employee("小明", 3000);
const dobule = new Visitor(2);
const triple = new Visitor(3);
emp.accept(dobule);
console.log(emp.getSalary()); //6000
emp.accept(triple);
console.log(emp.getSalary()); //18000
11.解释器
对于一种语言,给出其文法表示形式,并定义一种解释器,通过使用这种解释器来解释语言中定义的句子。
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);