[toc]
从 javascript 的角度去了解 设计模式 与各模式的适用场景。
0. 个人对于设计模式的认识:
- 设计模式其实就是代码实现的各种最佳实践,然后由前人总结归纳并取了个相应的的名字,有些时候我们在做项目的时候可能不知不觉就运用了设计模式中的一些思想。
- 前端的各种框架或工具里也运用到了各种设计模式,例如:antd 的
Modal,jquery中的$, lodash 中的_是单例模式;webApi 中的addEventListener, jquery 中的on方法其实都可以成为观察者模式;很多类库中对于浏览器兼容性的处理算是外观模式, 事件的冒泡与捕获是责任链模式。 - 一个项目中根据需要可能会在设计的时候运用多种设计模式。
- redux 的 store 就是一个单例,其内部的 subscribe 对于dispatch的监听,其实就是观察者模式。compose 其实就是组合模式,将多个函数组合成一个函数, 等等......
- React 的核心其实是结合了 状态模式与观察者模式,观察 state 的变化并实时更新 UI 层。
- 其实从前端的角度去理解后端的设计模式有其局限性。毕竟 js 在目前来说大部分情况下的运用都不是面向对象的。而且大部分讲设计模式的书籍都是以面向对象的方式为范例去讲解的(例如java),如果从 js 层面去理解设计模式的话就不能直接用面向对象中的设计模式的概念往 js 上套,需要从另一层面去理解其内在的思想、想要解决的痛点。
- 很多创建型模式与结构型模式中,很多对于他们范例与定义是拿
类来做说明的,但其实 js 中并没有 类这个概念,即使是 ES6 中的class也只是构造函数的语法糖,typeof class === 'function'。其特性与面向对象的语言也有很大的不同。例如享元模式如果从类的创建与实例缓存层面去理解就不太容易,但是从前端的 DOM 创建与复用层面去理解的话就容易得多。
- 很多创建型模式与结构型模式中,很多对于他们范例与定义是拿
- 有些设计模式的 设计理念 与 想要解决的痛点是一致的。例如单例模式与享元模式,都是为了减少对象的创建,对既有的对象进行复用,减少内存占用。不过一个是在创建层面去优化,一个从应用层去优化。
- 对于前端来说,我认为比较常用到的设计模式有:单例模式、适配器模式、装饰器模式、外观模式、过滤器模式、组合模式、与大部分的行为型模式。
- 单例模式可以解决单一功能对象重复使用的问题。一般用于在某个作用域的顶层。创建一次整个作用域公用这个实例。ES6 的 Symbol 类型其实就是一个单例,标识独一无二的。
- 适配器模式 可以解决不同模块的统一使用问题,是一层包装。例如我们在 Angular 代码中引入 React 的项目作为子模块。
- 装饰器模式 可以解决 class 的增强困难的问题,通常每个装饰器的功能是单一的,有些时候一个类为了功能的复用可能继承了好几层类,这时候把单一的功能抽出来用装饰器去增强的话代码更灵活解耦、容易理解、易于重构。
- 外观模式其实本质上与适配器模式有点相似,都是为了解决代码的适配问题,只不过维度不一样。适配器是以模块为维度,外观是以某个功能能为维度。
- 过滤器模式场景更多,最简单的 Array.filter 就是个过滤器。
- 组合模式其实就是各种功能、组件、甚至函数的组合,使代码在使用的时候更加简洁方便。例如 redux 中
applyMiddleware对每个中间件的组合,使其在发起 dispatch 的时候不需要一个个去调用中间件函数,调用组合好的函数就可以。 - 观察者模式 是为了解决模块间的通信问题,使不同的模块在相同的时间可以同步通知状态并作出相应的反应。
1. 单例模式(Singleton)
单例模式是创建型实例,意图产生一个类的唯一实例。
- type: 创建型模式
- 优点:
- 在内存里只有一个实例,较少了内存的开销,加快对象访问速度。
- 避免对资源的多重占用。
- 适用场景:
- 多个地方被使用,尤其是频繁被创建、销毁的对象(例如Dialog 或者 Toaster)。
- 适合用来记录全局的状态(例如 Redux)。
- 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
- 示例:
把 Dialog 做成一个单例的模式, 全局只有一个 Dialog 实例。
export default createDialog = ( _ => {
// 声明 Dialog 实例的引用,其始终存在于这个闭包内
let instanceDialog;
return () => {
const content = (<div>{option.title}</div>);
class Dialog {
constructor() {
super({});
}
show(option) {
const dialogDom = document.createElement('div');
const content = (<div>{ option.title }</div>);
document.body.appendChild(dialogDom);
ReactDOM.render(content, dialogDom);
}
}
// 如果已经实例过 Dialog, 那么就是用这个 or 实例化 Dialog
return instanceDialog || (instanceDialog = new Dialog());
};
})();
使用时:
var Dialog = createDialog();
Dialog.show({title: '简单的 Dialog'});
2. 简单工厂模式 (Simple Factory Pattern)
简单工厂模式是创建型模式,由一个方法来决定到底要创建哪个类的实例,其所实例化的类型在创建时并不确定, 而是在使用时候根据参数决定实例化某个类。
- type: 创建型模式
- 适用场景:
- 需要根据不同参数产生不同实例,这些实例有一些共性的场景。
- 使用者只需要使用产品,不需要知道产品的创建细节。
- 示例:
创建一个学生类,每一个学生都可以在其自身的任务列表里添加学习任务,例如家庭作业 与 问卷
// createTask 是一个函数,功能很单一,其会根据传入的任务类型,new 一个相应的任务。这是一个工厂
const createTask = (() => {
const taskMap = {
homework: () => {
return new Homework(); // Homework 的类
},
question: () => {
return new Question(); // Question 的类
},
};
return (taskType) => {
taskMap[taskType]();
}
})();
// 这是一个Student 类,用于创建学生,其内部有一个任务列表(taskList),通过addTask方法给任务列表添加新的任务
class Student {
constructor() {
this.taskList = [];
}
addTask(taskType) {
// 调用简单工厂函数去创建任务
this.taskList.push(createTask(taskType));
}
}
// 创建一个学生:小明,并给小明布置家庭作业
const xiaoming = new Student();
xiaoming.addTask('homework');
整个过程中,其实由三部分组成,
- Student: 用于创建学生,创建时不关心其是什么样子,分配有什么任务。
- createTask: 这是一个工厂,根据传入的参数创建不同的对象并返回给对象使用类(Student),内部有不同对象(task)的创建过程,如果新增taskType,需要在这里添加。
- Homework、Question等对象类:不同的taskType产生的不同类,也就是上面的工厂(createTask)生产的产品。
3. 工厂方法模式 (Factory Method Pattern)
工厂方法模式属于类创建型模式。工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象,这样做的目的是将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品类。
- type: 创建型模式
- 跟简单工厂模式的不同:
- 简单工厂模式中,所有的创建逻辑和判断逻辑的代码都内聚在一个工厂中进行,根据传入的参数实例相应的产品。这样随着项目越来越大,每次增加子类或者删除子类对象的创建都需要打开这简单工厂类来进行修改,这个工厂也会越来越臃肿,内部有更多的
switch case结构,代码耦合都会越来越高。 - 工厂方法模式,将产品的创建方法拆开来,每一个产品都有一个自己的工厂类,将产品的实例化下放到在子类中去进行,核心类就变成了抽象类。
- 简单工厂模式中,所有的创建逻辑和判断逻辑的代码都内聚在一个工厂中进行,根据传入的参数实例相应的产品。这样随着项目越来越大,每次增加子类或者删除子类对象的创建都需要打开这简单工厂类来进行修改,这个工厂也会越来越臃肿,内部有更多的
- 适用场景:
- 客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建;客户端需要知道创建具体产品的工厂类。
- 在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象
- 实例:
假如说,我们要抽象出一个 teacher 类,通过这个抽象类我们可以创建出各种各样的老师。
// 英语老师类
class EnglishTeacher {}
// 美术老师类
class ArtTeacher {}
// 老师工厂类,其内部有各种老师的创建方法
class TeacherFactory {
englishTeacherCreator() {
this.name = '英语老师';
return new EnglishTeacher();
}
artTeacherCreator() {
this.name = '美术老师';
return new ArtTeacher();
}
}
// 创建一个英语老师
const englishTeacherFactory = new TeacherFactory().englishTeacherCreator;
const englishTeacher = englishTeacherFactory();
上面的代码中每一种老师都有一个独立的创建方法,实例时必须先将创建方法得到,虽然创建的步骤变多了但是更利于日后 Teacher 工厂类的扩展,需要新增老师类型的时候,要新增创建方法和产品类
4. 抽象工厂模式(Abstract Factory)
5. 建造者模式(Builder Pattern)
创建型模式的一种,主要关注于将各个小对象组装成一个复杂对象的过程
- type: 结构型模式
- 优点:
- 建造者相对独立,易扩展
- 示例:
肯德基里有薯条、汉堡、可乐、雪碧、等各种各样的单品,这些单品组成了各种花样的套餐
class Burger {} // 汉堡类 class HotDog {} // 热狗类 class Cola {} // 可乐类 class Sprite {} // 雪碧类 // 套餐类 class KFCPackage { conctructor() { this.stapleFood = ''; this.Drink = ''; } } // 建造者类,里面有汉堡、热狗、可乐等生产机 class KFCBuilder { burgerBuilder() { this.stapleFood = new Burger(); } hotDogBuilder() { this.stapleFood = new HotDog(); } colaBuilder() { this.drink = new Cola(); } spriteBuilder() { this.drink = new Sprite(); } getFood() { var foodPackage = new KFCPackage(); foodPackage.stapleFood = this.stapleFood; foodPackage.drink = this.drink; return foodPackage; } } // 汉堡套餐指挥者类 class BurgerPackageDirector { create(builder) { this.stapleFood = builder.burgerBuilder(); this.drink = builder.colaBuilder(); } } // 热狗套餐指挥者类 class hotDogPackageDirector { create(builder) { this.stapleFood = builder.hotDogBuilder(); this.drink = builder.spriteBuilder(); } } // 建造者实例 const foodBuilder = new KFCBuilder(); // 汉堡指挥者实例 const burgerDirector = new BurgerPackageDirector(); // 热狗指挥者实例 const hotDogDirector = new hotDogPackageDirector(); // 生产汉堡套餐 burgerDirector.create(foodBuilder); // 获取汉堡套餐,里面有汉堡和可乐 var burgerPackage = foodBuilder.getFood(); // 生产热狗套餐 hotDogDirector.create(foodBuilder); // 获取热狗套餐,里面有热狗和雪碧 var hotDogPackage = foodBuilder.getFood();- 建造者模式结合装饰器模式
上面的KFC的例子中,每当推出一个新的套餐的时候,都要增加一个全新的指挥类,这里的逻辑可以使用修饰器来简化
// 套餐类与建造者不变 ...... // 指挥者装饰器,接受食物名与饮料名两个参数 const directorDecorator = (option) => { return (wrappedClass) => { wrappedClass.create = (builder) => { this.stapleFood = builder[option.foodName](); this.drink = builder[option.drinkName](); } return wrappedClass; } } // 指挥者基类 class basePackageDirector { static create } // 推出鸡肉卷牛奶套餐 @directorDecorator({foodName: 'chickenRoll', drinkName: 'milk'}) class chickenRollDirector extends basePackageDirector; // 鸡肉卷牛奶指挥者实例 const chickenRollDirector = new chickenRollDirector(); // 生产鸡肉卷牛奶套餐 chickenRollDirector.create(foodBuilder); // 获取鸡肉卷牛奶套餐,里面有鸡肉卷和牛奶 var burgerPackage = foodBuilder.getFood(); - 建造者模式结合装饰器模式
6. 适配器模式(Adapter Pattern)
适配器模式如字面意义上的功能,适配两个不同的模块,使其可以一起工作,可以类比生活当中的读卡器,使得电脑与内存卡之间经过读卡器的转换可以正常连接
-
type: 结构型模式
-
优点:
- 增加各模块的复用程度,使不兼容的模块一起运行。
- 使各模块间的关系更加灵活。
-
适用场景:
- 一般不是项目一开始就设计成酱紫,而是对现有项目进行兼容性改造而采用。
- 新老接口更替时对老数据的兼容。
-
示例:
假如我们有两个项目,一个是 angular1.5 的项目,一个是 React 的项目,由于性能、工程效率、复用性等原因,我们打算在将一些可以通用的组件使用React来写,然后通过适配器在 Angular 中复用。
// React 模块: 需要导出适配器需要的的成员与组件
export const platform = {
React,
ReactDOM,
Component1,
Component2,
};
// Angular 模块: 需要一个专门用于调用 React 组件的 directive,其接受两个属性,react 组件的组件名、props
var platform = require('xxx'); // 引入公用的 React 模块
var React = platform.React;
var ReactDOM = platform.ReactDOM;
Module.directive('reactConnector',
[function () {
return {
restrict: 'EA',
template: '<div></div>',
replace: true,
scope: {
componentName: "<",
props: '='
},
link: function (scope, element) {
/**
* 因为angular 中不支持 jsx,所以使用`React.createElement`创建节点,
* 将需要使用的 React 组件作为他的children,并将 props 传进去
* 被创建出来的组件 ReactConnectorChild 内部会将他的 children render 出来
* 将创造出的 Element 挂载到 angular 节点上
*/
function render() {
ReactDOM.render(
React.createElement(ReactConnectorChild, angular.extend({
children: [
React.createElement(platform[componentName]),
]
}, scope.props)),
element[0]
);
}
// 监听注销事件,移除节点
scope.$on('$destroy', () => {
ReactDOM.unmountComponentAtNode(element[0]);
});
render();
}
};
}]
);
// 使用 reactConnector 指令的时候
<react-connector componentName="Component1" props="props" />
假如说我想要在angular模块内实时监听 React 模块中内部state的变化,这时候可以使用 观察者模式 创建一个单例的模块
// 创建一个模块,专门用来进行 React 模块的 state 发生改变的时候派发事件
export const stateListener = (() => {
let dispatchEvent, listenerEvent;
const eventMap = {}; // 存储事件的容器
// return 一个函数,用以移除对state的监听
const removeListener = (key) => {
return () => {
delete eventMap[key];
}
};
listenerEvent = (key, callback) => {
// 将监听函数加入事件模型
eventMap[key] = callback;
return removeListener(key);
};
dispatchEvent = (key, state) => {
const eventCallback = eventMap[key];
eventCallback && eventCallback(state);
};
return {
listenerEvent,
dispatchEvent,
};
}())
/** Angular 模块
* reactConnector 指令中,可以新加入两个参数
* listenedName,与 listenedCallback,用于规定监听某个 React 组件的 state
* 由于React组件是通用的,所以请保持 listenedName 是单一的
* 注销时清掉 监听
*/
import { stateListener } from 'xxx';
link: function (scope, element) {
// ...
const listener = stateListener.listenerEvent(scope.listenedName, scope.listenedCallback);
scope.$on('$destroy', () => {
// ...
listener();
});
}
/** React 模块,可以创建一个基类
* 其在 DidUpdate 的时候就派发事件并将更新过的state传递出去,事件名为定好的 listenedName
* 需要同步state的话,通用组件就可以去继承它
*/
import { stateListener } from 'xxx';
export default class basePlatform extends React.Component {
componentDidUpdate(prevProps, prevState) {
const listenedName = this.props.ListenedName;
if (listenedName) {
stateListener.dispatchEvent(listenedName, prevState);
}
}
}
7. 装饰器模式(Decorator pattern)
这种模式是创建一个修饰器类,用来包装原有的类,并在保持被包装的类方法签名完整性的前提下,提供额外的功能,其实所谓的高阶组件就是一个装饰器。
-
type: 结构型模式
-
使用场景:
- 需要对原有的类进行扩展,但又不想使用继承的方式增加一些子类。
- 需要大范围的对既有的类进行功能上的扩展。
- 其实 react-redux 中的 connect 方法,就是返回了一个修饰器函数,用于包装传入的。 WrappedComponent
-
优点:
- 通用功能可以简单便捷地进行的复用。
- 装饰类和被装饰类可以独立发展,不会相互耦合。
-
与适配器模式的区别:
- 其跟适配器模式有相似之处,都是通过封装其他对象达到设计的目的,但是它们的形态有很大区别。
- 适配器是为了包装某个模块的接口,从而使两个不同模块之间能实现连接,比如上面举的将 React 模块放在 Angular 的指令中的例子。
- 而装饰器是为了仅仅是包装现有的模块,给其增加功能,不改变其内部结构与原有功能。
-
示例:
比如说,为了保障我们现在项目上资源的安全性,需要在全网所有有关数据与列表的页面加水印,如果我们一个页面一个页面的去加相同的功能的话,会非常的费时费力,而且还容易出错。这时候就可以写一个装饰器,在需要加水印的组件上包装一下。
/** * 装饰器函数,其接受两个参数 * WrappedComponent为需要进行包装的组件, option 为需要的参数,如水印的大小,透明度等 * return 一个包装过的组件,其有了添加水印的功能 */ const watermarkDecorator = (WrappedComponent, option) => { return class Connect extends React.Component { constructor(props: IProps) { super(props); this.waterType = option.waterType; } render() { const props = this.props; // 在传入的组件外包一层节点,并添加水印,不改变原组件原有的功能 const backStyle = { backgroundImage: 'url(xxx.png)', }; return( <div className="wrapped-watermark" style={backStyle}> <WrappedComponent {...props} /> </div> ); } }; }; // 使用的时候,将容器组件用这个装饰器包装一下就可以 const someComponent = watermarkDecorator(WrappedComponent, option);如果项目中支持 ES7 语法的话还可以使用修饰器语法来进行包装组件,不过修饰器的写法要变一下,因为ES7修饰器只支持一个参数
const watermarkDecorator = (option) => { return (WrappedComponent) => { return class Connect extends React.Component { constructor(props: IProps) { super(props); this.waterType = option.waterType; } render() { const props = this.props; // 在传入的组件外包一层节点,并添加水印,不改变原组件原有的功能 const backStyle = { backgroundImage: 'url(xxx.png)', }; return( <div className="wrapped-watermark" style={backStyle}> <WrappedComponent {...props} /> </div> ); } }; }; }; // 使用的时候,直接在原来的组件上加上修饰器并传入参数就可以 @watermarkDecorator(option) class someComponent { // ... }- 装饰器模式结合简单工厂模式
回想上面简单工厂模式里面的例子,假如说我们创建学生的时候,可能会小学生、高中生等不同类型的学生怎么办?可能需要每一个学生类型都要去写一个class,或者可以写一个基类,然后让不同类型的学生去继承,但是高中生又分化为理科生与文科生呢?那文科生又要去继承高中生的类。随着学生类型越分越细,继承下来依赖的类越来越多,相似功能的方法也会越来越多,代码的正交性也会变差,后期的可维护也会降低。所以我们可以把Student作为一个基类,如果需要扩展不同的类型的学生,直接用修饰器去修饰。
以 Student 为基类,使用修饰器创建出普通学生与美术特长生两种类,不同的学生有不同默认的学习任务
这样,再需要有新的学生类型,只需要去创建新的修饰器添加特定的行为,各个修饰器可以进行组合// 添加学习任务的工厂函数 const createTask = (() => { const taskMap = { homework: () => { return new Homework(); }, English4: () => { return new English4(); }, art: () => { return new Art(); }, }; return (taskType) => { taskMap[taskType](); }; })(); // Student 基类,创建子类的时候只需要给其添加修饰器 class Student { static addTask(taskType) { // 调用简单工厂函数去创建任务 this.taskList.push(createTask(taskType)); } taskList = []; } // 高中生的修饰器 const heightStudentDecorator = (WrappedClass) => { WrappedClass.addTask('English4'); return WrappedClass; }; // 美术生的修饰器 const artStudentDecorator = (WrappedClass) => { WrappedClass.addTask('art'); return WrappedClass; }; @heightStudentDecorator class HeightStudent extends Student; @artStudentDecorator @heightStudentDecorator class ArtStudent extends Student; // 创建一个普通学生: 小明 const xiaoming = new Student(); // 创建一个高中生: 小刚 const xiaogang = new HeightStudent(); // 创建一个高中美术特长生: 小红 const xiaohong = new ArtStudent();
- 装饰器模式结合简单工厂模式
8. 代理模式 (Proxy Pattern)
代理模式的定义是把对一个对象的访问, 交给另一个代理对象来操作。
-
type: 结构型模式
-
适用场景:
- 创建一个对象需要很大的成本,可以通过一个代理对象来代表一个大对象
- 为了安全性考虑不允许直接访问对象,通过一个代理来作为其的访问层。
-
优点:
- 模块职责清晰:主类只关心其核心业务。
- 代理对象可以在访问者和目标对象之间起到中介的作用,保护目标对象。
-
示例:
考试的时候,需要根据分数的高低来决定去哪个学校,这个分数线是由教育局定制的,但是我们又不能直接去教育局问可以去哪个学校,而是通过一个教育局的代理查询系统来查看。
// 教育局的类 class EducationBureau { static goToHarvard(name) { console.log(name + '去哈佛大学'); return new Harvard(); } static goToTsinghua(name) { console.log(name + '去清华大学'); return new Tsinghua(); } static goToHomedun(name) { console.log(name + '去家里蹲大学'); return new Homedun(); } } // 代理查询系统的类 class Counselor { static distributionSchool(student) { var studentName = student.name; var studentFraction = student.fraction; if (studentFraction > 80) { EducationBureau.goToHarvard(studentName); } else if (studentFraction > 60) { EducationBureau.goToTsinghua(studentName); } else { EducationBureau.goToHomedun(studentName); } } } // 学生类,可以传入其学生信息,包括考试分数 class Student { constructor(studentInfo) { this.name = studentInfo.name; this.fraction = studentInfo.fraction; } } // 当需要查询一个学生的可报名院校时,将学生信息输入系统就可以 const xiaoming = new Student({name: 'xiaoming', fraction: 99}); Counselor.distributionSchool(xiaoming); // xiaoming 去哈佛大学 const xiaohong = new Student({name: 'xiaohong', fraction: 0}); Counselor.distributionSchool(xiaohong); // xiaohong 去家里蹲大学 -
代理模式 配合 适配器模式
继续上面的例子,虽然现在学生可以输入成绩查询录取院校了,但是真实情况是全国有不同的试卷,比如说全国卷、山东卷、江苏卷。每个学校对不同卷的分数线也是不同的,比如说清华大学对北京地区学生的录取分数线是86分,对山东地区学生的录取分数线是90分。这时候不同户籍的学生就要去不同地区的查询系统查询。
// 江苏卷修饰器 const JiangsuTestPaperDecorator = (WrappedClass) { const scoreLineMap = { Harvard: 90, Tsinghua: 85, Homdun: 60, }; WrappedClass.scoreLineMap = scoreLineMap; } // 山东卷修饰器 const ShandongTestPaperDecorator = (WrappedClass) { const scoreLineMap = { Harvard: 99, Tsinghua: 83, Homdun: 60, }; WrappedClass.scoreLineMap = scoreLineMap; } // 代理查询系统的基类 class Counselor { static scoreLineMap = { Harvard: 88, Tsinghua: 80, Homdun: 70, } static distributionSchool(student) { const studentName = student.name; const studentFraction = student.fraction; const scoreLineMap = Counselor.scoreLineMap; if (studentFraction > scoreLineMap.Harvard) { EducationBureau.goToHarvard(studentName); } else if (studentFraction > scoreLineMap.Tsinghua) { EducationBureau.goToTsinghua(studentName); } else { EducationBureau.goToHomedun(studentName); } } } // 江苏卷的类 @JiangsuTestPaperDecorator Class JiangsuCounselor extends Counselor; // 山东卷的类 @ShandongTestPaperDecorator Class ShandongCounselor extends Counselor; // 查询入口,这也是一个代理,接受学生信息,然后再根据学生户籍分配不同的系统去查询 const distributionArea = (studentInfo) => { switch (studentInfo.census) { case 'jiangsu': JiangsuCounselor.distributionSchool(studentInfo); // 江苏卷查询系统 break; case 'shandong': ShandongCounselor.distributionSchool(studentInfo); break; default: Counselor.distributionSchool(studentInfo); // 全国卷查询系统 } } // 来自江苏的小明 const xiaoming = new Student({ name: 'xiaoming', fraction: 91, // 分数 census: jiangsu // 户籍 }); // 来自山东的小红 const xiaohong = new Student({ name: 'xiaohong', fraction: 91, census: shandong }); distributionArea(xiaoming); // xiaoming 去哈佛大学 distributionArea(xiaohong); // xiaohong 去清华大学这个例子用了两个代理,一个是 distributionArea,用于根据不同地区的学生分配对应的查询系统,一个是各个查询系统(Counselor),用于根据其不同的分数线调用教育局的录取证书。
用到了给 Counselor 添加 scoreLineMap 的修饰器,将全国卷的查询系统修饰为地区级别的查询系统9. 享元模式 (Flyweight Pattern)
结构型模式的一种,主要用于减少创建对象的数量,以减少内存占用和提高性能。通过减少对象数量从而改善应用所需的对象结构的方式。
- type: 结构型模式
- 适用场景:
- 有大量的相似结构的对象
- 这些对象的部分状态可以放在外面
- 优点:
- 减少了对象的创建,减少内存占用,提高效率。
- 示例:
作为前端我首先想到的大量的、重复对象的场景是 UI 列表,比如说,我要创建一个学生列表,这个列表里有三种学生,文科生、理科生、艺术生。
创建一个 Student 类,每一种学生都可以共享其内部状态 -> 学生类型的映射 typeMap, 在render时,由外部传入学生名字,生成不同的学生实例。// 学生享元类,可能被用于重复创建学生对象 class Student extends React.Component { constructor(props) { this.typeMap = { culture: '文科生', science: '理科生', art: '艺术生' }; this.type = this.props.type; } render(name) { this.studentType = typeMap[this.type] return ( <li>{name} 是 {studentType}</li> ); } } // 列表容器,学生将在这里展示,接受一个学生列表 class StudentList extends React.Component { constructor(props) { this.state = { studentInstance: [], // 存放学生实例 }; } conponentDidMount() { this.createStudentFactory(); } // 这是一个创建学生的 工厂 createStudentFactory() { const studentList = this.props.studentList; const studentInstance = []; // 将学生按照文理科进行划分的map // 如果已经创建过该学生类型的话,就复用里面的。否则新创建对象 const studentMap = {}; studentList.forEach((item) => { const studentType = studentMap[item.type]; if (studentType) { studentInstance.push(studentType.render(item.name)); } else { studentType = new Student(item); studentInstance.push(studentType.render(item.name)); } }) this.setState({studentInstance}); } render() { return ( <ul> {this.state.studentInstance} </ul> ) } } // 三个学生的列表 const studentList = [ {name: xiaogang, type: science}, {name: xiaowu, type: science}, {name: xiaohong, type: art} ]; ReactDOM.render( <StudentList studentList={studentList} />, document.querySelector('body') );上面的列表中,虽然展示了三个学生,[小刚,小吴,小红],但是其实只创建了两个学生对象,因为小刚跟小吴都是理科生, 在创建学生的时候复用了该类型的对象。
-
享元模式 配合 代理模式
开学的时候公布录取结果,有个复杂的对象为学校,其内部有名字,类型等信息,还可以创建学生,因为他足够复杂,且大多雷同。所以想把它做成一个享元类以尽可能的复用,同时,在创建学校的工厂中添加代理用来添加学生。
// 学校享元类 class School { constructor(props) { this.name = props.schoolName; this.studentList = []; } // 创建学生,加入本校学生列表并log欢迎语 createStudent(studentInfo) { this.studentList.push(studentInfo); console.log(`欢迎${studentInfo.name}`同学加入${this.name}); } } // 创建学校的工厂,主要功能为创建学校,如果创建过就记录下来,没有则创建 // 而且,其中代理了学校的 createStudent 方法,将学生加入学校 const addStudentToSchool = (() => { const schoolMap = {}; return (studentInfo) => { const schoolName = studentInfo.admissionSchool; const joinSchool = schoolMap[schoolName]; if (joinSchool) { // 代理学校的 createStudent 方法将学生加入学校 joinSchool.createStudent(studentInfo); } else { schoolMap[schoolName] = newSchool({ schoolName }); schoolMap[schoolName].createStudent(studentInfo); } } }()) const xiaokai = { name: '小凯', admissionSchool: '哈佛大学', }; addStudentToSchool(xiaokai); // -> 欢迎小凯同学加入哈佛大学 const xiaohong = { name: '小红', admissionSchool: '济南大学', }; addStudentToSchool(xiaohong); // -> 欢迎小红同学加入济南大学上面的例子中,School 是一个享元类,其共享相同大学的信息,无论有多少个不同的学生,只要他们的录取学校是相同的,就只会生成一个 school 对象。
10. 桥接模式 (Bridge Pattern)
结构型模式的一种,目的是将抽象部分与它的实现部分分离,使它们都可以独立地变化。
-
type: 结构型模式
-
优点:
- 将抽象部分与实例部分相分离。
- 有时候,为了实现一个功能,但是这个功能上有多个类的影子,我们可能会一层层继承各个类以获得他们的特性。复用性比较差,而且多继承结构中类的数量可能会比较多,桥接模式是比多继承方案更好的解决方法。
- 提高了抽象与实例的可扩充性,两个维度可以独立扩展,都不需要修改已有的功能。
-
示例:
假如说我们创建学生的时候,可能会有幼儿园、初中、大学等不同类型的学生,他们的行为可能不一样,例如幼儿园早上吃奶粉,中学生中午做操、大学生早上吃煎饼中午上自习。
这个例子跟在修饰器模式中举的例子相似,修饰器的例子中是将各种类型学生的行为做成了修饰器,使用的时候去一层层组装,这里用桥接模式实现这个场景。// 早上 action 类 class MorningAction { constructor(morningModel) { this.foodName = morningModel.foodName; } action() { const actionDesc = `早上吃${this.foodName}`; console.log(actionDesc); } } // 中午 action 类 class NoonAction { constructor(noonModel) { this.actionName = noonModel.actionName; } action() { const actionDesc = `中午${this.actionName}`; console.log(actionDesc); } } // 学生类 class Student { constructor(morningModel, noonModel) { morningModel && this.morningAction = new MorningAction(morningModel); noonModel && this.noonAction = new NoonAction(noonModel); } actionTrack() { this.morningAction && this.morningAction.action(); this.noonAction && this.noonAction.action(); } } // 学前班学生 -> 早上吃奶粉 var preschoolStudent = new Student({foodName: '奶粉'}); // 中学生 -> 中午做操 var middleSchoolStudent = new Student(null, {actionName: '做操'}); // 大学生 -> 早上吃煎饼,中午上自习 var collegeStudents = new Student({foodName: '煎饼'}, {actionName: '上自习'});上面的例子创建了一个 Student 类,在实例的时候根据传入参数决定生成的学生的行为,不传对应的属性则没有该行为。后期随着学生类型的越来越多,如果要扩展 Student 类,添加参数与行为类就可以,不影响之前的实例对象。扩展实例学生时也不会影响 Student 类。
- 桥接模式 结合 享元模式
此时,可以根据传入参数位置与value的不同来创建学生了,但是 Student 每次创建学生都必须去new 一个 Student 对象,我想让相同类型的学生共享一些信息,例如学生类型、行为;
// 早上 action 类 class MorningAction { constructor(type) { this.type = type; } action(name, morningModel) { const actionDesc = `${name}早上${morningModel.courseName}`; console.log(actionDesc); } } // 下午 action 类 class AfternoonAction { constructor(type) { this.type = type; } action(name, afternoonModel) { const actionDesc = `${name}下午${afternoonModel.sportName}`; console.log(actionDesc); } } // 学生享元类 class Student { constructor(type, morningModel, afternoonModel) { this.type = type; this.morningModel = morningModel; this.afternoonModel = afternoonModel; this.morningAction = morningModel && new MorningAction(type); this.afternoonAction = afternoonModel && new AfternoonAction(type); } actionTrack(name) { this.morningAction && this.morningAction.action(name, this.morningModel); this.afternoonAction && this.afternoonAction.action(name, this.afternoonModel); } } // 创建学生的工厂,创建并享元 Student 对象 const studentFactory = (() => { const studentTypeMap = {}; return (studentInfo) => { var studentInstance = studentTypeMap[studentInfo.type]; if (studentInstance) { studentInstance.actionTrack(studentInfo.name); } else { studentTypeMap[studentInfo.type] = new Student({...studentInfo}); studentTypeMap[studentInfo.type].actionTrack(studentInfo.name); } } }()) // 小红早上升国旗 studentFactory({ type: 'primaryStudent', name: '小红', morningModel: { courseName: '升国旗' }, }); // 强东早上概率论,强东下午踢足球 studentFactory({ type: 'collegeStudents', name: '强东', morningModel: { courseName: '概率论' }, afternoonModel: { sportName: '踢足球' } });
- 桥接模式 结合 享元模式
11. 外观模式(Facade Pattern)
结构型模式的一种,外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
-
type: 结构型模式
-
优点:
- 减少系统相互依赖。
- 提高灵活性。
- 在内部可以做一些安全措施。
-
跟代理模式的区别:
- 代理模式是在一个类中去调用另一个对象的方法,当使用代理模式的时候,我们常常在一个代理类中创建一个对象的实例。
- 外观模式是通过外观的包装,使应用程序只能看到外观对象,而不会看到具体的细节对象。外观模式注重的是多个类的集成、统一适配。
-
示例:
例如我们要写一个方法,这个方法的目的是解决各个版本的浏览器对addEventListener的兼容问题。
const addEventListener = (element, e, fn) => { if (element.addEventListener) { element.addEventListener(e, fn, false); } else if( element.attachEvent('on' + e, fn); ) else { element['on' + e] = fn; } } -
外观模式 结合 工厂模式
创建一个 person 类,其有一个获取衣服的方法,在获取衣服时候会自动根据 person 的特征创建 clothes 实例
// 裙子类
class Skirt {
constructor(property) {
console.log('符合女士property的裙子');
}
}
class Jeans {
constructor(property) {
console.log('符合男士property的牛仔裤');
}
}
const createClothes = ((personInfo) => {
let property;
if (type === 'woman') {
property = {
height: personInfo.height,
hair: personInfo.hair,
bust: personInfo.bust,
};
return new Skirt(property)
} else {
property = {
height: personInfo.height,
};
return new Jeans(property)
}
})();
class Person {
constructor(personInfo) {
this.name = personInfo.name;
this.type = personInfo.type;
}
getClothes() {
const clothes = createClothes(this.type);
this.clothes = clothes;
}
}
// 创建一个185身高的男生,并给他搭配合适的牛仔裤
const xiaogang = new Person({name: 'xiaogang', height: 185});
xiaogang.getClothes();
// 创建一个150身高、长发、胸小的女生, 并给她搭配合适的牛仔裤
const xiaohong = new Person({name: 'xiaohong', height: 150, hair: '1m', bust: 'A'});
xiaohong.getClothes();
12. 过滤器模式(Filter Pattern)
这是一种结构型模式,允许开发者使用不同的标准来过滤一组对象,通过逻辑运算以解耦的方式把它们连接起来。
- type: 结构型模式
- 适用场景:
- 例如请求的反向代理,根据不同的请求代理到不同的服务器
- 对数组的过滤,例如Array.filter
示例: 现在有一组 student 对象,现在老师想以各种维度来过滤符合相应条件的学生
// 有一组数据,里面有名字 与 成绩信息
const studentList = [
{name: '张三', score: 80},
{name: '刘能', score: 40},
{name: '刘丽红', score: 99}
];
// 过滤出姓 ‘刘’ 的过滤器
const lastnameFilter = (list) => {
return list.filter((v) => {
return /刘/.test(v.name);
});
}
// 过滤出分数 > 60 的过滤器
const scoreFilter = (list) => {
return list.filter((v) => {
return v.score > 60;
});
}
// 这是一个过滤器组合器,可以将任意多个过滤器组合成一个
const filterCompose = (...arg) => {
const funcs = [...arg];
// 如果只有一个,返回此过滤器
if (funcs.length === 1) {
return funcs[0];
}
return funcs.reduce(function (a, b) {
return function () {
return a(b.apply(undefined, arguments));
};
});
}
// 获取姓 ‘刘’ 的学生
const liuStudents = lastnameFilter(studentList);
// 获取姓刘 && 分数 > 60 的学生
// 生成组合的过得过滤器
const combinFilter = filterCompose(lastnameFilter, scoreFilter);
combinFilter(studentList);
- 过滤器模式 配合 外观模式
上面的例子,使用了 Array.filter 用来过滤一个列表,现在想用一个方法,接受array 与 obiect 两种类型,返回一个 list ,过滤出所有符合删选条件的对象的属性或数组的分量
上面的例子,filter 是一个过滤器,其内部根据传入参数的不同类型分别调用不同的方法进行过滤,外部使用时不需要关心其内部逻辑/** * 过滤出符合条件的对象属性 或 数组分量 * @param 需要过滤的对象或数组 * @param 过滤条件 * @returns [array] */ filter = (obj, cb) => { if (obj instanceof Array) { return obj.filter(cb); } else if (typeof obj === 'object') { const list = []; for (let key in obj) { const objValue = obj[key]; if (cb(objValue)) { list.push[objValue]; } } return list; } else { return obj; } } // 过滤 string 的过滤器 const stringFilter = (item) => { return typeof item === 'string'; } var arr = ['1', 2, '3']; var obj = {a: '1', b: 2, c: '3'}; filter(arr, stringFilter); // ['1', '3'] filter(obj, stringFilter); // ['1', '3']
13. 组合模式(Composite Pattern)
这是一种结构型模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。
- type: 结构型模式
- 优点:
- 将对象组合成树形结构以表示"部分-整体"的层次结构。
- 组合模式使得用户对单个对象和组合对象的使用具有一致性。
- 客户程序可以向处理简单元素一样来处理复杂元素。
- 示例:
假如说我们有一个表单,其表单内有各个控件,当表单提交的时候,我们需要将所有控件都获取到,一个一个校验,这无疑是个重复的体力活。此时,我想用一个容器组件将这些表单控件组合起来,需要校验的时候只需要调用容器组件的 validate 就可以校验所有的控件。
// textarea 控件
class Textarea {
render() {
return (
<textarea ref="inputRef" />
);
}
validate() {
const value = this.refs.inputRef.value;
return value.length > 5;
}
}
// checkbox 控件
class Checkbox {
render() {
return (
<checkbox ref="inputRef" />
);
}
validate() {
const value = this.refs.inputRef.value;
return value === 1;
}
}
// 容器组件
class composeForm extend React.Component {
render() {
return (
<div>
<Checkbox ref="CheckboxRef" />
<Textarea ref="TextareaRef" />
</div>
);
}
validate() {
const refs = this.refs;
for (key in refs) {
refs[key].validate();
}
}
}
所有的子控件可以包在 composeForm 中,包括 composeForm 组件自己。每一个表单控件都有一个 validate 方法用于校验,所有的子控件被组合成了一个树状结构的表单,需要校验的时候,只需要调用顶层的 composeForm 组件的 validate 方法,即可以调动所有子控件的 validate 方法进行校验。实现了用户对单个对象和组合对象的使用具有一致性,客户程序可以向处理简单元素一样来处理复杂元素。
这个例子还可以升级改造,现在 composeForm 内的子组件是死的,composeForm 的作用也并不纯粹,可以使 composeForm 更加纯粹仅作为一个容器存在,把内部所有的 dome 结构抽象出来,使用时作为配置传进去。
- 组合模式 结合 工厂方法模式
上面的例子中,composeForm 组件是作为多个表单控件的组合容器,但现在他是不纯粹的,里面是固定的表单内容。我想把它抽象一下,由外部传入表单的配置文件,然后 composeForm 内部根据配置文件动态的去创建
控件实例并进行组合。
// 表单控件的工厂类,里面有各种控件的创建方法
class InputengFactory {
text() {
return new TextInput();
}
textArea() {
return new Textarea();
}
select() {
return new Select();
}
// ......
}
/**
* 表单控件的容器,在其内部创建相应的控件实例
*/
class FormItem extend React.Component {
constructor(props) {
super(props);
}
render() {
return this.instanceInput();
}
/**
* 实例化表单控件的方法
* 创建控件工厂并实例化控件
*/
instanceInput() {
const option = this.props.option;
const inputengFactory = new InputengFactory();
const instanceInput = InputengFactory[option.type](option);
this.element = instanceInput;
return instanceInput;
}
// 校验控件
validate() {
this.element && this.element.validate();
}
}
/**
* compose 组件,接受 model 参数,是一个配置列表,并进行组合
* @props {Array} [{type: 'text', label: '标签', value: 'string', validate: fnc}]
*/
class composeForm extend React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
{this.renderFormList()}
</div>
);
}
renderFormList() {
const model = this.props.model || [];
return model.map((item) => {
<FormItem option={item} ref={item.name}/>
});
}
validate() {
const refs = this.refs;
for (key in refs) {
refs[key].validate();
}
}
}
这里例子中更有四种角色,
- composeForm 组合组件,作用是接受配置文件并进行单元组合
- FormItem 表单控件的容器组件,是 composeForm 组合中的单元,在其内部进行创建控件
- InputengFactory 控件工厂类,里面有各种控件的创建方法
- TextInput、Select 等控件产品类
14. 观察者模式 (Observer Pattern)
观察者模式(发布者-订阅者模式),其实就是字面意义上的发布与订阅的一种关系模式,订阅一个约定好的时间,发布的时候通知订阅者。
-
type: 结构型模式
-
适用场景:
- 可用于两个模块间的通信
- 其实对于javascript这种事件驱动的语言来说说,观察者模式使用非常广泛,例如通过 addEventListener 去监听某些事件,就是在订阅事件的触发时机。
- 再例如所熟悉的 Angular 中的事件传递系统,
$emit、$on, 再例如Redux的 subscribe 也是观察者模式。
-
示例:
简单实现 Angular 的事件传递, 生成一个
$scope实例,其有两个属性$emit与$on,在注册监听$on的时候,会返回一个函数,用来移除监听。
const Scope = () => {
let $emit, on;
const eventMap = {}; // 存储事件的容器
// return 一个函数,用以移除相应的监听
const $del = (key) => {
return () => {
delete eventMap[key];
}
};
// 订阅者
$on = (key, callback) => {
// 如果已经注册过对此时间的监听,排除一个错误
if (eventMap[key]) {
throw (`已注册对 ${key} 的监听`);
} else {
// 将监听函数加入事件模型
eventMap[key] = callback;
return $del(key);
}
};
// 发布者,发布时将参数传入相应的监听函数并执行。
$emit = (key, option) => {
const eventCallback = eventMap[key];
eventCallback && eventCallback(option);
};
return {
$emit,
$on,
};
}
// 实例化一个 $scope
var $scope = Scope();
// 订阅 getUp 事件
var delGetUp = $scope.$on('getUp', (say) => {
console.log('Hi boy,' + say);
});
// 发布 getUp 事件, 得到控制台log:Hi boy, Good morning
$scope.$emit('getUp', 'Good morning');
// 移除掉对 getUp 的监听
delGetUp();
/** 也可以把 $scope 做成一个单例的,免得创建不同的 $scope 事件订阅机制被扰乱 **/
// 使用的时候用 createScope() 创建 $scope,只会被创建一次,再创建会返回之前的 $scope
const createScope = ( _ => {
let $scope;
return () => {
return $scope || ($scope = scope());
};
})();
15. 责任链模式 (Chain of Responsibility Pattern)
责任链模式为请求创建了一个接收者对象的链。每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。
- type: 行为型模式
- 优点:
- 避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。
- 适用场景:
- 事件的冒泡与捕获
- 针对不同角色的同一行为的分层处理
- 实例:
有这么一个场景,比如说,在浏览器上点击了一个按钮,我想要获取到这个按钮上级元素的有我想要的类名或者其他属性的上级元素。
/**
* 获取某个元素的上级元素中,符合搜索条件的,离他最近的,元素的某个属性值
* @params ele 起点元素
* @params filter 过滤类型。如class、tagName等
* @params value 过滤类型的过滤条件
* @returns element
*/
const getDomContent = (ele, filter, value) => {
let resElement = resElement;
if(filter === 'class') {
resElement = getClassElement(ele, value);
} else if (filter === 'tagName') {
resElement = getTagElement(ele, value)
}
return resElement;
}
// 不断向上寻找符合 class 条件的DOM
const getClassElement(ele, value) {
let parentElement = ele.parentElement;
if (parentElement.classList.indexOf(value) !== -1) {
return parentElement;
} else if(parentElement.tagName !== null) {
classElementFilter(ele, value);
}
throw('未找到符合该类名的上级元素');
}
// 不断向上寻找符合 tagName 条件的DOM
const getTagElement = (ele, value) => {
let parentElement = ele.parentElement;
if (parentElement.tagName !== value) {
return parentElement;
} else if (parentElement.tagName !== null) {
classElementFilter(ele, value);
}
throw('未找到符合该标签的上级元素');
}
// 使用时, 获取 button 所离他最近的类名为container的父元素
const ele = document.querySelector('button');
const whantDom = getDomContent(ele, 'class', 'container');
- 责任链模式 配合 过滤器模式
有这么一个场景,我想要在页面上给所有类名为 ‘item’ 的 div 元素添加
16. 迭代器模式(Iterator Pattern)
这种模式用于顺序访问集合对象的元素,不需要知道集合对象的底层表示。
- type: 行为型模式
- 示例:
创建一个迭代器类, 并利用来迭代一个对象,只可以被迭代一次
/**
* 迭代器类,接受一个对象并生成一个迭代器
* hasNext 属性,用来判定还有没有下一个可迭代属性
* next 获取下一个属性
*/
class Iterator {
constructor(iteratorObj) {
this.iteratorObj = iteratorObj;
this.index = 0;
this.iteratorKeys = Object.keys(iteratorObj);
}
hasNext() {
return this.index < this.iteratorKeys.length;
}
next() {
if(this.hasNext()){
const nextKey = this.iteratorKeys[this.index++];
return this.iteratorObj[nextKey];
}
return null;
}
}
const obj = {a: 1, b: 2, c: 3};
// 生成一个迭代器
const objIterator = new Iterator(obj);
// 迭代
while(objIterator.hasNext()) {
console.log(objIterator.next()); // => 1, 2, 3, 4
}
- ES6 中的Iterator
ES6 中可迭代对象是包含
Symbol.iterator属性的对象,这个Symbol.iterator属性对应着能够返回该对象的迭代器的函数。在ES6中,所有的集合对象(数组、Set和Map)以及字符串都是可迭代对象,因此它们都被指定了默认的迭代器。可迭代对象都可以与ES6中新增的for-of循环配合使用。