🏷设计模式-section1
ZengTianShengZ opened this issue · comments
JavaScript 中的设计模式
设计模式的**都是一样的,但语言的特性不一样,在实现方式上还是有出入的。这里用JavaScript 这门语言来简单探讨一下传统的一些设计模式,利用JavaScript这们语言的特性来实现这些传统的设计模式。
一、单例模式
单例模式: 保证一个类只有一个实例,并提供一个访问它的全局访问点
虽然定义是这么定义,但咱们�面对的是 JavaScript 这么语言,在实现上就可以结合特性来思考如何实现 单例模式
。
1、全局变量来实现
const a = {}
window.a = {}
全局变量不是单例模式,但一些情况下我们可以使用全局变量当成单例模式来使用。在浏览器下的做法经常使用 window 这全局唯一的对象来实现。
2、高阶函数结合闭包实现
const getSingle = function (fn) {
let result
return function () {
return result || (result = fn.apply(this, arguments))
}
}
result 用来保存 fn 的计算结果,因为 result 变量在闭包中,不会销毁,当被赋值过一次时 getSingle 函数会直接返回该结果
3、应用情况
什么情况下我们只需要一个对象呢,比如线程池,全局缓存,或者是登录的浮窗只需创建一次等等。
下面以创建一个单一浮窗为例子
const getSingle = function (fn) {
let result
return function () {
return result || (result = fn.apply(this, arguments))
}
}
const creatLayer = function() {
var div = document.createElement('div')
div.innerHTML = '浮窗'
document.body.appendChild(div)
return div
}
const getLayer_1 = getSingle(creatLayer)
const getLayer_2 = getSingle(creatLayer)
console.log(getLayer_1 === getLayer_2) // ture
二、策略模式
策略模式: 定义一系列的算法,把他们一个个封装起来,并且使他们可以相互替换
1、实现
策略模式一般由两部分组成。一是 策略类
,策略类
封装了具体的算法,并负责具体的计算过程。二是 环境类(calculateBons)
,环境类
接受客户的请求,并把请求委托给某一个策略类。
一个例子加以说明:如,绩效为S级,奖励金为其4倍工资,A级3倍工资,B级2倍工资,你会如何实现呢,是用一堆 if-else 去判断吗
// 策略类,封装一系列算法
const strategies = {
"S": function(salary){
return salary * 4
},
"A": function(salary){
return salary * 3
},
"B": function(salary){
return salary * 2
}
}
// 环境类
const calculateBons = function(lever, salary){
return strategies[lever](salary)
}
let result_1 = calculateBons("B", 2000) // 输出 4000
let result_2 = calculateBons("S", 5000) // 输出 20000
也许你看到上面的实现方式觉得没必要这么麻烦,直接在 环境类
里面做一些 if - else 的判断不是�也能照样使用该功能么,但想想,如果该功能后续需要支持 C级1倍薪资 的需求,那是不是要修改 环境类
的代码呢,根据 单一职责原则
,策略模式就�能很好的实现需求的扩展。
"C": function(salary){
return salary * 1
}
let result_3 = calculateBons("C", 1000) // 输出 1000
3、应用
什么情况下我们需要使用 策略模式
呢,比如多种表单的校验、一种场景多种身份的切换、有多重 if-else 分支的情况,那可以考虑下 策略模式
三、代理模式
代理模式:为一个�对象提供一个代用品或占位符,以控制对它的访问
`客户` -> `本体` // 不用代理
`客户` -> `代理` -> `本体` // 使用代理
1、实现
代理模式没有具体的模板,根据不同的使用场景产生不同的代理
一个图片加载的例子:
网页加载大图片时有时候因为网络问题而出现页面空白,可以通过代理,在加载图片的时候先显示一张 loading 图片
// 本体 - 图片加载
var myImage = (function(){
var imgNode = document.createElement('img')
document.body.appendChild(imgNode)
return {
setSrc : function(src){
imgNode.src = src
}
}
})()
// 代理 - 图片加载前先 loading
var proxyImg = (function(){
var img = new Image()
img.onload = function(){
myImage.setSrc(this.src)
}
return {
setSrc : function(src){
myImage.setSrc('./img/loading.gif')
img.src = src
}
}
})()
// 客户 - 加载一张图片
proxyImg.setSrc('http://img.com.xxxxx.jpg')
上面可以看出使用代理的两点好处:
-
单一职责原则
: 一个类(对象或函数),应该只有一个引起它变化的原因。上面图片加载,使用了代理 proxyImg 去做网络请求图片,职责单一 , myImage 去做图片显示,职责也是单一 -
开放封闭原则
: 软件实体(类、模块、函数等)应该是可以扩展的,但是不可修改的。如果几年后网速大幅提高,那就不必使用 proxyImg 方法来预加载一张 loading 图片,那上面的写法完全不用修改,去掉 proxyImg 方法,直接用 myImage 方法即可
2、应用
代理模式应用在哪些地方呢,可以是 请求代理、缓存代理、节流代理,总之如果需要在 客户
和 本体
之间多做一些操作,就可以考虑引入代理模式
四、发布-订阅模式
发布-订阅模式:它定义对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都将得到通知
1、初步理解 发布-订阅模式
其实发布-订阅模式我们经常用到,如常用的事件监听就是一个发布-订阅模式
document.body.addEnentListener('click', function () {
console.log('on click');
}, false)
document.body.click(); // 模拟用户点击
在监听事件中,body
向 用户
订阅了一个点击事件,用户
发布了一个点击后,就通知 body
做出响应。
2、实现
const event = {
clientList: [],
listen(key, fn) {
if (!this.clientList[key]) {
this.clientList[key] = [];
}
this.clientList[key].push(fn); // 订阅的消息加进缓存列表
},
tigger() {
const key = Array.prototype.shift.call(arguments);
const fns = this.clientList[key];
if (!fns || fns.length === 0) { // 如果没有绑定对应的消息
return false;
}
for (let i = 0, fn = null ; fn = fns[i++];) {
fn.apply(this. arguments);
}
}
}
event.listen('1点', function(doing) {
console.log(doing);
})
event.listen('2点', function(doing) {
console.log(doing);
})
event.tigger('1点', '吃饭') // 吃饭
event.tigger('2点', '睡觉') // 睡觉
在 event 中,我们定义了个 listen 方法来�提供订阅消息,定义了个 tigger 方法来实现�发布消息
3、应用
发布-订阅模式可以应用在什么地方呢,比如在异步编程中,�可以代替回调的方式来通知异步事件;在模块间的通信也可以应用发布-订阅模式,常见的就是 eventBus ,做信息通信。
五、组合模式
组合模式:用小的子对象来构建更大的对象
1、实现
有这么个需求场景,需要执行一个统一操作,从而触发一系列的子操作。
const command = function () {
return {
commandList: [],
add(command) {
this.commandList.push(command)
},
execute() {
for (var i = 0, command; command = this.commandList[i++];) {
command.execute();
}
}
}
}
const command1 = {
execute() {
console.log('----command1---');
}
}
const command2 = {
execute() {
console.log('----command2---');
}
}
const cmd = command()
cmd.add(command1)
cmd.add(command2)
cmd.execute()
在 command 方法中,我们返回一个对象,定义了 add 方法来添加子操作,定义 execute 方法来执行所有的子操作。
3、应用
如果你的需求遇到树形结构,需要多种组合并统一操作,那可以考虑下组合模式
六、享元模式
享元模式:运行共享技术有效地支持大量细粒度的对象,避免大量拥有相同内容的小类的开销(如耗费内存),使大家共享一个类(元类)
1、实现
有这么个例子,假设有个内衣工厂,目前的产品有50种男士内衣与50种女士内衣,为了推销产品,工厂决定生产一些塑料模特来穿上它们的内衣拍成内衣广告
var Model = function (sex, underwear) {
this.sex = sex;
this.underwear = underwear;
}
Model.prototype.takePhoto = function () {
console.log('sex=' + this.sex + 'underwear=' + this.underwear)
}
for (var i = 1; i <= 50; i++) {
var maleModel = new Model('male', 'underwear' + i)
maleModel.takePhoto();
}
for (var i = 1; i <= 50; i++) {
var maleModel = new Model('female', 'underwear' + i)
femaleModel.takePhoto();
}
在这个例子中我们实例化了 100 个对象,想想要是10000套衣服或更多呢,性能将会下降。
我们用享元模式来优化
2、外部状态和内部状态
享元模式
最主要的是区分外部状态
和内部状态
,上面例子由于 sex 属性是区分 Model 的性别,属于内部状态
,属性 underwear 为所有 Model 共有,属于外部状态
,所以我们用享元模式
优化出来的代码如下:
var Model = function (sex) {
this.sex = sex;
}
Model.prototype.takePhoto = function () {
console.log('sex=' + this.sex + 'underwear=' + this.underwear)
}
var maleModel = new Model('male');
var female = new Model('female');
for (var i = 1; i <= 50; i++) {
maleModel.underwear = 'underwear' + i;
maleModel.takePhoto();
}
for (var i = 1; i <= 50; i++) {
female.underwear = 'underwear' + i;
femaleModel.takePhoto();
}
3、应用
享元模式是为了解决性能问题而生的模式,在一个存在大量相似的对象系统中,享元模式可以很好的解决大量对象带来的性能问题。
七、装饰者模式
装饰者模式:在不改变对象自身的基础上,在程序运行期间给对象动态的添加职责
1、简单实现
例子:一个战斗机有发射子弹的功能,后来装备加强了,还具有发射导弹的功能。
const Plane = {
fire() {
console.log('-----发射子弹');
}
}
Plane.fire() // -----发射子弹
// 升级后
const fireAtom = function () {
console.log('-----发射原子弹');
}
const fire = Plane.fire
Plane.fire = function () {
fire()
fireAtom()
}
Plane.fire() // -----发射子弹, -----发射原子弹
2、用 AOP(面向切面编程) 装饰函数
上面例子的做法有点不妥,因为我们改变了原有函数的内部实现,下面使用 AOP 装饰函数
const Plane = {
fire() {
console.log('-----发射子弹');
}
}
Plane.fire() // -----发射子弹
// 升级后
const fireAtom = function () {
console.log('-----发射原子弹');
}
Function.prototype.after = function (afterFn) {
const _this = this;
return function () {
const ret = _this.apply(this, arguments);
afterFn.apply(this, arguments)
return ret
}
}
Plane.fire.after(fireAtom)() // -----发射子弹, -----发射原子弹
上面例子中我们没有更改对象的原有方法,而是添加了个 after 函数,在原有 fire 函数执行完执行 fireAtom 函数。
3、应用
如果你有遇到需要在不改变原有函数内部实现的情况下给该函数添加功能,可以使用装饰者模式。
8、小结
前面我们学习了一些常用的设计模式,设计模式可以说是在一些特定的场景下使用的一套编程规则,使之代码更加健壮易于扩展和维护。在常规的编码中我们也可以遵循一些编程原则,比如单一职责,最少知识原则等。虽然这些规则在编码过程中不强制要求,但这些前辈们总结出的经验有利于我们代码的健壮和可维护。
1、单一职责原则
单一职责原则(SRP):一个对象(方法)只做一件事。
还记得我们前面讲的 单例模式
吗,用到的就是单一职责原则,在单例方法中,我们只做一件事,创建单例,至于是一个弹窗单例还是对象单例,我们不管,交由入参函数 fn 去实现。
const getSingle = function (fn) {
let result
return function () {
return result || (result = fn.apply(this, arguments))
}
}
2、最少知识原则
最少知识原则(LKP):一个软件实体应当尽可能少地与其他实体发生相互作用。
也就是尽可能的减少对象或方法间的调用,减少对象或方法之间的联系。
不过这好像 最少知识原则
和 单一职责原则
存在了冲突,在实际开发中怎么选择 最少知识原则
或 单一职责原
还要依据具体的环境来定。
3、开放-封闭原则
开放-封闭原则(OCP): 软件实体(类、模块、函数)等应该是可以扩展的,但不可修改。
由上面的定义咱们可以很快的联想到前面文章提到的设计模式很多都有用到该原则,如 装饰者模式
, 我们在不改变原因函数 fire 的内部逻辑,添加了个函数 after 实现了可扩展。
Function.prototype.after = function (afterFn) {
const _this = this;
return function () {
const ret = _this.apply(this, arguments);
afterFn.apply(this, arguments)
return ret
}
}
Plane.fire.after(fireAtom)() // -----发射子弹, -----发射原子弹
还有 发布-订阅模式
, 代理模式
都有用到了 开放-封闭原则
。
总结:
本篇讲了一些传统的设计模式和一些使用场景,没有深入的去分析具体的推导过程,只是让大家有个印象,当在实际开发过程中如有遇到对应的场景,可以考虑能否套用一些设计模式进去,让你的代码更加健壮合理。