楔子
👣 先立定一下:可以从这篇文章获得什么
- 这篇是一个楔子,不是百科全书;因为网络中对于设计模式的介绍非常普遍,没必要重复造轮子;
- 但,什么驱动我们需要去理解设计模式,以及找到百科全书之后,如何实践:提供小小的例子,以产生大大的灵光;
🤔 再解构一下主题:设计模式是什么
在面向对象软件设计过程中,针对特定问题的简洁而优雅的解决方案;
- 面向对象软件设计过程中,如何理解?
- 面向对象编程基本概念 - 学习 Web 开发 | MDN
- 继承(extend)与封装(encapsulated),多态(polymorphism)和抽象(abstract)
- JavaScript 的 OOP?
- 面向对象编程基本概念 - 学习 Web 开发 | MDN
- 简洁和优雅是指
- 增加 xx 可复用性以及维护性
- 误区:减少代码行数
- 引出标准:找出程序中变化的地方,并将变化封装起来
e.g. 设计模式和知识库一致
- 设计模式,针对通用范围特定问题
- 知识库,针对特定业务场景的问题
🎯 设计模式的适用性(使用的范围)
简单不需要设计模式,我们是为了增加项目的维护性和可复用性;
💦 看起来,JavaScript 的设计模式可以理解为...
- 在业务迭代需求开发中(前提)
- 为了增强项目的维护性和可复用性(why)
- 针对特定的逻辑场景总结出的一套实用风格解决方案(how)
设计模型(what)
💦 如何阅读这一章节?
分辨模式的关键是意图而不是结构
原型模式
用于创建对象,区别于传统使用类型来创建对象的方式;
- 不关注对象具体类型:找到一个对象,然后通过克隆来创建一个一模一样的对象;
- 问题:必须新建一个属于相同类的对象。然后,你必须遍历原始对象的所有成员变量,并将成员变量值复制到新对象中。
- 实现
使用创建对象模式:以存在分身技能的飞机为例子;
var Plane = function() {
this.blood = 100;
}
var plane = new Plane();
plane.attack = oldplane.attack;
// 原型模式创建分身
var clonePlane = Object.create(plane);
Object.create = function(obj) {
var F = function(){};
F.prototype = obj;
return new F();
}
- JavaScript 使用原型模式作为编程范式
- 所有数据都是对象:js 中大部分数据都是对象
- 基本类型 + 对象类型
- 但基本类型也可以通过包装类的形式存在
- 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型并克隆
- JavaScript 中的
new关键词背后的实质是:调用函数构造器 - 先克隆 Object.prototype,再进行额外操作
- JavaScript 中的
- 对象会记住它的原型
- proto 属性
- 如果对象无法响应某个请求,会把请求委托给原型
- 原型层级:如果对象上不存在 xx 成员,去它的
__proto__原型对象去找;
- 原型层级:如果对象上不存在 xx 成员,去它的
💡
复习一下
单例模式
- 保证一个类只有一个实例,并提供一个访问它的全局访问点;
- 解决什么问题呢:
- 保证一个类只有一个实例。为什么会有人想要控制一个类所拥有的实例数量?最常见的原因是控制某些共享资源(例如数据库或文件)的访问权限
- 为该实例提供一个全局访问节点。存储重要对象的全局变量吗?它们在使用上十分方便,但同时也非常不安全。
window.market,pageRecordData
- 实现
- 将默认构造函数设为私有,防止其他对象使用单例类的
new运算符。 - 新建一个静态构建方法作为构造函数。该函数会“偷偷”调用私有构造函数来创建对象,并将其保存在一个静态成员变量中。此后所有对于该函数的调用都将返回这一缓存对象。
var Singleton = function(name) {
this.name = name;
this.instance = null;
}
Singleton.prototype.getName = function() {
alert(this.name);
}
Singleton.getInstance = function(name) {
if (!this.instance) this.instance = new Singleton(name);
return this.instance;
}
// a === b --> true
var a = Singleton.getInstance('eva1');
var b = Singleton.getInstance('eva2');
这是一个 mvp,存在小问题:都需要使用 Singleton.getInstance 才能使用单例;
变得更加自然 👇
const CreateDiv = (function() {
let instance = null;
const CreateDiv = function(html) {
if (instance) {
return instance;
}
this.html = html;
this.init();
return instance = this;
}
CreateDiv.prototype.init = function() {
const div = document.createElement('div');
div.innerHTML = this.html;
document.body.append(div);
}
})()
// a === b --> true
var a = new CreateDiv('sven1');
var b = new CreateDiv('sven2');
- 单例模式在业务中有什么用呢?
- 频繁实例化然后又销毁的对象,使用单例模式可以提高性能
- (全局状态管理)项目获取到请求实例,这样不需要每次都会创建一个新的 axios 配置实例
// base.js
var instance = null;
export {
getInstance: function() {
if (instance) instance = new Axios();
return instance;
}
}
// req.js
function get() {
return Axios.getInstance().get();
}
-
获取到 market.getDeviceInfo() 设备信息;
// 返回设备信息 @javascriptInterface function getDeviceInfo() { DeviceInfo deviceInfo = new DeviceInfo(); return deviceInfo; }let ajaxData; function getDeviceInfo() { if (!ajaxData) { ajaxData = market.getDeviceInfo(); } return ajaxData; }
策略模式
- 定义:定义一系列的算法,把他们一个个封装起来,并且使他们可以相互替换。
- 适用范围:在程序中,一个功能可能有多种方案可以选择:一个压缩文件程序,可以选择 zip 算法,也可以选择 gzip 算法;
-
又比如,【一体两面】小游戏下载提效方案;
-
又比如,一个样式是使用什么 css 特性来实现(兼容性);
-
- 实现
var calculateBonus = function(level, salary) {
if (level === 's') {
return salary * year * 4;
}
if (level === 'a') {
return salary * 3;
}
if (level === 'b') {
return salary * 2;
}
if (level === 'c') {
return salary * -1;
}
if (level = 'd') {
}
}
- 如果后面我们想要复用这些其中算法该怎么办呢?
- 而且逻辑也很多,分支很乱;不清晰
// 单独解耦算法,清晰
var strategies = {
"s": v => 4 * v, // 可复用,单独抽出
"a": v => 3 * v,
"b": v => 2 * v,
"c": (v, n) => (n + 1) * v
}
var calculateBouns = function(level, salary) {
return strategies[level](salary);
}
- 在业务中
-
一个简单场景,postcss 编译逻辑
暂时无法在飞书文档外展示此内容
-
feedbackV1 这个页面存在很多的校验逻辑,里面校验逻辑混杂,可能还有很多副作用
function submit() {
if (!isLogin()) {
goToLogin();
return;
}
// 判空校验 1: 举报理由
if (!data.issueTypes.lvl2Option) {
showTip(feedbackText('confirmIssueTypes'));
return;
}
// 判空校验 2: 问题描述
if (!data.problemDesc) {
showTip(feedbackText('confirmIssueDesc'));
return;
}
// 判空校验 3: 证明材料,长度校验
const issuePicList =checkImgs();
if (!issuePicList || !issuePicList.length) {
showTip(feedbackText('confirmAttachedImg'));
return;
}
// ...
}
使用策略模式该如何去实现校验呢?
function submit() {
validator.add(data.issueTypes.lvl2Option, {
type: 'required'
});
validator.add(data.issuePicList, [
{
type: 'maxlength',
opt: {max: 30, min: 5}
},
{
type: 'required'
}
]);
validator.add(data.tel, [
type: 'isMobile'
])
validator.start();
}
var strategies = {
'required': v => v !== undefined;
'maxlength': (v, { max, min }) => v.length < max && v.length > min;
'isMobile': (v) => /^1[3|5|8][0-9]{9}$/.test(value)
}
var Validator = function() {
this.cache = [];
}
Validator.prototype.add = function(v, rules) {
rules.reduce((p, rule) => {
this.cache.push(function() {
var strategy = rule.type;
return strategies[strategy](v, rule.opt);
})
})
}
Validator.prototype.start = function(v, rule, msg) {
this.cache.reduce((p, c) => c(), {});
}
发布订阅模式
又叫观察者模式,定义对象间的一对多的依赖关系:当一个对象发生改变时,所有依赖它的对象都得到通知;
- 实现
e.g. 是不是经典的 document.addEventListener 就是个模式的实践;
- 在业务中有什么用呢
- Vue 的依赖收集就是一种
- Antd 不少实现都是经典的设计模式,以
antd.grid为例子:如何实现设备断点发生变化的时候触发钩子逻辑呢 https://github1s.com/ant-design/ant-design/blob/master/components/_util/responsiveObserver.ts#L64 - 场景描述:antd 定义了一批有语义的断点 xs,sm,md,想要在这些断点生效的时候触发事件
- 注册 subscribe
- 触发监听器,通过 matchMedia 实现
- 在 dispatch 中执行回调逻辑
subscribe(func: SubscribeFunc): number {
if (!subscribers.size) this.register();
subUid += 1;
subscribers.set(subUid, func);
func(screens);
return subUid;
}
register() {
Object.keys(responsiveMap).forEach((screen:Breakpoint) => {
const matchMediaQuery = responsiveMap[screen];
constlistener = ({ matches }: { matches: boolean }) => {
this.dispatch({
...screens,
[screen]: matches,
});
};
const mql = window.matchMedia(matchMediaQuery);
mql.addListener(listener);
this.matchHandlers[matchMediaQuery] = {
mql,
listener,
};
listener(mql);
});
}
const getResponsiveMap = (token: GlobalToken): BreakpointMap => ({
xs: `(max-width: ${token.screenXSMax}px)`,
sm: `(min-width: ${token.screenSM}px)`,
md: `(min-width: ${token.screenMD}px)`,
lg: `(min-width: ${token.screenLG}px)`,
xl: `(min-width: ${token.screenXL}px)`,
xxl: `(min-width: ${token.screenXXL}px)`,
});
const getResponsiveMap = (osversion: number, deviceTYpe: fold | pad) => {
if (pad) {
return (
xs: xxxx
sm: ssss
freeform: (medai)
)
}
}
适配器模式(包装器)
- 适配器模式的作用是解决两个软件实体之间接口不兼容的问题
- 实现
- 我想写一个渲染地图的方法,renderMap
- Google map 和 baidumap 都提供了地图展示的方法,不过,前者 google map 的显示逻辑叫做 show,后者叫做 put
- 我需要给 google map 写一个 renderGoogleMap,还要写一个 renderBaiduMap?
- 地图的功能在不断扩展,里面各种判断 google map 和 baidu map 的方法,很麻烦
var renderMap = function(map) {
if (typeof map === 'googlemap') {
map.show();
}
if (typeof map === 'baidumap') {
map.put();
}
}
varbaiduMapAdapter = {
show: import('baidum').then(baidumap => baidumap.put()) /* xxx */
}
var renderMap = function(map) {
map.show && map.show();
}
renderMap()
-
在业务中有什么用呢
-
appstore-mobile 经常存在一个组件因为和第一次写的业务场景耦合太深,没法把 ui 交互逻辑在别的地方使用的情况
- 可以分离开来,一个是 ui 组件,然后在业务场景中加一个适配器
// xx ui 组件 interface uiprops { cover: ImgUrl; title: string; } // adapter 适配器逻辑 A interface AdapterAProps { comsData: { title: string; cover: string; }; keyName: string; } const uiprops = computed(() => ({ title: props.comsdata.title, cover: props.comsdata.cover, // ... })) // adapter 适配器逻辑 B,这个场景放在应用号场景,所以信息都在 assembleInfo 中 interface AdapterBProps { comsData: { assembleInfo: { title: string; cover: string; } }; keyName: string; } const uiprops = computed(() => ({ title: props.comsdata.assembleInfo.title, cover: props.comsdata.assembleInfo.cover, // ... }))