Javascript 设计模式
设计模式的五大设计原则(SOLID)
单一职责:一个程序只需要做好一件事。如果功能过于复杂就拆分开,保证每个部分的独立
开放封闭原则:对扩展开放,对修改封闭。增加需求时,扩展新代码,而不是修改源代码。这是软件设计的终极目标。
里氏置换原则:子类能覆盖父类,父类能出现的地方子类也能出现。
接口独立原则:保持接口的单一独立,避免出现“胖接口”。这点目前在TS中运用到。
依赖导致原则:面向接口编程,依赖于抽象而不依赖于具体。使用方只专注接口而不用关注具体类的实现。俗称“鸭子类型”
工厂模式
工厂模式是用来创建对象的常见设计模式,在不暴露创建对象的具体逻辑,而是将逻辑进行封装,那么它就可以被称为工厂。工厂模式又叫做静态工厂模式,由一个工厂对象决定创建某一个类的实例。
class Plant {constructor(name) {this.name = name;}grow() {console.log("grow up");}
}class Apple extends Plant {constructor(name, flavour) {super(name)this.flavour = flavour;}
}class Orange extends Plant {constructor(name, flavour) {super(name)this.flavour = flavour;}
}class Factory {static create(type) {switch (type) {case 'apple':return new Apple('xiao ping','sweet')case 'orange':return new Orange('xiao ju','sour')default:throw new Error("Unknown plant type")}}
}let apple = Factory.create('apple');
console.log(apple.flavour)
let orange = Factory.create('orange');
console.log(orange.flavour)
小结:
优点
调用者创建对象时只要知道其名称即可
扩展性高,如果要新增一个产品,直接扩展一个工厂类即可。
隐藏产品的具体实现,只关心产品的接口。
缺点
每次增加一个产品时,都需要增加一个具体类,这无形增加了系统内存的压力和系统的复杂度,也增加了具体类的依赖
单例模式
单例模式的思路是:保证一个类只能被实例一次,每次获取的时候,如果该类已经创建过实例则直接返回该实例,否则创建一个实例保存并返回。
单例模式很常用,比如:
vue项目中的Vue实例
node项目中的App实例
vuex react-redux 中的store
全局唯一的组件,像弹出框,模态窗口,购物车,登录框等
class LoginFrame {static instance = nullconstructor(state){this.state = state}show(){if(this.state === 'show'){console.log('登录框已显示')return}this.state = 'show'console.log('登录框展示成功')}hide(){if(this.state === 'hide'){console.log('登录框已隐藏')return}this.state = 'hide'console.log('登录框隐藏成功')}// 通过静态方法获取静态属性instance上是否存在实例,如果没有创建一个并返回,反之直接返回已有的实例static getInstance(state){if(!this.instance){this.instance = new LoginFrame(state)}return this.instance}
}
const p1 = LoginFrame.getInstance('show')
const p2 = LoginFrame.getInstance('hide')
console.log(p1 === p2) // true
小结:
优点
内存中只有一个实例,减少了内存的开销。
避免了对资源多重的占用。
缺点
违反了单一职责,一个类应该只关心内部逻辑,而不用去关心外部的实现
原型模式
function Person(name){this.name = name;
}Person.prototype.getName = function(){return this.name;
}let p1 = new Person('san')
let p2 = new Person('si')
console.log(p1.getName === p2.getName)
更多关于prototype的知识请去 JS原型与原型链详解
适配器模式
适配器模式的作用是解决两个软件实体间的接口不兼容的问题。使用适配器模式之后,原本由于接口由于接口不兼容而不能工作的两个软件实体可以一起工作。
class Power {charge(){return '220v'}
}class Adapter{constructor(){this.power = new Power()}charge(){let v= this.power.charge();return `${v} => 12v`}
}class Client{constructor(){this.adapter = new Adapter()}use(){console.log(this.adapter.charge())}
}let client = new Client()
client.use()
小结:
优点
让任何两个没有关联的类可以同时有效运行,并且提高了复用性、透明度、以及灵活性
缺点
过多的使用适配器模式,会让系统变得零乱,不易整体把控。建议在无法重构的情况下使用适配器。
代理模式
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”
let lily = {name: 'lily',age: '30',height: '160'
}// new Proxy(target, handler);
// new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。let lilyMa = new Proxy(lily, {//get方法的两个参数分别是目标对象和所要访问的属性。get(target, key) {if (key === 'age') {return target.age - 2} else if (key === 'height') {return target.height + 5} else {return target[key]}},set(target,key,value) {if(key ==='boyfriend'){let boyfriend = value;if(boyfriend.age >40){throw new Error('too old!')} else if(boyfriend.salary <20000){throw new Error('too poor!')} else{target[key] = value}}}
}) console.log(lilyMa.age)
lilyMa.boyfriend = {age:35,salary:25000,height:180}
小结:
代理模式符合开放封闭原则
本体对象和代理对象拥有相同的方法,在用户看来并不知道请求的本体对象还是代理对象。
代理模式 vs 适配器模式 适配器提供不同的接口,代理模式提供一摸一样的接口
代理模式 vs 装饰器模式 装饰器模式原来的功能不变还可以使用,代理模式改变原来的功能
优点
职责清晰,高扩展性,智能化
缺点
当对象和对象之间增加了代理可能会影响到处理的速度。
实现代理需要额外的工作,有些代理会非常的复杂。
观察者模式(订阅-发布模式)
观察者模式定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,
所有依赖于它的对象将得到通知。
如:当你给DOM绑定一个事件就已经使用了发布订阅模式,通过订阅DOM上的click事件,当被点击时会向订阅者发布消息。
class Star{constructor(name){this.name = name;this.state = '';this.observers = [];}getState(){return this.state;}setState(state){this.state = state;this.notifyAllObservers()}attach(observer){this.observers.push(observer)}notifyAllObservers(){if(this.observers.length>0){this.observers.forEach(observer=>observer.update())}}
}class Fun{constructor(name,star){this.name = name;this.star = star;this.star.attach(this)}update(){console.log(`${this.star.name} know ${this.star.getState()}`)}
}let s = new Star('liushishi')
let f = new Fun('me',s)
s.setState('xinai')
小结:
发布订阅模式可以使代码解耦,满足开放封闭原则
当过多的使用发布订阅模式,如果订阅消息始终都没有触发,则订阅者一直保存在内存中。
优点
观察者和被观察者它们之间是抽象耦合的。并且建立了触发机制。
缺点
当订阅者比较多的时候,同时通知所有的订阅者可能会造成性能问题。
在订阅者和订阅目标之间如果循环引用执行,会导致崩溃。
发布订阅模式没有办法提供给订阅者所订阅的目标它是怎么变化的,仅仅只知道它变化了。
装饰器模式
装饰者模式能够在不更改源代码自身的情况下,对其进行职责添加。相比于继承装饰器的做法更轻巧。通俗的讲我们给心爱的手机上贴膜,带手机壳,贴纸,这些就是对手机的装饰。
class Duck {constructor(name) {this.name = name;}eat(food) {console.log(`eat ${food}`)}
}class TangDuck {constructor(name) {this.duck = new Duck(name);}eat(food) {this.duck.eat(food);console.log('thanks')}
}let d = new TangDuck();
d.eat('apple');
还有ES6的Decorator
优点
装饰类和被装饰类它们之间可以相互独立发展,不会相互耦合,装饰器模式是继承的一个替代模式,它可以动态的扩展一个实现类的功能。
缺点
多层的装饰会增加复杂度
外观模式
外观模式本质就是封装交互,隐藏系统的复杂性,提供一个可以访问的接口。由一个将子系统一组的接口集成在一起的高层接口,以提供一个一致的外观,减少外界与多个子系统之间的直接交互,从而更方便的使用子系统。
class Sum{sum(a,b){return a+b}
}class Minus{minus(a,b){return a-b}
}
class Mutify{mutify(a,b){return a*b}
}
class Divide{divide(a,b){return a/b}
}class Calculate{constructor(a,b){this.sumc = new Sum(a,b);this.minusc = new Minus(a,b);this.mutifyc = new Mutify(a,b);this.dividec = new Divide(a,b);}sum(a,b){return this.sumc.sum(a,b)}minus(a,b){return this.minusc.minus(a,b)}mutify(a,b){return this.mutifyc.mutify(a,b)}divide(a,b){return this.dividec.divide(a,b)}
}let calculate = new Calculate()
console.log(calculate.sum(1,2))
优点
减少系统的相互依赖,以及安全性和灵活性
缺点
违反开放封闭原则,有变动的时候更改会非常麻烦,即使继承重构都不可行。
状态模式
允许一个对象在其内部状态改变的时候改变其行为,对象看起来似乎修改了它的类,通俗一点的将就是记录一组状态,每个状态对应一个实现,实现的时候根据状态去运行实现。
class SuccessState {show(){console.log('green')}
}class WarningState {show(){console.log('yellow')}
}class ErrorState {show(){console.log('red')}
}class Battery {constructor(){this.amout = 'high'this.state = new SuccessState()}show(){this.state.show();if(this.amout == 'high'){this.amout= 'middle';this.state = new WarningState()} else if(this.amout == 'middle'){this.amout= 'low';this.state = new ErrorState()}}
}let b = new Battery()
b.show()
b.show()
b.show()
优点
将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。
可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。
缺点
状态模式的使用必然会增加系统类和对象的个数。
状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
状态模式对"开闭原则"的支持并不太好,对切换状态的状态模式增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。
策略模式
策略模式指的是定义一系列算法,把他们一个个封装起来,目的就是将算法的使用和算法的实现分离开来。同时它还可以用来封装一系列的规则,比如常见的表单验证规则,只要这些规则指向的目标一致,并且可以被替换使用,那么就可以用策略模式来封装它们。
class Customer{constructor(kind){this.kind = kind;}pay(amount){return this.kind.pay(amount)}
}class Normal{pay(amount){return amount}
}class Member{pay(amount){return amount*.9}
}class VIP{pay(amount){return amount*.8}
}let c1 = new Customer(new Normal()).pay(100);
console.log(c1)
let c2 = new Customer(new Member()).pay(100)
console.log(c2)
let c3 = new Customer(new VIP()).pay(100)
console.log(c3)
优点
算法可以自由切换,避免了使用多层条件判断,增加了扩展性
缺点
策略类增多,所有策略类都需要对外暴露。