javascript设计模式(设计原则+设计模式+代码重构)

建议阅读前有一定的js基础
本博客内容借鉴于javascript设计模式与开发实践

设计原则

单一职责原则

一个对象(方法)只做一件事情。
如果我们有两个动机去改写一个方法,那么这个函数就有两个职责,在需求的变更中,改造这个函数的可能性就越大。通常这个函数就会是一个不稳定的方法,修改代码总是一个危险的事情,特别是职责太多的时候难免就会发生影响别的职责的实现,造成意想不到的bug
举例:

  1. 代理模式(例如图片只执行加载,但是如果需要图片加载好之前展示加载图片就不能写到加载图片的代码里面,这就违反了单一职责)
  2. 迭代器模式 (我们平时迭代需要for循环,但是for本身就是做的是循环,不能在循环里面再加别的功能代码,所以有了forEach,map,find等方法)
  3. 装饰器模式。类似于人穿衣服,可以根据心情,天气等穿自己喜欢的衣服,但是无论穿什么衣服都不会影响我们自身
优缺点

优点:有利于代码解耦与复用,当一个职责发生变化的时候不会影响到其他的职责
缺点:增加代码的复杂度,当我们把功能拆成细小颗粒的时候,也会加大对象间的联系难度

最少知识原则(迪米特法则)

尽可能减少对象之间的联系,与单一职责有点相对应
举个例子,老板要做一个项目,把事情告诉经理,然后经理处理后再交给负责人,负责人再人员工作,员工工作好了之后负责人确认,负责人再通知经理,经理再告诉老板,想一想这个关系链就很复杂,而且中间不能变化,如果某一链断开了就会出问题,比如负责人请假了,老板就一直得不到反馈。但是老板可以直接告诉经理,你去把这个项目完成,怎么做我不关心,你只要做好了告诉我一声就好
常用模式:
中介者模式(有一个共有对象专门处理各个用户的消息,然后再发送给指定对象)
闭包(封装通常是对过程的封装和细节的隐藏,只暴露给外界特定的api)

优缺点:
减少了对象之间的引用,但是可能会存在一个特别庞大的中间对象

开放-封闭原则

代码应该是可以扩展的,不应该是可以修改的(通俗的说就是新功能不要改老代码,应该扩展旧代码)
改代码是一件很危险的事情,稍不注意就会产生很大的问题,所以我们应该在不动旧代码的前提下完成新功能,可以考虑从以下几个方面实现该原则

  1. 子类的数量是无限制的,总有一些个性化的子类逼迫我们已封装的代码。所以在程序可能发生变动的地方放置一个挂钩,挂钩的返回结果决定程序的下一个方向。
  2. 使用回调函数,例如map,filter,every

常见模式

  1. 策略模式
  2. 发布-订阅模式
  3. 代理模式
  4. 职责链模式

设计模式

单例模式

保证一个类只有一个实例,并提供一个访问他的全局访问点

常见的单例

const Single = function(name) {
	this.name = name;
}
Single.prototype.say = function() {
	console.log(this.name);
}
Single.instance = null;
Single.getInstance = function(name) {
	if (!this.instance) {
		this.instance = new Single(name);
	}
	return this.instance;
}
const s1 = Single.getInstance("张三")
const s2 = Single.getInstance("李四")
console.log(s1 === s2)  // true

但如果使用以上的方法仍会存在一些可以优化的地方,比如常见的jquery的one(只点击一次),或者vue的.once等,都是用的单例模式,如果我们每写一个按钮,都要加一段getInstance方法,,无疑是加了很多代码

let hasClickA = false;
let hasClickB = false;
a.onclick = function() {
	if (hasClickA) {
		return;
	}
	hasClickA = true;
	...
}
b.onclick = function() {
	if (hasClickB) {
		return;
	}
	hasClickB = true;
	...
}

以上代码中有很多的重复代码,也不符合单一原则,既要保证只点击一次,又要执行第一次的代码,可以看到有共同代码

let flag = false;
if (!flag) {
    ...
}

所以应当将这部分代码抽离出来

function getInstance(fn) {
    let result;

    return function() {
        return result || (result = fn.apply(this, arguments))
    }
}

实际效果

function getInstance(fn) {
    let result;

    return function() {
        return result || (result = fn.apply(this, arguments))
    }
}

let say = () => {
    console.log('hello world')
    return true
}

let hello = () => {
    console.log('hello')
    return true
}

let sayInstance = getInstance(say)
let helloInstance = getInstance(hello)

sayInstance()
sayInstance()
sayInstance()

helloInstance()
helloInstance()
helloInstance()

以上代码尽管两个函数各执行三次,但是只会输出一次hello和hello world,即可实现效果,当然,只是简单举一个例子,实际中根据实际需要完成代码

策略模式

定义一系列算法,把他们一个个的封装起来,并且可以相互替换
常见场景:
1.压缩,定义好各种压缩方式,但是实际根据需要使用哪种
2.导航,定义好两个地址间的各种通行方式,根据实际选择走那一个就导航那一个
3.表单验证

举个例子,年终发年终奖,绩效s的发薪资的4倍,绩效a的发3倍,绩效b的发2倍,所以有一下代码

function calc(level, salary) {
	if (level === 's') {
		return salary * 4;
	}
	if (level === 'a') {
		return salary * 3;
	}
	if (level === 'b') {
		return salary * 2;
	}
}
console.log(calc('s', 10000));  // 40000
console.log(calc('a', 8000));  // 24000
console.log(calc('b', 6000));   // 12000

可以发现以上代码比较简单,但是又存在几个问题
1.函数比较庞大,又有很多的if判断,即便改成switch,也需要覆盖所有的判断
2.缺乏弹性,如果要新增一种绩效,或者要修改一种条件,必须要到函数内部实现,违反开放-封闭原则
3.复用性差,如果别的地方需要的话,需要cv再加部分修改
所以需要对代码进行修改,如下

function calcS(salary) {
    return salary * 4;
}

function calcA(salary) {
    return salary * 3;
}

function calcB(salary) {
    return salary * 2;
}

function calc(level, salary) {
    if (level === 's') {
        return calcS(salary);
    }
    if (level === 'a') {
        return calcB(salary);
    }
    if (level === 'b') {
        return calcB(salary);
    }
}

console.log(calc('s', 10000))
console.log(calc('a', 8000))
console.log(calc('b', 6000))

以上代码虽然进行了一些改善,但是仍存在函数越来越庞大,弹性不足的问题,可以考虑使用策略模式,主要就是用于算法的实现和算法的使用相分离。
一个策略模式的应用,应该包含两部分,一部分是算法的实现和计算,一部分是对于用户的数据的处理和委托算法,例如以下传统的面向对象的策略模式

function performanceS() {}
performanceS.prototype.calculate = function(salary) {
    return salary * 4;
}

function performanceA() {}
performanceA.prototype.calculate = function(salary) {
    return salary * 3;
}

function performanceB() {}
performanceB.prototype.calculate = function(salary) {
    return salary * 2;
}

function Bonus() {
    this.salary = null;    // 员工的工资
    this.strategy = null;   // 员工工资对应的策略对象
}

Bonus.prototype.setSalary = function(salary) {
    this.salary = salary;   // 设置工资
}

Bonus.prototype.setStrategy = function(strategy) {
    this.strategy = strategy;   // 设置工资对应的策略对象
}

Bonus.prototype.getBonus = function() {
    if (!this.strategy) {
        throw new Error("未设置策略对象");
    }
    return this.strategy.calculate(this.salary);
}

const bonus1 = new Bonus();
bonus1.setSalary(10000);
bonus1.setStrategy(new performanceS());
console.log(bonus1.getBonus());   // 40000

const bonus2 = new Bonus();
bonus2.setSalary(8000);
bonus2.setStrategy(new performanceA());
console.log(bonus2.getBonus());    // 24000

策略模式的思想就是定义一系列算法,把他们一个个的封装起来,并且可以相互替换。换句话说就是定义一系列算法,各自封装为策略类,算法被封装在策略类内部的方法中,对于用户的请求会委托给策略中对象的某一个进行计算。
可以看到在重构后,代码显得更加清晰,职责更加鲜明。对于js的设计模式,可以进行以下操作,显得更加简洁

const strategies = {
    's': function(salary) {
        return salary * 4;
    },
    'a': function(salary) {
        return salary * 3;
    },
    'b': function(salary) {
        return salary * 2;
    },
}

function calc(level, salary) {
    return strategies[level](salary);
}

console.log(calc('s', 10000));
console.log(calc('a', 8000));

表单验证

// 当前值 const form = { username: "", password: "" };
// 提交的函数
function submit() {
	if (form.username === "") {
		alert("用户名不能为空");
		return false;
	}
	if (form.password.length < 6) {
		alert("密码最少六位");
		return false;
	}
	...
}

以上代码完成了对于登录的数据验证,但是和上面计算奖金的代码一样,多个if判断,弹性差,复用性差,可以考虑使用策略模式重构

// 定义一些初始规则
const strategies = {
    isNonEmpty: function(value, errMsg) {
        if (value === '') {
            return errMsg;
        }
    },
    minLength: function(value, length, errMsg) {
        if (value.length < length) {
            return errMsg;
        }
    },
    ...
}

我们期望实现一下效果

function validateFunc() {
    const validator = new Validator();    // 创建验证对象
    // 添加一些验证规则
    validator.add(form.username, "isNonEmpty", "用户名不能为空")
    validator.add(form.password, "minLength:6", "密码不能为空")
    const errMsg = validator.validate();  // 获取验证结果
    return errMsg;  // 返回结果  
}

function submit() {
    const errMsg = validateFunc();   // 如果有错误信息,代表验证未通过
    if (errMsg) {
        alert(errMsg);
        return false;
    }
    ...
}

接下来,我们只要实现Validator类就可以了

const Validator = function() {
    this.cache = [];  // 保存校验规则
}

Validator.prototype.add = function(dom, rule, errMsg) {
    const ary = rule.split(":");   // 处理规则为minLength:6这种情况,根据实际情况来
    this.cache.push(function() {
        const strategy = ary.shift();   // 把类型和参数分离开来,minLength用于strategies中的key,第二个用于minLength中的第二个参数,此时ary应为[]或者[rule的参数]
        ary.unshift(dom);  // 此时ary应该是[要验证的数据]或[要验证的数据,rule的参数]
        ary.push(errMsg);  
        return strategies[strategy].apply(dom, ary);  
    });
}


Validator.prototype.validate = function() {
    for(let i = 0, validateFunc; validateFunc = this.cache[i++];) {   
        const msg = validateFunc();
        if (msg) {
            return msg;
        }
    }
}

通过重构,我们可以实现仅通过配置的方式就可以完成表单验证,复用性很强,也可以通过插件的形式移植到其他项目,当然上面只是简单一个配置,例如antd或者elementui等rules,是支持多种验证格式的,可以再add方法添加验证时自定义格式支持,这里就不延申了

小注

在js中函数是第一公民,上面所写是传统的面向对象的写法,在js中,函数的多态性更将简单,例如上面那个计算奖金的函数,用js可以做以下写法

function calcS(salary) {
    return salary * 4;
}

function calcA(salary) {
    return salary * 3;
}

function calcB(salary) {
    return salary * 2;
}

function calc(func, salary) {
	return func(salary)
}

console.log(calc(calcS, 10000))   // 40000

代理模式

为一个对象提供一个替代品或者占位符,以便控制对他的访问
常见使用:
1.图片预加载:先使用自定义图片,等图片加载好后进行替换
生活例子:

  1. 男生向女生表白,先找闺蜜做代理
  2. 明星的经纪人

例如:小明想向A送花,通过A的好友B送给A

const Flower = function() {}

const xiaoming = {
    sendFlower: function(target) {
        const flower = new Flower();
        target.receiveFlower(flower);
    }
}

const A = {
    receiveFlower: function(flower) {
        console.log("收到花 " + flower);
    }
}

const B = {
    receiveFlower: function(flower) {
        A.receiveFlower(flower);
    }
}

xiaoming.sendFlower(B)

虽然看起有些复杂,但是引用了代理,是一个不错的起点。接下来我们改变一下情况,小明把话交给B,B会在A心情好的时候交给A。所以小明只管把花交给B,B会监听A的心情,如果A的心情很好的话B会交给A

const Flower = function() {}

const xiaoming = {
    sendFlower: function(target) {
        const flower = new Flower();
        target.receiveFlower(flower);
    }
}

const A = {
    receiveFlower: function(flower) {
        console.log("收到花 " + flower);
    },
    listenMood(fn) {
        // 举个例子,A将会在10秒后心情好,可以执行fn
        setTimeout(() => {
            fn();
        }, 10 * 1000)
    }
}

const B = {
    receiveFlower: function(flower) {
        // A.receiveFlower(flower);
        A.listenMood(function() {
            A.receiveFlower(flower)
        })
    }
}

xiaoming.sendFlower(B)

图片预加载

// 页面需要显示一张图片,我们的代码如下
const myImage = (function() {
	const img = document.createElement('img');
	document.body.appendChild(img);
	return {
		setSrc: function(src) {
			img.src = src;
		}
	}
})();
myImage.setSrc('xxxxxx')

上面图片虽然也没什么问题,但是如果图片过大或者网速过慢的时候,就会有很大的一块空白时间,这个时候就需要引用代理,一开始显示一张loading图或者其他,等到图片加载结束后再显示图片

const myImage = (function() {
	const img = document.createElement('img');
	document.body.appendChild(img);
	return {
		setSrc: function(src) {
			img.src = src;
		}
	}
})();

const proxyImage = (function() {
	const img = new Image();
	img.onload = function() {
		myImage.setSrc(this.src);
	}
	return {
		setSrc: function(src) {
			myImage.setSrc('loading.gif');
			img.src = src;
		}
	}
})

proxyImage.setImage("xxxxxx")

其实这个代理也可以不用,直接给myImage中加入img.onload事件,但是这样做违反了单一职责原则。myImage既要加载图片,又要实现预处理。一个对象如果承担的职责过多,就会耦合度太高,后面如果稍有改动,就会处理过多的事情。比如如果几年后网速足够快,根本不需要预加载,这里只要不用proxyImage就可以,不会改动太多

迭代器模式

这里就不作详细说明了,就比如for循环能实现的功能,要封装成map,every,find,each等,优势在于把逻辑放在callback中,职责更加清晰

发布订阅模式

定义对象间的一种一对多的依赖关系,当一个对象状态发生改变的时候,所有依赖于它的对象都会收到通知
现实中的例子:
超市再特定时间有活动,但是顾客不能每天都打电话询问,于是超市建立了一个群,只要有活动,就在群里发布一条消息让所有知道,或者有电话,一个个的打电话通知
工作中的例子

dom.onclick = function() {   // 如果是多个事件绑定在一个元素上,建议使用addEventListener
	...
}

我们没办法知道用户什么时候点击,但是通过发布订阅模式,我们就可以在用户点击的时候收到通知
下面我们以超市为例做一个发布订阅模式

const supermarket = {}; // 定义超市
supermarket.clientList = [];  // 存放订阅者的回调函数

supermarket.listen = function(fn) {  // 增加订阅者
    this.clientList.push(fn);  // 把订阅的消息放进缓存列表
}

supermarket.trigger = function() {  // 发布消息
    for(let i = 0, fn; fn = this.clientList[i++];) {
        fn.apply(this, arguments)
    }
}


// 测试添加订阅
supermarket.listen(function(name, money) {  // 用户一订阅
    console.log(name, money);
})

supermarket.listen(function(name, money) {  // 用户二订阅
    console.log(name, money);
})

// 发布消息
supermarket.trigger('五花肉', 10);
supermarket.trigger('土豆', 1);

以上我们实现了最简单的发布订阅模式,但是还有一个问题,我们有的用户想买肉,有的用户想买土豆,但是用户只想对自己的消息订阅,与自己无关的不想订阅,所以仍要改进,我们要增加一个key来标记用户的订阅

const supermarket = {}; // 定义超市
supermarket.clientList = {};  // 存放订阅者的回调函数

supermarket.listen = function(key, fn) {  // 增加订阅者
    if (!this.clientList[key]) {  // 如果没有订阅过此类消息,就给这类消息创建一个缓存列表
        this.clientList[key] = [];
    }
    this.clientList[key].push(fn)
}

supermarket.trigger = function() {  // 发布消息
    const key = Array.prototype.shift.call(arguments),  // 获取消息类型
        fns = this.clientList[key];   // 取出消息对应的回调函数
    if (!fns || fns.length === 0) {
        return false;
    }
    for (let i = 0, fn; fn = fns[i++];) {
        fn.apply(this, arguments)
    }
}


// 测试添加订阅
supermarket.listen('土豆', function(money) {  // 用户一订阅
    console.log(money);
})

supermarket.listen('五花肉', function(money) {  // 用户二订阅
    console.log(money);
})

// 发布消息
supermarket.trigger('五花肉', 10);
supermarket.trigger('土豆', 1);

这样我们就是先所有的订阅者只订阅自己感兴趣的事情了,那么问题来了,如果不在这个超市买东西,去另外一个超市买东西,代码需要再复制一份吗?当然不用,我们可以把发布订阅的功能提取出来,放在一个单独的对象里

const event = {
    clientList: {},
    listen: function(key, fn) {  // 增加订阅者
        if (!this.clientList[key]) {  // 如果没有订阅过此类消息,就给这类消息创建一个缓存列表
            this.clientList[key] = [];
        }
        this.clientList[key].push(fn)
    },
    trigger: function() {  // 发布消息
        const key = Array.prototype.shift.call(arguments),  // 获取消息类型
            fns = this.clientList[key];   // 取出消息对应的回调函数
        if (!fns || fns.length === 0) {
            return false;
        }
        for (let i = 0, fn; fn = fns[i++];) {
            fn.apply(this, arguments)
        }
    }
}

function init(obj) {
    for(let key in event) {
        obj[key] = event[key];
    }
}

const supermarket2 = {};
init(supermarket2);
supermarket2.listen(...)
supermarket2.trigger(...)

当然也可以删除订阅,在此代码不做展示,可以再supermarket的listen同级加一个remove函数,感兴趣可以自己去试试

命令模式

有时候需要向某些对象发送请求,但是并不知道接收者是谁,也不知道被请求的操作是什么。此时希望用一种松耦合的方式设计程序,使请求发送者和接收者能够消除彼此间的耦合关系
比如说,我们点外卖,我们并不知道商家是谁接收了单子,又是谁在做饭,但是商家会拿个订单(命令)自己去处理,在一个小时后将餐送到你的手上,从而实现松耦合的联系

下面有个场景,一个项目由一部分人去做html+css部分,有很多按钮,有一部分去实现点击按钮的功能,写按钮的人也不知道点击按钮会触发什么,只知道会触发某个事件,所以要怎么绑定onclick呢?

...一些按钮和一些样式

定义一个setCommand函数,负责给按钮上装命令,至于执行什么并不关系

const setCommand = function(button, command) {
	button.onclick = function() {
		command.execute();
	}
}

这个时候负责事件部分的员工实现了他们的成功

const MenuBar = {
	refresh: function() {
		console.log("刷新");
	}
}

const SubMenu = {
	add: function() {
		console.log("添加");
	},
	del: function() {
		console.log("删除");
	}
}

在使用前,我们把他们封装在命令类中

const RefreshMenuBarCommand = function(receiver) {
	this.receiver = receiver;
}
RefreshMenuBarCommand.prototype.execute = function() {
    this.receiver.refresh();
}

const AddSubMenuCommand = function(receiver) {
	this.receiver = receiver;
}
AddSubMenuCommand.prototype.execute = function() {
    this.receiver.add();
}

const DelSubMenuCommand = function(receiver) {
	this.receiver = receiver;
}
DelSubMenuCommand.prototype.execute = function() {
    this.receiver.del();
}

把命令接收者传入到command中,并安装到button上

const refreshMenuBarCommand = new RefreshMenuBarCommand(MenuBar);
const addSubMenuCommand = new AddSubMenuCommand(SubMenu);
const delSubMenuCommand = new DelSubMenuCommand(SubMenu);

setCommand(btn1, refreshMenuBarCommand);                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          
setCommand(btn2, addSubMenuCommand);
setCommand(btn3, delSubMenuCommand);

以上是传统面向对象的命令模式,在js中也可以这样写

function buttonClick(button, fn) {
    button.onclick = fn;
}


const MenuBar = {
	refresh: function() {
		console.log("刷新");
	}
}

const SubMenu = {
	add: function() {
		console.log("添加");
	},
	del: function() {
		console.log("删除");
	}
}

buttonClick(btn, SubMenu.add);

享元模式

享元模式是一种用于性能优化的模式。例如系统中因为创建了大量类似的对象导致内存过高,这个时候享元模式就很有用了

举个例子,有一个内衣工厂,有50中男士内衣和50中女士内衣,需要一些塑料模特穿上拍照,正常下需要50个男模特和50个女模特分别穿上一件内衣拍照


function Model(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 < 50; i++) {
	const maleModel = new Model("male", `underwear${i}`)
	maleModel.takePhoto()
}

for (let j = 0; j < 50; j++) {
	const femaleModal = new Model("female", `underwear${j}`)
	femaleModal.takePhoto()
}

事实上我们并不需要50个男模特和50个女模特,只需要一个男模特和一个女模特,分别穿上50件内衣然后拍照即可

function Model(sex) {
	this.sex = sex;
}
const maleModel = new Model("male")
const femaleModal = new Model("female")

Model.prototype.takePhoto = function() {
	console.log(`sex = ${this.sex} && underwear = ${this.underwear}`)
}

for (let i = 0; i < 50; i++) {
	maleModel.underwear = `underwear${i}`
	maleModel.takePhoto()
}

for (let j = 0; j < 50; j++) {
	femaleModal.underwear = `underwear${j}`
	femaleModal.takePhoto()
}

以上就是享元模式的雏形。享元模式要求将对象的属性划分为内部状态和外部状态,目标是尽量减少共享对象的数量。所以就有了以下要求

  • 内部状态储存于对象内部
  • 内部状态可以被一些对象共享
  • 内部状态独立于具体场景,通常不会改变
  • 外部状态取决于具体的场景,并根据场景而变化,不能被共享

对象池共享

类似于java的对象池,当某一个字符串已存在在对象池中,就返回这个对象的引用,否则就添加进对象池并返回引用。这里使用的共享,而不是纯粹的享元模式。
理解原理:假如我们每个人都要有同一本书,从节约的角度来看并不很划算,因为这些书平时都被闲置在各自的书架上。所以我们成立一个图书角,图书角没有的书我们就买来放上去,有的数我们就直接拿来看

下面举个例子,当我们打开地图的时候,页面在附近有两个标志,我们叫他tootip,当我搜索某些地址的时候,图上不光有原先的两个标志,还多了我搜索的四个标志。按对象池的思路,我们搜索的时候应该创建4个而不是6个,因为之前那两个已经被创建了。

const tooltipFactory = (function() {
	const tooltipPool = [];
	return {
		// 创建
		create: function() {
			// 没有则创建
			if (tooltipPool.length === 0) {
				const div = document.createElement("div");
				document.body.append(div);
				return div;
			} else {
				// 有则返回
				return tooltipPool.shift();
			}
		},
		// 回收
		recover: function(tooltipDom) {
			return tooltipPool.push(tooltipDom)
		}
	}
})()

// 第一次有两个气泡
const arr = [];
for(let i = 0; str; str = ['A', 'B'][i++]) {
	var tooltip = tooltipFactory.create();
	tooltip.innerHTML = str;
	arr.push(tooltip)
}

// 重新绘制前回收气泡
for(let i = 0; topltip; tooltip = arr[i++]) {
	tooltipFactory.recover(tooltip);
}

// 再创建6个小气泡
for(let i = 0; str; str = ['A', 'B', 'C', 'D', 'E', 'F'][i++]) {
	var tooltip = tooltipFactory.create();
	tooltip.innerHTML = str;
}

此时实现了共享。

职责链模式

使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止

举个例子:
由于上车后人太多,找不到售票员,于是我们把硬币往前递。我们不知道售票员在哪里,也不知道会传递多少次,我们只知道当前第一个节点(自己),弱化了发送者和一组传递者的关系。如果不使用职责链模式,我就必须知道售票员在哪里,才能把硬币传给他。

实际开发中的职责链模式

我们再开发一个电商网站,分别缴纳500元定金和200元点金的两轮预定后(订单已生成),现在已经到了正式购买的阶段了
公司针对支付过定金的用户有优惠政策,已经支付500定金的用户会有100元的优惠券,200元定金的用户会有50元购物券,没有支付定金的用户没有优惠券,并且不保证一定可以买到
页面加载之初,后端会返回给我们字段
1. orderType: 订单类型。1代表500元定金用户,2代表200元定金用户,3代表普通用户
2. pay:是否已支付定金,值为true和false,即便下过定金,只要没支付就按普通用户计算
3. stock:用于普通购买的库存数量,支付过定金的用户不受限制

所以我们编写了以下代码

function order(orderType, pay, stock) {
	if (orderType === 1) {
		// 500定金用户
		if (pay === true) {
			// 支付定金
			console.log("500定金预购,获得100元优惠券")
		} else {
			// 没支付定金
			if (stock > 0) {
				console.log("普通购买,无优惠券")
			} else {
				console.log("库存不足")
			}
		}
	}

	else if (orderType === 2) {
		// 200定金用户
		if (pay === true) {
			// 支付定金
			console.log("200定金预购,获得50元优惠券")
		} else {
			// 没支付定金
			if (stock > 0) {
				console.log("普通购买,无优惠券")
			} else {
				console.log("库存不足")
			}
		}
	}

	else if (orderType === 3) {
		if (stock > 0) {
			console.log("普通购买,无优惠券")
		} else {
			console.log("库存不足")
		}
	}
}

order(1, true, 500)

我们终于写完了代码,并且也实现了功能,但是这并不是一段值得夸奖的代码,虽然可以运行,但是接下来的维护工作将会很困难,恐怕只有新手会写出这样的代码。
所以我们按照职责链模式进行改造

function order500(orderType, pay, stock) {
	if (orderType === 1 && pay === true) {
		console.log("500定金预购,获得100元优惠券")
	} else {
		order200(orderType, pay, stock)   // 将请求转发给定金200用户
	}
}

function order200(orderType, pay, stock) {
	if (orderType === 2 && pay === true) {
		console.log("200定金预购,获得50元优惠券")
	} else {
		order(orderType, pay, stock) // 将请求转发给普通用户
	}
}

function order(orderType, pay, stock) {
	if (stock > 0) {
		console.log("普通购买,无优惠券")
	} else {
		console.log("库存不足")
	}
}

order500(1, true, 500)
order500(1, false, 500)
order500(2, true, 500)
order500(2, false, 500)
order500(3, false, 500)
order500(3, false, 0)

可以看到可以实现和之前的一样的效果,但是代码已经清晰多了,我们把一个大函数拆分成三个小函数,去掉了很多嵌套的条件分支。但是仍有一些问题,比如在order500中调用order200,使得传递顺序非常僵硬,传递的代码被耦合在业务代码中,违反了开放封闭原则,将来如果再加一个300定金或600定金的功能,就必须要改动业务内部代码。所以我们仍然需要改造

function order500(orderType, pay, stock) {
	if (orderType === 1 && pay === true) {
		console.log("500定金预购,获得100元优惠券")
	} else {
		return "nextSuccessor"   // 我也不知道下一个节点,我只要往后传递就可以了
	}
}

function order200(orderType, pay, stock) {
	if (orderType === 2 && pay === true) {
		console.log("200定金预购,获得50元优惠券")
	} else {
		return "nextSuccessor"   // 我也不知道下一个节点,我只要往后传递就可以了
	}
}

function order(orderType, pay, stock) {
	if (stock > 0) {
		console.log("普通购买,无优惠券")
	} else {
		console.log("库存不足")
	}
}

// 创造一个职责链节点,定义函数Chain,实例化的时候传入函数,同时拥有一个实例属性可以访问下一个节点
function Chain(fn) {
	this.fn = fn;
	this.successor = null;
}

Chain.prototype.setNextSuccessor = function(successor) {
	return this.successor = successor;
}

Chain.prototype.passRequest = function() {
	const ret = this.fn.apply(this, arguments);
	if (ret === "nextSuccessor") {
		return this.successor && this.successor.passRequest.apply(this.successor, arguments);
	}
	return ret;
}

// 把三个订单函数分别包装成职责链的节点
const chainOrder500 = new Chain(order500);
const chainOrder200 = new Chain(order200);
const chainOrder = new Chain(order);

// 指定职责链的顺序
chainOrder500.setNextSuccessor(chainOrder200);
chainOrder200.setNextSuccessor(chainOrder);

// 执行
chainOrder500.passRequest(1, true, 500)
chainOrder500.passRequest(1, false, 500)
chainOrder500.passRequest(2, true, 500)
chainOrder500.passRequest(2, false, 500)
chainOrder500.passRequest(3, false, 500)
chainOrder500.passRequest(3, false, 0)

// 如果会有300定金的逻辑
function order300() {
	... // 具体逻辑省略
}

const chainOrder300 = new Chain(order300);
chainOrder500.setNextSuccessor(chainOrder300)
chainOrder300.setNextSuccessor(chainOrder200)
...

对于程序员来说,我们总是喜欢去改那些相对容易改动的地方,就像改配置文件比改源代码简单的多。在这里我们完全不用理会原先的代码,如果功能有增加,我们只需要把新功能加入节点,改变原先链的连接关系即可。

中介者模式

中介者模式主要作用就是接触对象与对象之间的耦合关系。
当我们的对象和对象之间的关系变多的时候,当我们修改某个对象的属性的时候必须小心翼翼,否则可能会出现难以预料的bug。使用中介者模式后,对象发生变化,只需要通知中介者即可,对象与对象之间的关系变得松散,网状结构的关系变成了一对多的关系

举个例子,一个泡泡堂的简单游戏,当玩家死亡的时候通知对手胜利
function Player(name) {
	this.name = name;
	this.enemy = null;  // 敌人
}

Player.prototype.win = function() {
	console.log(this.name + " won");
}

Player.prototype.lose = function() {
	console.log(this.name + " lost")
}

Player.prototype.die = function() {
	this.lose();
	this.enemy.win();
}

// 创建两个玩家
const player1 = new Player("张三");
const player2 = new Player("李四");

// 互相设置为敌人
player1.enemy = player2;
player2.enemy = player1;

// 执行某些操作后,player1死亡
player1.die();   // 输出 张三 lost 李四 won

接下来我们改造一下游戏,因为玩家数量增多用下面的这种方式增加很低效

player1.partners = [player2, player3, player4, player5];
player1.enemies = [player6, player7, player8, player9];

所以我们定义一个数组保存所有的玩家,创建玩家后循环数组给这个玩家设置敌人和队友

const players = [];

创建一个构造函数,使每个玩家都有一些属性:队友列表,敌人列表,当前 状态,玩家名字和当前队伍颜色

function Player(name, teamColor) {
    this.partners = []; // 队友列表
    this.enemies = []; // 敌人列表
    this.state = "live"; // 当前状态
    this.name = name; // 角色名字
    this.teamColor = teamColor; // 队伍颜色
}

Player.prototype.win = function () {  // 玩家队伍胜利
    console.log(`winner : ${this.name}`)
}

Player.prototype.lose = function () {  // 玩家队伍失败
    console.log(`loser : ${this.name}`)
}

Player.prototype.die = function () { // 玩家死亡
    let all_dead = true;
    this.state = "dead"; // 设置当前玩家死亡

    for (let i = 0, partner; partner = this.partners[i++];) { // 遍历队友查看状态
        if (partner.state !== "dead") { // 有一个没死的就没输
            all_dead = false;
            break;
        }
    }
    
    if (all_dead) { // 队友全都死了
        this.lose(); // 通知自己游戏失败
        for (let i = 0 ,partner; partner = this.partners[i++];) { // 通知队友输了
            partner.lose(); 
        }
        for (let i = 0, enemy; enemy = this.enemies[i++];) { // 通知敌人赢了
            partner.win();
        }
    }
}

最后定义一个工厂来创建玩家

function playerFactory(name, teamColor) {
    const newPlayer = new Player(name, teamColor); // 创建新玩家
    for (let i = 0, player; player = players[i++];) {
        if (player.teamColor === newPlayer.teamColor) { // 互相添加为队友
            player.partners.push(newPlayer);
            newPlayer.partners.push(player);
        } else { // 互相添加敌人
            player.enemies.push(newPlayer);
            newPlayer.enemies.push((player))
        }
    }
    players.push(newPlayer)
    return newPlayer
}

创建玩家

const player1 = playerFactory('A', "red");
const player2 = playerFactory('B', "red");
const player3 = playerFactory('C', "red");
const player4 = playerFactory('D', "red");
const player5 = playerFactory('E', "red");
const player6 = playerFactory('F', "blue");
const player7 = playerFactory('G', "blue");
const player8 = playerFactory('H', "blue");
const player9 = playerFactory('I', "blue");
const player10 = playerFactory('J', "blue");

经过复杂的战斗得到结果(至于咋战斗的,自行实现)


player1.die();
player2.die();
player3.die();
player4.die();
player5.die();

我们已经实现初步效果,但是这是10个玩家的情况,如果我们的游戏有成百上千的玩家,某一个角色状态变化都要通知成千上万的玩家,那就不是一个简单的遍历可以完成的,稍微一点变化,就可以双手投降了。
所以我们要使用中介者模式进行改造,首先还是创建一个Player构造函数,但是只执行创建用户和维护用户操作,具体行为交给中介者

function Player(name, teamColor) {
    this.state = "live"; // 当前状态
    this.name = name; // 角色名字
    this.teamColor = teamColor; // 队伍颜色
}

Player.prototype.win = function () {  // 玩家队伍胜利
    console.log(`winner : ${this.name}`)
}

Player.prototype.lose = function () {  // 玩家队伍失败
    console.log(`loser : ${this.name}`)
}

Player.prototype.die = function () {
    this.state = "dead";
    playerDirector.postMessage("playerDead", this); // 向中介者发消息,当前用户死亡
}

Player.prototype.remove = function () {
    this.state = "dead";
    playerDirector.postMessage("playerRemove", this); // 向中介者发消息,当前用户离线
}


function playerFactory(name, teamColor) {
    const newPlayer = new Player(name, teamColor); // 创建新玩家
    playerDirector.postMessage("addPlayer", newPlayer); // 向中介者发消息,告知新增用户
    return newPlayer
}

创建中介者

const playerDirector = (function () {
    const players = {}; // 保存所有用户
    const operations = {}; // 保存所有操作

    operations.addPlayer = function (player) {
        const teamColor = player.teamColor;
        players[teamColor] = players[teamColor] || []; // 当前队伍为空,创建一个默认队伍
        players[teamColor].push(player); // 当前队伍添加新用户
    }

    operations.playerRemove = function (player) {
        const teamColor = player.teamColor;
        teamPlayers = player[teamColor] || []; // 当前队伍为空,创建一个默认队伍
        for (let i = teamPlayers.length; i >= 0; i--) {
            if (teamPlayers[i] === player) {
                teamPlayers.splice(i, 1)
            }
        }
    }

    operations.playerDead = function (player) { // 玩家死亡
        const teamColor = player.teamColor;
        const teamPlayers = players[teamColor] || []; // 玩家所在队伍
        let all_dead = true;

        for (let i = 0, player; player = teamPlayers[i++];) { // 遍历队友查看状态
            if (player.state !== "dead") { // 有一个没死的就没输
                all_dead = false;
                break;
            }
        }
        if (all_dead) {
            for (let i = 0, player; player = teamPlayers[i++];) {
                player.lose(); // 本队所有玩家lose
            }
            for (const color in players) {
                if (color !== teamColor) {
                    const teamPlayers = players[color]; // 其他队伍的玩家
                    for (let i = 0, player; player = teamPlayers[i++];) {
                        player.win(); // 其他队伍所有玩家胜利
                    }
                }
            }
        }
    }

    function postMessage() {
        const message = Array.prototype.shift.call(arguments);
        operations[message].apply(this, arguments)
    }
    return {
        postMessage,
        getData: function () {
            return {
                operations,
                players
            }
        }
    }
})()

此时我们创建了一个中介者,在用户发生行为时向中介者发送消息,由中介者去通知其他用户,从而增加的项目的扩展性
中介者可以很好对模块和对象进行解耦,但是对象和对象之间难免会有一些关系,毕竟我们写代码是要快速的高质量的完成项目,而不是一次次的堆叠模式,关键在于衡量对象和成本之间的轻重,如果后期维护和对象之间的耦合比较高,就比较适用于中介者模式

装饰器模式

动态的给某个对象添加一些行为,但是不会影响这个对象的一些子类的行为
传统的面向对象的继承,当父类发生变化的时候也很可能会影响子类,父子类之间其实是有着一定的耦合性的,装饰器模式就更好的解决此类问题

现在有个场景,我们需要对之前的功能做一些修改

const a = () => {
	console.log('hello')
}

接下来我们修改

const a = () => {
	console.log('hello')
	console.log('world')
}

该方法简单粗暴,但是是直接修改原代码,违反了开放-封闭原则,不是一个很好的代码,所以我们尝试修改

const a = () => {
	console.log('hello')
}
const _a = a;
a = () => {
	_a();
	console.log('world')
}
a();

这是一个比较常见的写法,也没违反开放-封闭原则,但是却有两个问题

  1. 必须要维护一个中间变量,随着项目变化会越来越复杂
  2. 有可能会有this的问题

所以可以使用AOP装饰函数

function test() {
	console.log('2')
}

Function.prototype.before = function(beforeFn) {
	const that = this; // 保存原函数的引用
	return function() { // 返回原函数和新函数的代理函数
		beforeFn.apply(this, arguments) // 执行新函数,并保证this不丢失,新函数接受的参数也会被原封不动的传给原函数,新函数在原函数之前执行
		return that.apply(this, arguments) // 执行原函数并返回原函数的执行结果
	}
}

Function.prototype.after = function (afterFn) {
    const that = this;
    return function () {
        const ret = that.apply(this, arguments)
        afterFn.apply(this,arguments)
        return ret
    }
}

test.before(() => {
	console.log('1')
}).after(() => {
    console.log('3')
})()

以上代码可以按顺序输出123
Function.prototype.before接受一个函数作为参数,这个函数即新添加的函数,装载了新加的功能代码
接下来把this保存下来,这个this指向原函数,然后返回一个代理函数,并不承担代理的职责,他的主要工作是把请求分别转发给新添加的函数和原函数,且负责保证他们的执行顺序,让新加的函数在原函数之前执行,这样就实现了动态装饰的效果。Function.prototype.after原理和before一样,唯一不同的就是新添加的函数在原函数之后执行

常见操作:数据埋点等

适配器模式

类似于高阶组件和高阶函数,举个例子,我们要加载高德地图,百度地图和腾讯地图,但是这三个可能各自有各自的渲染方法,如果直接调某个函数可能会导致别的地图就会报错,所以我们需要一个适配器函数来兼容三种地图的渲染方法,确保可以使用同一个命令调用这三个地图的任意一个的渲染函数

代码重构

提炼函数

如果一个函数需要加很多的函数才能看懂,就需要进行重构了
重构前

function getInfo() {
	ajax("http://xxxx", function(data) {
		console.log(xxx)
		console.log(xxx)
		console.log(xxx)
	})
}

上面的代码既要请求一些信息,又要输出信息的一些内容,所以输出的内容就可以封装在一个函数里

function getInfo() {
	ajax("http://xxxx", function(data) {
		console.log(xxx)
		console.log(xxx)
		console.log(xxx)
	})
}

function printData(data) {
	console.log(xxx)
	console.log(xxx)
	console.log(xxx)
}

合并重复代码

改造前

function paging(currentPage) {
	if (currentPage<= 0) {
		currentPage= 0;
		jump(currentPage)  // 跳转页面
	} else if (currentPage >= totalPage) {
		currentPage= totalPage;
		jump(currentPage)  // 跳转页面
	} else {
		jump(currentPage)  // 跳转页面
	}
}

因为jump(currentPage)都有出现,所以可以独立出来

function paging(currentPage) {
	if (currentPage<= 0) {
		currentPage= 0;
	} else if (currentPage >= totalPage) {
		currentPage= totalPage;
	} 
	jump(currentPage)  // 跳转页面,独立出来
}

把条件分支语句提炼成函数

改造前

const getPrice = (price) => {
    const date = new Date()
    if (date.getMonth() >= 6 && date.getMonth() <= 9) {
        return price * 0.8
    }
    return price
}

这段代码很简单,如果是夏季,打八折,但是阅读代码的时候,特别是判断语句,还是要稍微花点心思才能读懂意图,所以可以把判断语句提炼成一个单独的函数,既能准确表达代码的意思,又能起到注释的作用

function isSummer() {
    const date = new Date()
    return date.getMonth() >= 6 && date.getMonth() <= 9
}

const getPrice = (price) => {
    const date = new Date()
    if (isSummer()) {
        return price * 0.8
    }
    return price
}

提前让函数退出而不是循环嵌套判断

改造前

function xxx() {
	if (xxx) {
		if(xxx) {
			
		}
		if (xxx) {
			if (xxx) {
		
			}
		}
	}
}

太多的嵌套只会是维护者的噩梦,不符合条件可以使函数提前退出

function xxx() {
	if (!xxx) {
		return
	}
	if (!xxx.xxx) {
		return
	}
	if (!xxx.xxx.xxx) {
		return
	}
}

减少传参

function setUserInfo(username, password, nickName, avatar, sex, status) {
	console.log(username)
	console.log(password)
	console.log(nickName)
	console.log(avatar)
	console.log(sex)
	console.log(status)
}

这种代码不仅要传很多参数,还要注意顺序,一不小心就会出错

function setUserInfo(obj) {
	console.log(obj.username)
	console.log(obj.password)
	console.log(obj.nickName)
	console.log(obj.avatar)
	console.log(obj.sex)
	console.log(obj.status)
}

减少不必要的三目运算

如果三木运算太复杂,不如老老实实的用if和else更能理解,例如

const flag = a && b || c && d && e || f || g && f

合理使用链式调用

使用链式调用可以很方便的减少代码定义,字符和中间变量

user.setUsername(xxx).setPassword(xxx)

但也有缺陷,比如出了问题不易查找,功能变化的时候不利于维护

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值