前提: 发现自己发起项目合并请求时,常常被打回来要求修改,总结原因还是低质量代码太多,明明写代码之前也思考过,只是实现了需求,但是思考的程度太表面,现在打算总结代码的设计模式,为写出高质量代码而加油
高质量代码的要求:
1、代码语义化:变量名和函数名都有意义,最好代码既注释
2、减少重复代码:代码的抽象度高,代码的复用性提高
3、模块的功能单一:一个模块专注于一个功能,当我们需要做大功能时,就可以将多个模块组合起来,复用性也就增加
4、代码扩展性高:代码耦合性低,扩展度高,局部代码的修改不会引入大规模的改动,可以方便的引入新功能和新模块
5、封装代码性能强:高内聚,低耦合,内部变量不会污染外部,可以作为一个模块给外部调用,外部调用者不需要知道
内部实现的细节,只需要按照约定的规范使用即可,对扩展开放,对修改关闭(开闭原则),外部不能修改模块内部,内部
只需要留出扩展接口,模块内部也保证了自己的正确性
1. 策略/状态模式
**策略模式基本结构:**减少了if…else的数量,提升了代码的可读性,通过传入一个状态来选取具体的策略
假如我们需要做一个计算器,需要支持加减乘除,为了判断用户具体需要进行哪个操作,我们需要4个if…else来进行判断,如果支持更多操作,那if…else会更长,不利于阅读,看着也不优雅。所以我们可以用策略模式优化如下:
function calculator(type, a, b) {
const strategy = {
add: function(a, b) {
return a + b;
},
minus: function(a, b) {
return a - b;
},
division: function(a, b) {
return a / b;
},
times: function(a, b) {
return a * b;
}
}
return strategy[type](a, b);
}
// 使用时
calculator('add', 1, 1);
状态模式基本架构:
状态模式和策略模式很像,也是有一个对象存储一些策略,但是还有一个变量来存储当前的状态,我们根据当前状态来获取具体的操作:
function stateFactor(state) {
const stateObj = {
status: '',
state: {
state1: function(){},
state2: function(){},
},
run: function() {
return this.state[this.status];
}
}
stateObj.status = state;
return stateObj;
}
// 使用时
stateFactor('state1').run();
2、外观模式(常用)
**外观模式基本结构:**将一个个小模块封装成更高级的接口内部,传给外部一个简单的接口
当我们设计一个模块时,里面的方法可以会设计得比较细,但是暴露给外面使用的时候,不一定非得直接暴露这些细小的接口,外部使用者需要的可能是组合部分接口来实现某个功能,我们暴露的时候其实就可以将这个组织好。
function model1() {}
function model2() {}
// 可以提供一个更高阶的接口,组合好了model1和model2给外部使用
function use() {
model2(model1());
}
实例:常见的接口封装
外观模式说起来其实非常常见,很多模块内部都很复杂,但是对外的接口可能都是一两个,我们无需知道复杂的内部细节,只需要调用统一的高级接口就行,比如下面的选项卡模块:
// 一个选项卡类,他内部可能有多个子模块
function Tab() {}
Tab.prototype.renderHTML = function() {} // 渲染页面的子模块
Tab.prototype.bindEvent = function() {} // 绑定事件的子模块
Tab.prototype.loadCss = function() {} // 加载样式的子模块
// 对外不需要暴露上面那些具体的子模块,只需要一个高级接口就行
Tab.prototype.init = function(config) {
this.loadCss();
this.renderHTML();
this.bindEvent();
}
3、迭代器模式
**迭代器模式基本结构:**当后端传来大量结构相似的数据时,js的数组并不能很好的处理这些数据时,我们可以自己按照需求封装迭代器功能
迭代器模式模式在JS里面很常见了,数组自带的forEach就是迭代器模式的一个应用,我们也可以实现一个类似的功能:
function Iterator(items) {
this.items = items;
}
Iterator.prototype.dealEach = function(fn) {
for(let i = 0; i < this.items.length; i++) {
fn(this.items[i], i);
}
}
4、备忘录模式
**备忘录模式基本结构:**加一个缓存对象,来记录之前获取过的数据或者操作的状态,后面可以用来加快访问速度或者进行状态回滚
备忘录模式类似于JS经常使用的缓存函数,内部记录一个状态,也就是缓存,当我们再次访问的时候可以直接拿缓存数据,可以用其实现操作的前进后退功能:
function memo() {
const cache = {};
return function(arg) {
if(cache[arg]) {
return cache[arg];
} else {
// 没缓存的时候先执行方法,得到结果res
// 然后将res写入缓存
cache[arg] = res;
return res;
}
}
5、桥接模式
桥接模式人如其名,其实就相当于一个桥梁,把不同维度的变量桥接在一起来实现功能。假设我们需要实现三种形状(长方形,圆形,三角形),每种形状有三种颜色(红色,绿色,蓝色),这个需求有两个方案,一个方案写九个方法,每个方法实现一个图形:
function redRectangle() {}
function greenRectangle() {}
function blueRectangle() {}
function redCircle() {}
function greenCircle() {}
function blueCircle() {}
function redTriangle() {}
function greenTriangle() {}
function blueTriangle() {}
使用桥接模式后,我们可以观察重复代码拆成多个维度,再将这些维度拼接起来
function rectangle(color) { // 长方形
showColor(color);
}
function circle(color) { // 圆形
showColor(color);
}
function triangle(color) { // 三角形
showColor(color);
}
function showColor(color) { // 显示颜色的方法
}
// 使用时,需要一个红色的圆形
let obj = new circle('red');
**6、享元模式:**当我们观察到代码中有大量相似的代码块,他们做的事情可能都是一样的,只是每次应用的对象不一样,我们就可以考虑用享元模式。现在假设我们有一个需求是显示多个弹窗,每个弹窗的文字和大小不同:
// 已经有一个弹窗类了
function Popup() {}
// 弹窗类有一个显示的方法
Popup.prototype.show = function() {}
如果我们不用享元模式,一个一个弹就是这样:
var popup1 = new Popup();
popup1.show();
var popup2 = new Popup();
popup2.show();
使用享元模式后:
var popupArr = [
{text: 'popup 1', width: 200, height: 400},
{text: 'popup 2', width: 300, height: 300},
]
var popup = new Popup();
for(var i = 0; i < popupArr.length; i++) {
popup.show(popupArr[i]); // 注意show方法需要接收参数
}
简单来说享元模式在开发中使用比较常见,我们把不同实例中重复使用的方法提取出来,只需要传入不同的参数进去,就可以用同样的方法实现不同的功能
**7、模板方法模式:**模板方法模式其实类似于继承,就是我们先定义一个通用的模板骨架,然后后面在这个基础上继续扩展。我们通过一个需求来看下他的基本结构,假设我们现在需要实现一个导航组件,但是这个导航类型还比较多,有的带消息提示,有的是横着的,有的是竖着的,而且后面还可能会新增类型:
// 先建一个基础的类
function baseNav() {
}
baseNav.prototype.action = function(callback){} //接收一个回调进行特异性处理
上述代码我们先建了一个基础的类,里面只有最基本的属性和方法,其实就相当于一个模板,而且在具体的方法里面还可以接收回调,这样后面派生出来的类可以根据自己的需求传入回调
实例:弹窗
还是之前用过的弹窗例子,我们要做一个大小文字可能不同的弹窗组件,只是这次我们的弹窗还有取消和确定两个按钮,这两个按钮在不同场景下可能有不同的行为,比如发起请求什么的。但是他们也有一个共同的操作,就是点击这两个按钮后弹窗都会消失,这样我们就可以把共同的部分先写出来,作为一个模板:
function basePopup(word, size) {
this.word = word;
this.size = size;
this.dom = null;
}
basePopup.prototype.init = function() {
// 初始化DOM元素
var div = document.createElement('div');
div.innerHTML = this.word;
div.style.width = this.size.width;
div.style.height = this.size.height;
this.dom = div;
}
// 取消的方法
basePopup.prototype.cancel = function() {
this.dom.style.display = 'none';
}
// 确认的方法
basePopup.prototype.confirm = function() {
this.dom.style.display = 'none';
}
**8、工厂模式:**封装的模块就像一个工厂一样批量的产出需要的对象。常见工厂模式的一个特征就是调用的时候不需要使用new,而且传入的参数比较简单。但是调用次数可能比较频繁,经常需要产出不同的对象,频繁调用时不用new也方便很多。一个工厂模式的代码结构如下所示:
function factory(type) {
switch(type) {
case 'type1':
return new Type1();
case 'type2':
return new Type2();
case 'type3':
return new Type3();
}
}
// 我们传入了type,然后工厂根据不同的type来创建不同的对象
实例: 弹窗组件
我们项目需要一个弹窗,弹窗有几种:消息型弹窗,确认型弹窗,取消型弹窗,他们的颜色和内容可能是不一样的,工厂模式改造:
// 新加一个方法popup把这几个类都包装起来
function popup(type, content, color) {
switch(type) {
case 'infoPopup':
return new infoPopup(content, color);
case 'confirmPopup':
return new confirmPopup(content, color);
case 'cancelPopup':
return new cancelPopup(content, color);
}
}
// 调用方法
let infoPopup1 = popup('infoPopup', content, color);
改造成面向对象
上述代码虽然实现了工厂模式,但是switch始终感觉不是很优雅。我们使用面向对象改造下popup,将它改为一个类,将不同类型的弹窗挂载在这个类上成为工厂方法:
function popup(type, content, color) {
// 如果是通过new调用的,返回对应类型的弹窗
if(this instanceof popup) {
return new this[type](content, color);
} else {
// 如果不是new调用的,使用new调用,会走到上面那行代码
return new popup(type, content, color);
}
}
// 各种类型的弹窗全部挂载在原型上成为实例方法
popup.prototype.infoPopup = function(content, color) {}
popup.prototype.confirmPopup = function(content, color) {}
popup.prototype.cancelPopup = function(content, color) {}
9、建造者模式
建造者模式是用于比较复杂的大对象的构建,比如Vue,Vue内部包含一个功能强大,逻辑复杂的对象,在构建的时候也需要传很多参数进去。像这种需要创建的情况不多,创建的对象本身又很复杂的时候就适用建造者模式。建造者模式的一般结构如下:
function Model1() {} // 模块1
function Model2() {} // 模块2
// 最终使用的类
function Final() {
this.model1 = new Model1();
this.model2 = new Model2();
}
// 使用时
var obj = new Final();
// 上述代码中我们最终使用的是Final,但是Final里面的结构比较复杂,有很多个子模块,
// Final就是将这些子模块组合起来完成功能,这种需要精细化构造的就适用于建造者模式。