设计模式的原则是找出程序中的变化,并将变化封装起来,实现高效的可复用性。核心在于意图,而不在结构。通过设计模式可以帮助我们增强代码的可重用性、可扩充性、 可维护性、灵活性。我们使用设计模式的最终目的是为了实现代码的高类聚和低耦合。你是否思考过这样的一个问题,如何让代码写的更有健壮性,其实核心在于把握变与不变。确保变的部分更加灵活,不变的地方更加稳定,而使用设计模式可以让我们达到这样的目的。
1.S – Single Responsibility Principle 单一职责原则
a.一个程序只做好一件事
b.如果功能过于复杂就拆分开,每个部分保持独立
2.O – OpenClosed Principle 开放/封闭原则
a.对扩展开放,对修改封闭
b.增加需求时,扩展新代码,而非修改已有代码
3.L – Liskov Substitution Principle 里氏替换原则
a.子类能覆盖父类
b.父类能出现的地方子类就能出现
4.I – Interface Segregation Principle 接口隔离原则
a.保持接口的单一独立
b.类似单一职责原则,这里更关注接口
5.D – Dependency Inversion Principle 依赖倒转原则
a.面向接口编程,依赖于抽象而不依赖于具体
b.使用方只关注接口而不关注具体类的实现
设计模式可以分为三大类:
1.结构型模式(Structural Patterns): 通过识别系统中组件间的简单关系来简化系统的设计。常见结构性模式如下:
a.适配器模式
b.装饰器模式
c.代理模式
d.外观模式
e.桥接模式
f.组合模式
g.享元模式
2.创建型模式(Creational Patterns): 处理对象的创建,根据实际情况使用合适的方式创建对象。常规的对象创建方式可能会导致设计上的问题,或增加设计的复杂度。创建型模式通过以某种方式控制对象的创建来解决问题。常见创建型模式如下:
a.单例模式
b.原型模式
c.工厂模式
d.抽象工厂模式
e.建造者模式
3.行为型模式(Behavioral Patterns): 用于识别对象之间常见的交互模式并加以实现,如此,增加了这些交互的灵活性。常见行为型模式如下:
a.观察者模式
b.迭代器模式
c.策略模式
d.模板方法模式
e.职责链模式
f.命令模式
g.备忘录模式
h.状态模式
i.访问者模式
j.中介者模式
k.解释器模式
下面我们选择一些在前端开发过程中常见的模式进行一一讲解。
1.单例模式
定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
场景:确保只有一个实例。比如:登录浮窗、Vue中的axios实例(我们对axios进行请求拦截和响应拦截,多次调用封装好的axios但是仅设置一次,封装好的axios导出就是一个单例)、全局状态管理 store等
示例:
// 单例构造器
const FooServiceSingleton = (function () {
// 隐藏的Class的构造函数
function FooService() {}
// 未初始化的单例对象
let fooService;
return {
// 创建/获取单例对象的函数
getInstance: function () {
if (!fooService) {
fooService = new FooService();
}
return fooService;
}
}
})();
const fooService1 = FooServiceSingleton.getInstance();
const fooService2 = FooServiceSingleton.getInstance();
console.log(fooService1 === fooService2); // true
优点:
划分命名空间,减少全局变量
增强模块性,把自己的代码组织在一个全局变量名下,放在单一位置,便于维护且只会实例化一次。简化了代码的调试和维护
缺点:
不适用动态扩展对象。
2.策略模式
定义:定义一系列的算法,把它们一个个封装起来,并且使它们可以互相替换
适用场景:根据表单验证策略进行表单验证等
示例:
<!DOCTYPE html>
<html>
<head>
<title>策略模式-校验表单</title>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
</head>
<body>
<form id = "registerForm" method="post" action="http://xxxx.com/api/register">
用户名:<input type="text" name="userName">
密码:<input type="text" name="password">
手机号码:<input type="text" name="phoneNumber">
<button type="submit">提交</button>
</form>
<script type="text/javascript">
// 策略对象
const strategies = {
isNoEmpty: function (value, errorMsg) {
if (value === '') {
return errorMsg;
}
},
isNoSpace: function (value, errorMsg) {
if (value.trim() === '') {
return errorMsg;
}
},
minLength: function (value, length, errorMsg) {
if (value.trim().length < length) {
return errorMsg;
}
},
maxLength: function (value, length, errorMsg) {
if (value.length > length) {
return errorMsg;
}
},
isMobile: function (value, errorMsg) {
if (!/^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|17[7]|18[0|1|2|3|5|6|7|8|9])\d{8}$/.test(value)) {
return errorMsg;
}
}
}
// 验证类
class Validator {
constructor() {
this.cache = []
}
add(dom, rules) {
for(let i = 0, rule; rule = rules[i++];) {
let strategyAry = rule.strategy.split(':')
let errorMsg = rule.errorMsg
this.cache.push(() => {
let strategy = strategyAry.shift()
strategyAry.unshift(dom.value)
strategyAry.push(errorMsg)
return strategies[strategy].apply(dom, strategyAry)
})
}
}
start() {
for(let i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
let errorMsg = validatorFunc()
if (errorMsg) {
return errorMsg
}
}
}
}
// 调用代码
let registerForm = document.getElementById('registerForm')
let validataFunc = function() {
let validator = new Validator()
validator.add(registerForm.userName, [{
strategy: 'isNoEmpty',
errorMsg: '用户名不可为空'
}, {
strategy: 'isNoSpace',
errorMsg: '不允许以空白字符命名'
}, {
strategy: 'minLength:2',
errorMsg: '用户名长度不能小于2位'
}])
validator.add(registerForm.password, [ {
strategy: 'minLength:6',
errorMsg: '密码长度不能小于6位'
}])
validator.add(registerForm.phoneNumber, [{
strategy: 'isMobile',
errorMsg: '请输入正确的手机号码格式'
}])
return validator.start()
}
registerForm.onsubmit = function() {
let errorMsg = validataFunc()
if (errorMsg) {
alert(errorMsg)
return false
}
}
</script>
</body>
</html>
优点
利用组合、委托、多态等技术和思想,可以有效的避免多重条件选择语句
提供了对开放-封闭原则的完美支持,将算法封装在独立的strategy中,使得它们易于切换,理解,易于扩展
利用组合和委托来让Context拥有执行算法的能力,这也是继承的一种更轻便的代替方案
缺点
会在程序中增加许多策略类或者策略对象
要使用策略模式,必须了解所有的strategy,必须了解各个strategy之间的不同点,这样才能选择一个合适的strategy
3.工厂模式
定义:工厂模式是指通过一个工厂类来创建其他类的实例。这个模式非常适合那些需要根据不同条件创建不同实例的场景
场景:如jquery的$函数,该函数接收一个选择器字符串作为参数,然后返回与该选择器匹配的一个或多个元素。
(function () {
function jQuery(selector) {
return new jQuery.prototype.init(selector);
}
// jQuery对象的构造函数
jQuery.prototype.init = function (selector) {
}
// jQuery原型上的css方法
jQuery.prototype.css = function (config) {
}
// 将jQuery原型上的方法都放到init的原型链上
jQuery.prototype.init.prototype = jQuery.prototype;
window.$ = window.jQuery = jQuery;
})();
示例:
function Factory (name, age) {
this.name = name;
this.age = age;
}
Factory.prototype.say = function () {
console.log(我的名字叫${this.name}, 我今年${this.age}了
)
}
let zs = new Factory(‘张三’, 18);
let ls = new Factory(‘李四’, 20);
zs.say()
ls.say()
优点
创建对象的过程可能很复杂,但我们只需要关心创建结果。
构造函数和创建者分离, 符合“开闭原则”
一个调用者想创建一个对象,只要知道其名称就可以了。
扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
缺点
添加新产品时,需要编写新的具体产品类,一定程度上增加了系统的复杂度
考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度
4.外观模式
定义:外观模式是最常见的设计模式之一,它为子系统中的一组接口提供一个统一的高层接口,使子系统更容易使用。简而言之外观设计模式就是把多个子系统中复杂逻辑进行抽象,从而提供一个更统一、更简洁、更易用的API
场景:比如JQuery就把复杂的原生DOM操作进行了抽象和封装,并消除了浏览器之间的兼容问题,从而提供了一个更高级更易用的版本。
示例:
//兼容浏览器事件绑定
let addMyEvent = function (el, ev, fn) {
if (el.addEventListener) {
el.addEventListener(ev, fn, false)
} else if (el.attachEvent) {
el.attachEvent('on' + ev, fn)
} else {
el['on' + ev] = fn
}
};
优点
减少系统相互依赖。
提高灵活性。
提高了安全性
缺点
不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。
5.代理模式
定义:代理模式是指使用一个代理对象来控制对另一个对象的访问。访问之前先做一层拦截,这个模式非常适合那些需要控制对某些敏感资源的访问的场景。
场景:如缓存接口请求,如vue3的响应式原理proxy
示例:
let obj1 = {
name:'vue',
age:'10'
}
let obj2 = new Proxy(obj1,{
get(target,property) {
//访问obj2的某个属性的时候调用
//target即obj这个对象
//property 所访问的属性
console.log('触发get');
return target[property];
},
set(target,property,newVal) {
//修改obj2的某个属性的时候调用
//target即obj这个对象
//property 所访问的属性
//newVal 要设置的新值
console.log('触发set')
target[property] = newVal;
},
})
obj2.age = '12';
console.log(obj2.age);
优点
代理模式能将代理对象与被调用对象分离,降低了系统的耦合度。代理模式在客户端和目标对象之间起到一个中介作用,这样可以起到保护目标对象的作用
代理对象可以扩展目标对象的功能;通过修改代理对象就可以了,符合开闭原则;
缺点
处理请求速度可能有差别,非直接访问存在开销
6.观察者模式
定义:观察者模式又称发布订阅模式(Publish/Subscribe Pattern),是我们经常接触到的设计模式,日常生活中的应用也比比皆是,比如你订阅了某个博主的频道,当有内容更新时会收到推送;又比如JavaScript中的事件订阅响应机制。观察者模式的思想用一句话描述就是:被观察对象(subject)维护一组观察者(observer),当被观察对象状态改变时,通过调用观察者的某个方法将这些变化通知到观察者。
场景:比如给DOM元素绑定事件的 addEventListener() 方法、还有Vue中的watch 就是依赖于这个模式,在wacth中,被监听的数据充当了发布者,而执行的回调函数充当了订阅者。当数据变化时,发布者会通知所有订阅者执行回调函数,从而实现对数据变化的监听。
示例:
document.body.addEventListener("click", function() {
alert("Hello World");
});
document.body.click();
在这里需要监控用户点击 document.body 的动作,但是我们没办法预知用户将在什么时候点击。所以我们订阅了 document.body 的 click 事件,当 body 节点被点击时,body 节点便会向订阅者发布这个消息
优点
支持简单的广播通信,自动通知所有已经订阅过的对象
目标对象与观察者之间的抽象耦合关系能单独扩展以及重用
增加了灵活性
观察者模式所做的工作就是在解耦,让耦合的双方都依赖于抽象,而不是依赖于具体。从而使得各自的变化都不会影响到另一边的变化。
缺点
过度使用会导致对象与对象之间的联系弱化,会导致程序难以跟踪维护和理解
7.适配器模式
定义:适配器模式的英文翻译是 Adapter Design Pattern。顾名思义,这个模式就是用来做适配的,它将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。对于这个模式,有一个经常被拿来解释它的例子,就是 USB 转接头充当适配器,把两种不兼容的接口,通过转接变得可以一起工作。
场景:统一多个类的接口设计、兼容老版本接口、适配不同格式的数据等
示例:
class GooleMap {
show() {
console.log('渲染地图')
}
}
class BaiduMap {
display() {
console.log('渲染地图')
}
}
class GaodeMap {
show() {
console.log('渲染地图')
}
}
// 上面三个类,如果我们用多态的思想去开发的话是很难受的,所以我们通过适配器来统一接口
class BaiduAdaapterMap {
show() {
return new BaiduMap().display()
}
}
优点
可以让任何两个没有关联的类一起运行
提高了类的复用
增加了类的透明度
灵活性好
缺点
过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。
8.装饰器模式
定义:装饰器模式是一种在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责的设计模式。在前端开发中,装饰者模式可以用于对已有功能的扩展和增强。
场景:ES7中增加了对装饰器Decorator的支持,它就是用来修改类的行为,或者增强类,这使得在js中使用装饰者模式变得更便捷。
示例:
// 原始对象
function Component() {}
Component.prototype = {
operation: function () {
console.log("Component operation");
},
};
// 装饰者对象
function Decorator(component) {
this.component = component;
}
Decorator.prototype = {
operation: function () {
this.component.operation();
console.log("Decorator operation");
},
};
// 使用
var component = new Component();
component.operation(); // 输出 "Component operation"
var decorator = new Decorator(component);
decorator.operation(); // 输出 "Component operation" 和 "Decorator operation"
优点
装饰器是继承的有力补充,比继承灵活,在不改变原有对象的情况下,动态的给一个对象扩展功能,即插即用
通过使用不同装饰类及这些装饰类的排列组合,可以实现不同效果
装饰器模式完全遵守开闭原则
缺点
装饰器模式会增加许多子类,过度使用会增加程序得复杂性