这是我参与「第四届青训营 」笔记创作活动的的第4天
(WHAT)什么是设计模式?
假设有一个空房间,我们要日复一日地往里面放一些东西。最简单的办法当然是把这些东西直接扔进去,但是时间久了,就会发现很难从这 个房子里找到自己想要的东西,要调整某几样东西的位置也不容易。所以在房间里做一些柜子也许是个更好的选择,虽然柜子会增加我们的成 本,但它可以在维护阶段为我们带来好处。使用这些柜子存放东西的规则,或许就是一种模式
因此,学习设计模式,可以帮助我们写出可维护、复用性高的代码。
从上述例子中也可以看出设计模式和数据结构、算法之间的区别。
设计模式关注于”我们要存放东西”这个意图,数据结构关注于”存放东西需要什么(柜子还是书架?什么样的柜子?)”,算法关注于“怎么把书放进柜子”
软件设计中常见问题的解决方案模型
- 历史经验的总结
- 与特定语言无关
设计原则
单一职责原则(SRP)
一个对象或方法只做一件事情。如果一个方法承担了过多的职责,那么在需求的变迁过程中,需要改写这个方法的可能性就越大。
应该把对象或方法划分成较小的粒度
最少知识原则(LKP)
一个软件实体应当尽可能少地与其他实体发生相互作用
应当尽量减少对象之间的交互。如果两个对象之间不必彼此直接通信,那么这两个对象就不要发生直接的相互联系,可以转交给第三方进行处理
低耦合的表现,可以参考西欧等级森严的分封制度:
我附庸的附庸,不是我的附庸
开放-封闭原则(OCP)
软件实体(类、模块、函数)等应该是可以扩展的,但是不可修改
当需要改变一个程序的功能或者给这个程序增加新功能的时候,可以使用增加代码的方式,尽量避免改动程序的源代码,防止影响原系统的稳定
面向对象的语言中可扩展一般采用继承,不可修改一般采用权限修饰符(private等)
设计模式分类
- 创建型:如何创建一个对象
- 结构型:如何灵活地将对象组装成一个较大的结构(参考数据结构)
- 行为型:负责对象间的高校通信和职责划分
浏览器中的设计模式
- 单例模式
- 发布订阅模式(观察者模式)
单例模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
全局唯一访问对象
e.g. 缓存,全局状态管理,window
function SetManager(name) {
this.manager = name;
}
SetManager.prototype.getName = function() {
console.log(this.manager);
};
/**
* ES6写法
* Typescript
class Manager {
private manager: string;
constructor(name: string) {
this.manager = name;
}
public getName() {
console.log(this.manager);
return this.manager;
}
}
*/
var SingletonSetManager = (function() {
var manager = null;
return function(name) {
if (!manager) {
manager = new SetManager(name);
}
return manager;
}
})();
SingletonSetManager('a').getName(); // a
SingletonSetManager('b').getName(); // a
SingletonSetManager('c').getName(); // a
发布订阅模式(观察者模式)
一种订阅机制。定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。
e.g. 邮件订阅,上线订阅等
与传统的发布-订阅模式实现方式(将订阅者自身当成引用传入发布者)不同,在JS中通常使用注册回调函数的形式来订阅。
// 订阅
document.body.addEventListener('click', function() {
console.log('click1');
}, false);
document.body.addEventListener('click', function() {
console.log('click2');
}, false);
// 发布
document.body.click();
JavaScript中的设计模式
- 原型模式
- 代理模式
- 迭代器模式
原型模式
复制已有对象来创建新的对象。
e.g. 原型链,对象创建
Object.create()
代理模式
自定义控制原对象的访问方式,并且允许在更新前后做一些额外处理。
e.g. 监控,代理工具,前端框架实现等
当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象 来控制对这个对象的访问,客户实际上访问的是替身对象。 (Virtual DOM的思想来源)
替身对象对请求做出一些处理之后, 再把请求转交给本体对象。
代理和本体的接口具有一致性,本体定义了关键功能,而代理是提供或拒绝对它的访问,或者在访问本体之前做一些额外的事情。
代理模式主要有三种:保护代理、虚拟代理、缓存代理。
保护代理:实现了访问主体的限制行为,如HTTP请求过滤器。
虚拟代理:虚拟代理在控制对主体的访问时,加入了一些额外的操作。如上节提到的HOF实现函数装饰器的效果,对一些行为在保持核心功能不变的情况下做一些装饰。
缓存代理:为一些开销大的运算结果提供暂时的缓存,提升效率。
// 主体
function add() {
var arg = [].slice.call(arguments);
return arg.reduce(function(a, b) {
return a + b;
});
}
// 代理
var proxyAdd = (function() {
var cache = [];
return function() {
var arg = [].slice.call(arguments).join(',');
// 如果有,则直接从缓存返回
if (cache[arg]) {
return cache[arg];
} else {
var ret = add.apply(this, arguments);
return ret;
}
};
})();
console.log(
add(1, 2, 3, 4),
add(1, 2, 3, 4),
proxyAdd(10, 20, 30, 40),
proxyAdd(10, 20, 30, 40)
); // 10 10 100 100
迭代器模式
在不暴露数据类型的情况下访问集合中的数据。
不用关心对象的数据结构就可顺序访问其中每个元素
e.g. 为不同数据结构类型提供通用的操作接口
class MyDomElement {
tag: string;
children: MyDomElement[];
constructor(tag: string) {
this.tag = tag;
this.children = [];
}
addChildren(component: MyDomElement) {
this.children.push(component);
}
[Symbol.iterator]() {
const list: MyDomElement[] = [...this.children];
let node: MyDomElement;
return {
next: () => {
while((node = list.shift())) {
// 先序遍历,如果有子节点就放进数组尾部
node.children.length > 0 && list.push(...node.children);
// 并打印出当前节点
return {value: node, done: false};
}
return {value: null, done: true};
}
};
}
}
前端框架中的设计模式
- 代理模式
- 组合模式
代理模式
移步至Virtual DOM实现及Vue3 Ref()实现。
组合模式
可多个对象组合使用,也可单个对象独立使用。(组件化思想)
e.g. 前端组件,文件目录,DOM
扫描文件夹中的文件:
// 文件夹 组合对象
function Folder(name) {
this.name = name;
this.parent = null;
this.files = [];
}
Folder.prototype = {
constructor: Folder,
add: function(file) {
file.parent = this;
this.files.push(file);
return this;
},
scan: function() {
// 委托给叶对象处理
for (var i = 0; i < this.files.length; ++i) {
this.files[i].scan();
}
},
remove: function(file) {
if (typeof file === 'undefined') {
this.files = [];
return;
}
for (var i = 0; i < this.files.length; ++i) {
if (this.files[i] === file) {
this.files.splice(i, 1);
}
}
}
};
// 文件 叶对象
function File(name) {
this.name = name;
this.parent = null;
}
File.prototype = {
constructor: File,
add: function() {
console.log('文件里面不能添加文件');
},
scan: function() {
var name = [this.name];
var parent = this.parent;
while (parent) {
name.unshift(parent.name);
parent = parent.parent;
}
console.log(name.join(' / '));
}
};
// 测试
var web = new Folder('Web');
var fe = new Folder('前端');
var css = new Folder('CSS');
var js = new Folder('js');
var rd = new Folder('后端');
web.add(fe).add(rd);
var file1 = new File('HTML权威指南.pdf');
var file2 = new File('CSS权威指南.pdf');
var file3 = new File('JavaScript权威指南.pdf');
var file4 = new File('MySQL基础.pdf');
var file5 = new File('Web安全.pdf');
var file6 = new File('Linux菜鸟.pdf');
css.add(file2);
fe.add(file1).add(file3).add(css).add(js);
rd.add(file4).add(file5);
web.add(file6);
rd.remove(file4);
// 扫描
web.scan();