简单易懂的设计模式(上)
一、单例模式
1. 什么是单例模式
单例模式的定义是,保证一个类仅有一个实例,并提供一个访问它的全局访问点。
有一些对象,比如线程池/全局缓存/浏览器中的 window
对象等等,我们就只需要一个实例。
下面将根据实际场景进行介绍。
2. 实际场景
1. 登录浮窗
当我们单击登录按钮时,页面中会出现一个登录的浮窗,而这个登录浮窗是唯一的,无论单击多少次登录按钮,这个浮窗都只会被创建一次,那么这个登录浮窗就适合用单例模式来创建。
1.1 传统做法
传统做法在页面加载完成时,就创建好登录浮窗,当用户点击登录按钮时,显示登录浮窗,实现代码如下:
<button id="loginBtn">登录</button>
var loginLayer = (() => {
let div = document.createElement('div')
div.innerHTML = '我是登录弹窗'
div.style.display = 'none'
document.body.appendChild(div)
return div
})()
document.getElementById('loginBtn').onclick = () => {
loginLayer.style.display = 'block'
}
上述代码有以下缺点:
- 在无需登录的情况下,也会新增登录浮窗的
DOM
节点,浪费性能。
现在优化一下,将代码改为,在用户点击登录按钮后,才新增登录浮窗的 DOM
节点。
代码如下:
var createLoginLayer = () => {
let div = document.createElement('div')
div.innerHTML = '我是登录弹窗'
div.style.display = 'none'
document.body.appendChild(div)
return div
}
document.getElementById('loginBtn').onclick = () => {
const loginLayer = createLoginLayer()
loginLayer.style.display = 'block'
}
上述代码也存在缺陷,具体如下:
- 每次点击登录按钮,都会创建一个登录浮窗,频繁的创建
DOM
节点更加浪费性能。
实际上,我们只需要创建一次登录浮窗。
1.2 单例模式
通过单例模式,重构上述代码。
const createLoginLayer = () => {
const div = document.createElement('div')
div.innerHTML = '我是登录弹窗'
div.style.display = 'none'
console.log(123)
document.body.appendChild(div)
return div
}
const createSingle = (function () {
var instance = {}
return function (fn) {
if (!instance[fn.name]) {
instance[fn.name] = fn.apply(this, arguments)
}
return instance[fn.name]
}
})()
const createIframe = function () {
const iframe = document.createElement('iframe')
document.body.appendChild(iframe)
iframe.style.display = 'none'
return iframe
}
const createSingleLoginLayer = createSingle(createLoginLayer)
const createSingleIframe = createSingle(createIframe)
document.getElementById('loginBtn').onclick = () => {
const loginLayer = createSingleLoginLayer
const iframe = createSingleIframe
loginLayer.style.display = 'block'
iframe.style.display = 'block'
}
经过重构,代码做了以下优化:
- 将创建实例对象
createLoginLayer
/createIframe
的职责和管理单例对象createSingle
的职责分离,符合单一职责原则; - 通过闭包存储实例,并进行判断,不管点击登录按钮多少次,只创建一个登录浮窗实例;
- 易于扩展,当下次需要创建页面中唯一的
iframe
/script
等其他标签时,可以直接复用该逻辑。
3. 总结
单例模式是一种简单但非常实用的模式,特别是惰性单例技术,在合适的时候才创建对象,并且只创建唯一的一个。更奇妙的是,创建对象和管理单例的职责被分布在两个不同的方法中,这两个方法组合起来才具有单例模式的威力。
二、策略模式
1. 什么是策略模式
当我们计划国庆出去游玩时,在交通方式上,我们可以选择贵而快的飞机、价格中等但稍慢的动车、便宜但超级慢的火车,根据不同的人,选择对应的交通方式,且可以随意更换交通方式,这就是策略模式。
策略模式的定义是,定义一系列算法,把它们一个个封装起来,并且使它们可以相互替换。
2. 实际场景
1. 计算年终奖
1.1 传统做法
有一个计算员工年终奖的需求,假设,绩效为 S
的员工年终奖是 4
倍工资,绩效为 A
的员工年终奖是 3
倍工资,绩效为 B
的员工年终奖是 2
倍工资,下面我们来计算员工的年终奖。
var calculateBonus = function(performanceLevel, salary) {
if (performanceLevel === 'S') {
return salary * 4;
}
if (performanceLevel === 'A') {
return salary * 3;
}
if (performanceLevel === 'B') {
return salary * 2;
}
};
calculateBonus('B', 20000); // 输出:40000
calculateBonus( 'S', 6000 ); // 输出:24000
上述代码有以下缺点:
- 使用
if-else
语句描述逻辑,代码庞大; - 缺乏弹性,如果需要修改绩效
S
的奖金系数,必须修改calculateBonus
函数,违反了开放-封闭原则; - 无法再次复用,当其他地方需要用到这套逻辑,只能再复制一份。
1.2 策略模式做法
使用策略模式改良后
const strategies = {
S: salary => {
return salary * 4
},
A: salary => {
return salary * 3
},
B: salary => {
return salary * 2
}
}
const calculateBonus = (level, salary) => {
return strtegies[level](salary)
}
console.log(calculateBonus('s', 20000))
console.log(calculateBonus('a', 10000))
可以看到上述代码做了以下改动:
- 策略类
strategies
封装了具体的算法和计算过程(每种绩效的计算规则); - 环境类
calculateBonus
接受请求,把请求委托给策略类strategies
(员工的绩效和工资; - 将算法的使用和算法的实现分离,代码清晰,职责分明;
- 消除大量的
if-else
语句。
1.3 小结
策略模式使代码可读性更高,易于拓展更多的策略算法。当绩效系数改变,或者绩效等级增加,我们只需要为 strategies
调整或新增算法,符合开放-封闭原则。
2. 表单校验
当网页上的表单需要校验输入框/复选框等等规则时,如何去实现呢?
现在有一个注册用户的表单需求,在提交表单之前,需要验证以下规则:
- 用户名不能为空
- 密码长度不能少于 6 位
- 手机号码必须符合格式
2.1 传统做法
使用 if-else
语句判断表单输入是否符合对应规则,如不符合,提示错误原因。
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<form id='registerForm' action="xxx" method="post">
用户名:<input type="text" name="userName">
密码:<input type="text" name="password">
手机号:<input type="text" name="phone">
<button>提交</button>
</form>
<script type="text/javascript">
let registerForm = document.getElementById('registerForm')
registerForm.onsubmit = () => {
if (registerForm.userName.value) {
alert('用户名不能为空')
return false
}
if (registerForm.password.value.length < 6) {
alert('密码长度不能少于6')
return false
}
if (!/(^1[3|5|8][0-9]$)/.test(registerForm.phone.value)) {
alert('手机号码格式不正确')
return false
}
}
</script>
</body>
</html>
上述代码有以下缺点:
-
onsubmit
函数庞大,包含大量if-else
语句; -
onsubmit
缺乏弹性,当有规则需要调整,或者需要新增规则时,需要改动onsubmit
函数内部,违反开放-封闭原则; - 算法复用性差,只能通过复制,复用到其他表单。
2.2 策略模式做法
使用策略模式重构上述代码。
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<form action="http://xxx.com/register" id="registerForm" method="post">
请输入用户名:
<input type="text" name="userName" />
请输入密码:
<input type="text" name="password" />
请输入手机号码:
<input type="text" name="phoneNumber" />
<button>
提交
</button>
</form>
<script type="text/javascript" src="index.js">
</script>
</body>
</html>
// 表单dom
const registerForm = document.getElementById('registerForm')
// 表单规则
const rules = {
userName: [
{
strategy: 'isNonEmpty',
errorMsg: '用户名不能为空'
},
{
strategy: 'minLength:10',
errorMsg: '用户名长度不能小于10位'
}
],
password: [
{
strategy: 'minLength:6',
errorMsg: '密码长度不能小于6位'
}
],
phoneNumber: [
{
strategy: 'isMobile',
errorMsg: '手机号码格式不正确'
}
]
}
// 策略类
var strategies = {
isNonEmpty: function(value, errorMsg) {
if (value === '') {
return errorMsg;
}
},
minLength: function(value, errorMsg, length) {
console.log(length)
if (value.length < length) {
return errorMsg;
}
},
isMobile: function(value, errorMsg) {
if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
return errorMsg;
}
}
};
// 验证类
const Validator = function () {
this.cache = []
}
// 添加验证方法
Validator.prototype.add = function ({ dom, rules}) {
rules.forEach(rule => {
const { strategy, errorMsg } = rule
console.log(rule)
const [ strategyName, strategyCondition ] = strategy.split(':')
console.log(strategyName)
const { value } = dom
this.cache.push(strategies[strategyName].bind(dom, value, errorMsg, strategyCondition))
})
}
// 开始验证
Validator.prototype.start = function () {
let errorMsg
this.cache.some(cacheItem => {
const _errorMsg = cacheItem()
if (_errorMsg) {
errorMsg = _errorMsg
return true
} else {
return false
}
})
return errorMsg
}
// 验证函数
const validatorFn = () => {
const validator = new Validator()
console.log(validator.add)
Object.keys(rules).forEach(key => {
console.log(2222222, rules[key])
validator.add({
dom: registerForm[key],
rules: rules[key]
})
})
const errorMsg = validator.start()
return errorMsg
}
// 表单提交
registerForm.onsubmit = () => {
const errorMsg = validatorFn()
if (errorMsg) {
alert(errorMsg)
return false
}
return false
}
上述代码通过 strategies
定义规则算法,通过 Validator
定义验证算法,将规则和算法分离,我们仅仅通过配置的方式就可以完成表单的校验,这些校验规则也可以复用在程序的任何地方,还能作为插件的形式,方便的被移植到其他项目中。
3. 总结
策略模式是一种常用且有效的设计模式,通过上述例子,可以总结出策略模式的一些优点:
- 策略模式利用组合/委托和多态等技术和思想,可以有效的避免多重条件选择语句;
- 策略模式提供了对开放-封闭原则的完美支持,将算法封装中独立的策略类中,使得它们易于切换/理解/扩展;
- 在策略模式中利用组合和委托来让
Context
拥有执行算法的能力,这也是继承的一种更轻便的代替方案。
三、代理模式
1. 什么是代理模式
代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。
代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。
2. 模拟场景
1. 小明送花给小白
1.1 传统做法
传统做法是小明直接把花送给小白,小白接收到花,代码如下:
const Flower = function () {
return '玫瑰