Javascript 设计模式入门
前提¶
"The programming is all about managing complexity. "
Maybe you've heard that the computer world is a giant construction of abstractions. We simply wrap things and produce new tools over and over again.
"编程是对复杂性的管理。"
计算机世界是一个由抽象组成的巨大结构体;我们无非是重复着包装东西和生产新工具。
面向对象¶
- 继承,子类继承父类
- 封装,数据的局限和保护
- 多态,统一接口不同实现
面向对象举例¶
class jQuery {
constructor(selecor) {
let slice = Array.prototype.slice;
let dom = slice.call(document.querySelectorAll(selecor))
let len = dom ? dom.length : 0;
for(let i = 0; i <= len - 1; i ++) {
this[i] = dom[i];
}
this.length = len;
this.selecor = selecor || '';
}
append(node) {}
addClass(name) {}
html(data) {}
// ...
}
window.$ = function(selecor) {
// 工厂模式
return new jQuery(selecor);
}
UML 类图¶
- UML:统一建模语言(Unified Modeling Language)
- 类图:属性,方法
- 关系
- 泛化,表示继承
- 关联,表示引用
设计原则¶
何为设计¶
- 《UNIX / LINUX 设计哲学》
- 准则
- 小即是美
- 让每个程序只做好一件事
- 快速建立原型
- 舍弃高效率而取可移植性
- 采用纯文本来存储数据
- 充分利用软件的杠杆效应(软件复用)
- 使用 shell 脚本来提高杠杆效应和可移植性
- 避免强制性的用户界面
- 让每个程序都称为过滤器
设计原则¶
- SOLID 五大设计原则
- S - 单一指责原则
- O - 开放封闭原则
- L - 里氏置换原则
- I - 接口独立原则
- D - 依赖倒置原则
- 单一指责原则
- 一个程序只做好一件事情
- 如果功能过于复杂就拆分,每个部分保持独立
- 开放封闭原则
- 对扩展开发,对修改封闭
- 增加需求时,扩展新代码,而非修改已有代码
- 这是软件设计的终极目标
- 里氏置换原则
- 子类能覆盖父类
- 父类能出现的地方子类就能出现
- 接口独立原则
- 保持接口的单一独立,避免出现“胖”接口
- typescript 有接口
- 依赖倒置原则
- 面向接口(抽象)编程,依赖于抽象而非具体
- 使用方只关注接口,而不关注具体类的实现
设计模式¶
工厂模式¶
介绍¶
- 将 new 操作单独封装
- 遇得 new 时,就要考虑是否使用工厂模式
UML类图¶
代码¶
class Product {
constructor(name) {
this.name = name;
}
init() {}
fn1() {}
fn2() {}
}
class Creator {
create(name) {
return new Product(name);
}
}
export default Creator;
场景¶
- jQuery - $('p')
- React.createElement
class Vnode(tag, attrs, children) { /*... */ }
React.createElement = function(tag, attrs, children) {
//...
return new Vnode(tag, attrs, children);
}
- Vue 异步组件
设计原则验证¶
- 构造函数和创建者分离
- 符合开放封闭原则
单例模式¶
介绍¶
- 系统中被唯一使用
- 一个类只能有一个实例
- 需要用到 java/typescript 的特性 (private),或通过 IIFE 和闭包实现
UML 类图¶
代码¶
class SingleObject {
login() {
console.log('login');
}
};
SingleObject.getInstance = (function() {
let instance = null;
return function() {
if (!instance) {
instance = new SingleObject();
}
return instance;
};
})();
场景¶
- jQuery 只有一个全局函数
$
if (window.jQuery !== null) {
return window.jQuery;
} else {
// 初始化...
}
- 登陆窗
- vuex 中的 store
设计原则验证¶
- 符合单一指责原则,实力化唯一对象
适配器模式¶
介绍¶
- 旧接口与使用者格式不兼容
- 中间加一个适配器转换接口
UML 类图¶
代码¶
// 旧接口
class Adaptee {
specificRequst() {
return '日本插头';
}
}
// 适配器
export class Target {
constructor() {
this.adaptee = new Adaptee();
}
request() {
const info = this.adaptee.specificRequst();
return `${info} - 转换器 - 中国插头`;
}
}
// 使用者
export class Client {
constructor(target) {
this.target = target;
}
do() {
const info = this.target.request();
console.log('适配器模式:', info);
}
}
场景¶
- Vue.computed()
设计原则验证¶
- 将旧接口和使用者进行分离
- 符合开发封闭原则
装饰器模式¶
介绍¶
- 为对象添加新功能
- 不改变其原有的结构和功能
UML 类图¶
代码¶
class Circle {
draw() {
console.log('draw a circle');
}
}
class Decorator {
constructor(circle) {
this.circle = circle;
}
draw() {
this.circle.draw();
this.setRedBoarder();
}
setRedBoarder() {
console.log('set red border');
}
}
场景¶
- ES7 装饰器
// 装饰器原理
@decorator
class A {}
// 等价于
class A {}
A = decorator(A) || A;
// mixin 示例
function mixins(...list) {
return function (target) {
Object.assign(target.prototype, ...list);
}
}
const Foo = {
foo() { alert('foo') }
}
@mixins(Foo)
class MyClass{}
let obj = new MyClass();
obj.foo();
// 装饰属性
// ...
// 装饰方法, case 1
function readonly(target, name, decriptor) {
decriptor.writable = false;
return decriptor;
}
@readonly
name() { return 'name'; }
// ...
// 装饰方法, case 2
function log(target, name, decriptor) {
var oldVal = decriptor.value;
decriptor.value = function() {
console.log(`Calling ${name}`);
return oldVal.apply(this, arguments);
}
return decriptor;
}
@log
getSomething() { return 'something'; }
- core-decorator
第三方lib, 提供常用装饰器
设计原则验证¶
- 将现有对象和装饰分离,两者独立存在
- 符合开发封闭原则
代理模式¶
介绍¶
- 使用者无权访问目标对象
- 中间加代理,通过代理做授权和控制
UML 类图¶
代码¶
class RealImage {
constructor(filename) {
this.filename = filename;
this.loadFromDisk()
}
loadFromDisk() {
console.log('loading...' + this.filename);
}
display() {
console.log('display: ' + this.filename);
}
}
class ProxyImage {
constructor(filename) {
this.realImage = new RealImage(filename);
}
display() {
this.realImage.display();
}
}
场景¶
- 网页事件代理
- JQuery
$.proxy
$('#div').click(function() {
var self = this;
var fn = function() {
console.log(this, self); // window, DOM 元素
$(self).css('color', 'red');
};
setTimeout(fn, 500);
});
// 使用 $.proxy
$('#div').click(function() {
var fn = function() {
$(this).css('color', 'red');
};
fn = $.proxy(fn, this);
setTimeout(fn, 500);
});
- ES6 Proxy
// 明星
const star = {
name: '蔡徐坤',
age: 23,
phone: 1861234567
};
// 经纪人
const agent = new Proxy(star , {
get: function(target, key) {
if(key === 'phone') {
return 'agent phone: 1351234567'
}
if (key === 'price') {
return 8000;
}
return target[key];
},
set: function(target, key, val) {
if (key === 'customPrice') {
if (val < 6000) {
throw new Error('价格太低');
} else {
target[key] = val;
return true;
}
}
}
});
设计原则验证¶
- 代理类与目标类分离,隔离开目标类和使用者
- 符合开发封闭原则
外观模式¶
介绍¶
- 为子系统的一组统一接口提供一个高层接口
- 使用者使用这个高层接口
UML 类图¶
代码¶
场景¶
设计原则验证¶
- 不符合单一职责原则和开放封闭原则,故谨慎使用,不可滥用
观察者模式¶
介绍¶
- 发布 & 订阅
- 一对多
UML 类图¶
代码¶
// 被订阅的主题
class Subject {
constructor() {
this.observers = [];
this.state = 'init';
}
getState() {
return this.state;
}
setState(state) {
this.state = state;
this.notifyAllObservers()
}
attach(observer) {
this.observers.push(observer);
}
notifyAllObservers() {
this.observers.forEach((observer) => {
observer.update();
});
}
}
// 观察者
class Observer {
constructor(name, subject) {
this.name = name;
this.subject = subject;
this.subject.attach(this);
}
update() {
console.log('Subject updated!', this.name, this.subject.state);
}
}
// 测试
let s = new Subject();
let o1 = new Observer('o1', s);
let o2 = new Observer('o2', s);
s.setState('new state');
场景¶
- 页面事件绑定
- Promise
- JQuery Callbacks
var callbacks = $.Callbacks();
callbacks.add(function(info) {
console.log('fn1', info);
});
callbacks.add(function(info) {
console.log('fn2', info);
});
callbacks.fire('let\'s go!');
- Nodejs 自定义事件
// 事件模块
const EventEmitter = require('events').EventEmitter;
const e1 = new EventEmitter();
e1.on('some', function(info) {
console.log('fn1', info);
});
e1.on('some', function(info) {
console.log('fn2', info);
});
e1.emit('some');
// 文件读取
const fs = require('fs');
const readStream = fs.createReadStream('./test.txt', { encoding: 'utf8' });
readStream.on('open', data => {
console.log('开始', data)
});
readStream.on('data', data => {
console.log('数据读取中, 已读', readStream.bytesRead);
});
readStream.on('close', data => {
console.log('结束')
});
// 处理 Http 请求
const http = require('http');
http.get('http://nodejs.org/dist/index.json', (res) => {
res.setEncoding('utf8');
let rawData = '';
res.on('data', (chunk) => { rawData += chunk; });
res.on('end', () => {
const parsedData = JSON.parse(rawData);
console.log(parsedData);
});
}).on('error', (e) => {
console.error(`Got error: ${e.message}`);
});
// 多进程通讯
// parent.js
const cp = require('child_process');
const n = cp.fork(`${__dirname}/sub.js`);
n.on('message', m => {
console.log('PARENT got message:', m);
});
n.send({ hello: 'world' });
// sub.js
process.on('message', m => {
console.log('CHILD got message:', m);
});
process.send({ foo: 'bar' });
- 其他场景
- Vue 和 React 组件生命周期触发
- Vue watch
设计原则验证¶
- 主题与观察者分离,解耦
- 符合开放封闭原则
迭代器模式¶
介绍¶
- 顺序访问一个集合
- 使用者无需知道集合的内部结构(封装)
- 迭代器模式通常都是对一个数组,集合等进行访问,迭代器的设计是为了封装一个方法,可以对多种数据类型进行访问
UML 类图¶
代码¶
class Container {
constructor(list) {
this.list = list;
}
getIterator() {
return new Iterator(this);
}
}
class Iterator {
constructor(container) {
this.list = container.list;
this.index = 0;
}
hasNext() {
if (this.index >= this.list.length) {
return false;
}
return true;
}
next() {
if (this.hasNext()) {
return this.list[this.index++];
}
return null;
}
}
// 测试
const c = new Container([1, 2, 3]);
const it = new Iterator(c);
while(it.hasNext()) {
console.log(it.next());
}
场景¶
- jQuery each
- ES6 Iterator
Array/Map/Set/String/TypedArray/arguments/NodeList/Generator,
需要有一个统一的遍历接口来遍历多有数据类型,
以上数据类型,都有[Symbol.iterator]属性,执行函数返回一个迭代器。
let arr = [1, 2, 3, 4]
let m = new Map()
m.set('a', 100)
m.set('b', 200)
function each(data) {
const iterator = data[Symbol.iterator](); // 返回一个迭代器
let item = { done: false }
while (!item.done) {
item = iterator.next();
console.log(item);
}
}
each(arr)
each(m)
设计原则验证¶
- 迭代器对象与容器对象分离
- 符合开放封闭原则
状态模式¶
介绍¶
- 一个对象有状态的变化
- 每次状态变化都会触发一个逻辑
- 不能总是用
if...else
控制
UML 类图¶
代码¶
class State {
constructor(color) {
this.color = color;
}
handle(context) {
console.log('当前是' + this.color + '灯');
context.setState(this);
}
}
class Context {
constructor() {
this.state = null;
}
getState() {
return this.state;
}
setState(state) {
this.state = state;
}
}
// 测试
const context = new Context();
const r = new State('red');
r.handle(context);
const b = new State('blue');
b.handle(context);
const g = new State('green');
g.handle(context);
场景¶
- FSM 有限状态机
- Promise
var fsm = new StateMachine({
init: 'pending',
transitions: [
{
name: 'resolve',
from: 'pending',
to: 'fullfilled'
},
{
name: 'reject',
from: 'pending',
to: 'rejected'
}
],
methods: {
onResolve: function (state, data) {
data.successList.forEach(fn => fn())
},
onReject: function (state, data) {
data.failList.forEach(fn => fn())
}
}
})
// 定义 Promise
class MyPromise {
constructor(fn) {
this.successList = [];
this.failList = [];
fn(() => {
// resolve 函数
fsm.resolve(this);
}, () => {
// reject 函数
fsm.reject(this);
})
}
then(successFn, failFn) {
this.successList.push(successFn);
this.failList.push(failFn);
}
}
设计原则验证¶
- 状态和主体分离
- 符合开放封闭原则
原型模式¶
介绍¶
- 原型实例指向创建对象的种类
- clone 原型, 生产一个新的对象
场景¶
- ES5 的
Object.create
var prototype = {
say: function() { console.log('Hi'); }
}
var obj = Object.create(prototype);
obj.say();
obj.__proto__ === prototype;
桥接模式¶
介绍¶
- 将抽象部分与实现部分解耦
- 联接多个
代码¶
class One {
constructor(a) {
this.a = a;
}
}
class Two {
constructor(b) {
this.b = b;
}
}
class BridgeClass {
constructor(a, b) {
this.one = new One(a);
this.two = new Two(a);
}
}
设计原则验证¶
- 弱化了代码之间的耦合,将抽象和其实现分离
- 符合开发封闭原则
组合模式¶
介绍¶
- 将对象组合成树形结构,以表示"部分-整体"的层次结构
- 用户对单个对象和组合对象的使用具有一致性
- 无须关心对象有多少层,调用时只需在根部进行调用
代码¶
class MacroCommand {
constructor() {
this.list = [];
}
add(task) {
this.list.push(task);
}
excute() {
for (let i = 0; i < this.list.length; i++) {
this.list[i].excute();
}
}
}
// 测试
const task1 = {
excute: () => console.log('起床')
};
const task2 = {
excute: () => console.log('听音乐')
};
const task3 = {
excute: () => console.log('看电影')
};
const command1 = new MacroCommand();
command1.add(task1);
const command2 = new MacroCommand();
command2.add(task2);
command2.add(task3);
const macroCommand = new MacroCommand();
macroCommand.add(command1)
macroCommand.add(command2)
macroCommand.excute();
场景¶
- 扫描文件夹
class Folder {
constructor(folder) {
this.folder = folder;
this.list = [];
}
add(resource) {
this.list.push(resource);
}
scan() {
console.log('开始扫描文件夹:', this.folder);
for (let i = 0, folder; folder = this.list[i++];) {
folder.scan();
}
}
}
class File {
constructor(file) {
this.file = file;
}
add(resource) {
throw Error('文件下不能添加其它文件夹或文件');
}
scan() {
console.log('开始扫描文件:', this.file);
}
}
// 测试
const folder = new Folder('根文件夹');
const folder1 = new Folder('Movie 文件夹');
const folder2 = new Folder('Book 文件夹');
const file1 = new File('海上钢琴师.mkv');
const file2 = new File('银翼杀手.mkv');
const file3 = new File('小王子.pdf');
folder1.add(file1);
folder1.add(file2);
folder2.add(file3);
folder.add(folder1);
folder.add(folder2);
folder.scan();
设计原则验证¶
- 整体与单个节点抽象出来
- 符合开放封闭原则
享元模式¶
介绍¶
- 共享内存(时间换空间)
- 运用共享技术来有效支持大量细粒度的对象
设计原则验证¶
- 将相同的部分抽象出来
策略模式¶
介绍¶
- 不同策略分开处理,抽象成策略类
- 算法被封装在策略类内部的方法里
- 减少 if/else 判断
UML 类图¶
代码¶
// 策略
class PerformanceS {
calculate(salary) {
return salary * 4;
}
}
class PerformanceA {
calculate(salary) {
return salary * 3;
}
}
class PerformanceB {
calculate(salary) {
return salary * 2;
}
}
// 奖金
class Bonus {
constructor() {
this.salary = null; // 原始工资
this.strategy = null; // 绩效等级对应的策略对象
}
setSalary(salary) {
this.salary = salary;
}
setStrategy(strategy) {
this.strategy = strategy;
}
getBonus() {
return this.strategy.calculate(this.salary);
}
}
// 测试
const bonus = new Bonus()
bonus.setSalary(50000);
bonus.setStrategy(new PerformanceA());
bonus.getBonus();
场景¶
- 表单校验规则
- 实现 Animate 动画效果
设计原则验证¶
- 利用组合、委托和多态等思想,有效地避免多重条件选择语句
- 对开放—封闭原则的完美支持,将算法封装在独立的 strategy 中,易于切换,易于理解,易于扩展
- 利用组合和委托来让Context拥有执行算法的能力,这也是继承的一种更轻便的替代方案
职责链模式¶
介绍¶
- 一步操作可能分多个职责角色完成
- 角色分开,用一个链串起来
- 发起者与各个处理者隔离
UML 类图¶
代码¶
class Action {
constructor(name) {
this.name = name;
this.nextAction = null;
}
setNextAction(action) {
this.nextAction = action;
}
handle() {
console.log(`${this.name} 处理`);
if (this.nextAction) {
this.nextAction.handle();
}
}
}
// 测试
const a1 = new Action('组长');
const a2 = new Action('经理');
const a3 = new Action('总监');
a1.setNextAction(a2);
a2.setNextAction(a3);
a1.handle();
场景¶
- JS 链式操作
- Promise.then
设计原则验证¶
- 发起者与各个处理者隔离
- 符合开放封闭原则
中介者模式¶
介绍¶
- 解除对象与对象之间的耦合关系
- 当一个对象发生改变时,只需要通知中介者对象即可
代码¶
class Mediator {
constructor(a, b) {
this.a = a;
this.b = b;
}
setA() {
const num = this.b.number;
this.a.setNumber(num * 100)
}
setB() {
const num = this.a.number;
this.b.setNumber(num / 100)
}
}
class A {
constructor() {
this.number = 0;
}
setNumber(num, mediator) {
this.number = num;
if (mediator) {
mediator.setB();
}
}
}
class B {
constructor() {
this.number = 0;
}
setNumber(num, mediator) {
this.number = num;
if (mediator) {
mediator.setA();
}
}
}
// 测试
const a = new A();
const b = new B();
const m = new Mediator(a, b);
a.setNumber(100, m);
console.log(a.number, b.number);
b.setNumber(100, m);
console.log(a.number, b.number);
场景¶
设计原则验证¶
- 中介者模式是迎合迪米特法则的一种实现。迪米特法则,也叫最小知识原则,是指一个对象应该尽可能少地了解另外的对象。
- 通过中介者隔离关联对象。
- 符合开放封闭原则。
模版方法模式¶
介绍¶
- 抽象父类,定义算法骨架, 封装公共方法的实现,确定步骤执行顺序;
- 实现具体子类,将一些步骤延迟到子类中,重定义一些步骤。
UML 类图¶
命令模式¶
介绍¶
- 执行命令时,发布者与执行者分开
- 命令对象作为中转站
UML 类图¶
代码¶
// 发起者
class Invoker {
constructor(cmd) {
this.command = cmd;
}
invoke() {
console.log('发出命令');
this.command.cmd();
}
}
// 命令对象
class Command {
constructor(reciever) {
this.reciever = reciever;
}
cmd() {
console.log('触发 execute');
this.reciever.execute();
}
}
// 执行者
class Reciever {
execute() {
console.log('execute');
}
}
// 测试
const soldier = new Reciever();
const trumpeter = new Command(soldier);
const general = new Invoker(trumpeter);
general.invoke();
场景¶
- 网页富文本编辑器,
document.execCommand
API
设计原则验证¶
- 符合开放封闭原则
备忘录模式¶
介绍¶
- 随时记录一个对象的状态变化
- 随时恢复到之前的某个状态
代码¶
// 备忘项
class Memento {
constructor(content) {
this.content = content;
}
getContent() {
return this.content;
}
}
// 备忘录
class CareTaker {
constructor() {
this.list = [];
}
add(memento) {
this.list.push(memento);
}
get(index) {
return this.list[index];
}
getList() {
return this.list
}
}
// 编辑器
class Editor {
constructor() {
this.content = null;
}
setContent(content) {
this.content = content;
}
getContent() {
return this.content;
}
saveContentToMemento() {
return new Memento(this.content);
}
getConentFromMemento(memento) {
this.content = memento.getContent();
}
}
场景¶
- 分页组件,返回上一页事,可对数据进行缓存
- 编辑器
设计原则验证¶
访问者模式¶
介绍¶
- 使用一个访问者类,改变元素类的执行算法
- 将数据结构与数据操作分离
代码¶
class Elements {
action() {
console.log('hi~');
}
accept(visitor) {
visitor.visit(this)
}
}
// 访问者
class Visitor {
visit(elements) {
elements.action();
}
}
// 测试
const v = new Visitor();
e.accept(v);
场景¶
- 通过 call 或 apply 的方式传递 this 对象给一个 Visitor 函数
设计原则验证¶
- 符合单一职责原则: 被封装的操作通常来说都是易变, 但不改变元素类本身
- 扩展性良好:元素类可以通过接受不同的访问者来实现对不同操作的扩展
解释器模式¶
介绍¶
- 描述语言语法如何定义,如何解释和编译
场景¶
- babel、sass、less
控制反转模式¶
介绍¶
- 控制反转,Inversion of Control,简称 IoC。
- 通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。
- 依赖注入(Dependency Injection,简称 DI),由 IoC 容器在运行期间,动态地将某种依赖关系注入到对象之中。DI 是 IoC 的一种实现。
- 总结:控制反转,控制权从使用者本身转移到第三方容器上,而非是转移到被调用者上。控制反转是一种思想,依赖注入是一种设计模式。
代码¶
- 两个模块: 请求服务和路由
var service = function() {
return { name: 'Service' };
}
var router = function() {
return { name: 'Router' };
}
- 依赖上述模块
var doSomething = function(service, router, other) {
var s = service();
var r = router();
};
- 设计管理依赖的工具
- 可以注册依赖
- 注入器应该接受一个函数,并且返回一个已获得所需资源的函数
- 我们需要简短优雅的语法
- 注入器应该保持传入函数的作用域
- 被传入的函数应该可以接受自定义参数,不仅仅是被描述的依赖。
// 假设 write a module called injector
var doSomething = injector.resolve(['service', 'router'], function(service, router, other) {
debugger;
expect(service().name).to.be('Service');
expect(router().name).to.be('Router');
expect(other).to.be('Other');
});
doSomething('Other');
- injector 的实现
var injector = {
dependencies: {},
register: function(key, value) {
this.dependencies[key] = value;
},
resolve: function(deps, func, scope) {
var args = [];
for(var i = 0; i < deps.length, d = deps[i]; i++) {
if(this.dependencies[d]) {
args.push(this.dependencies[d]);
} else {
throw new Error('Can\'t resolve ' + d);
}
}
return function() {
func.apply(scope || {}, args.concat(Array.prototype.slice.call(arguments, 0)));
}
}
}
- 直接注入作用域
var injector = {
dependencies: {},
register: function(key, value) {
this.dependencies[key] = value;
},
resolve: function(deps, func, scope) {
var args = [];
scope = scope || {};
for(var i=0; i < deps.length, d=deps[i]; i++) {
if(this.dependencies[d]) {
// 区别在于,直接将依赖加到scope上
// 这样就可以直接在函数作用域中调用了
scope[d] = this.dependencies[d];
} else {
throw new Error('Can\'t resolve ' + d);
}
}
return function() {
func.apply(scope || {}, Array.prototype.slice.call(arguments, 0));
}
}
}
// 将依赖加到作用域上,这样的好处是不用再参数里加依赖了,已经是函数作用域的一部分了。
var doSomething = injector.resolve(['service', 'router'], function(other) {
expect(this.service().name).to.be('Service');
expect(this.router().name).to.be('Router');
expect(other).to.be('Other');
});
doSomething("Other");
场景¶
- RequireJS/AMD 的模块加载器的实现
设计原则验证¶
- 依赖倒置原则(Dependence Inversion Principle)的一种实现。
- 高层模块不再依赖低层模块;两个都依赖抽象。
- 抽象不再依赖具体实现。
- 面向接口编程,而非面向实现编程。