代码质量 与 设计模式应用
一、代码指标
1、健壮性
什么意思呢? 百度:在异常和危险情况下系统生存的能力。
用人的健壮性做个比方:如果一个人很健壮,那一般的小病小灾打不倒他,如感冒、咳嗽
对比过来,一个程序很健壮,那在出现预期之外的错误(eg:文件不存在),减少找bug的难度和影响程度。
健壮性好的代码有哪些特点
减少找bug的难度
很多同学可能有这么一个问题:发现自己每天写代码 2小时,改bug 6小时, 一天8个小时就这么过去了
降低对程序的影响程度
如果程序出现了问题,不至于这一个问题让我们整个系统跑不起来了
保证健壮性的手段
健壮性不同其他性能,没有多抽象的概念,也没有多深的思维,主要就是个人的习惯
1、参数层面
- 基础类型判断
先来看下面一个例子:
这就是典型的参数类型引起的问题1
2
3
4
5
6
7
8
9
10
11
12function add(a, b){
return a + b;
}
// 正常入参 没什么问题
add(1,2);
// 少传一个参数时
add(1) // ==> NaN 这里是不会报错的哦!
// 这时如果这个返回值是通过 axios.get()传到后台会是什么情况?
axios.get('url',{ num: add(1) }) // ==> 400 bad request
// 试想:如果你的代码量比较大时,要定位到这个问题是不是要花很长时间,还要debug之类的
[如何解决?]1
2
3
4
5
6
7
8function add(a, b){
// 1、基础参数类型判断
if(typeof a === 'number' && typeof b === 'number'){
return a + b;
} else {
throw new Error('a or b is not a number')
}
} - 参数是配置对象 选项合并[看Vue是怎么做的?]
1
2
3
4// 以Vue为例:
new Vue({});
// 有哪些必传参数?
// 如何保证这些必传参数都传了?1
2
3
4
5
6
7
8
9
10
11// 必传参数: el template
// 如何保证必传值不为空: 提供一个默认配置,把new时的入参与默认配置做合并
function Vue(config){
let _default = {
el: document,
template:'<div></div>'
}
for(let key in config){
_default[key] = config[key] || _default[key];
}
} - 参数是某个类的实例
这种情况,一般是我们在造轮子的时候会用到
[看这种情况怎么保证健壮性]1
2
3
4
5
6
7
8
9// Class1 是一个类
function Class1() {
}
// 这里有个方法fn 参数要求是Class1的实例
function fn(obj){
}1
2
3
4
5
6
7
8
9
10
11
12// Class1 是一个类
function Class1(){
}
// 这里有个方法fn 参数要求是Class1的实例
function fn(obj){
if(obj instanceof Class1){
// dosomthing
} else {
throw new Error('obj 必须是 Class1的实例')
}
}
这种情况在实际开发中可能用到的比较少,
但在自己造轮子的时候,这种情况就会非常多
js中特有的一个问题
1
2// function 除了可能是一个方法,还可能代表一个什么东东?
function a(){}[如何保证?]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function vue(){
if(this instanceof vue){ // 通过instanceof vue 判断this 是不是vue实例
// 类实例
} else {
// 当作一个方法调用
// return new vue(); // 这样即使当一个方法调用 ,拿到的还是一个vue的实例
// 像一些框架之类的都会有这种措施 vue是这样
throw new Error('vue is a constructor and should be called with the `new` keyword ')
}
}
vue(); // 直接调用时,this ---> window
new vue(); // new 操作符调用时, this 指向vue实例 通过instanceof vue 可以判断
易错代码
代码错了就是错了,什么叫易错代码?
不由自己控制的出错代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14// 例如: 前端请求某个接口,接口返回的数据结构
// 预期结构是下面这样的:
let res = {
list:[
{
attrA: 1
}
]
}
// 是这样取值的
let attrA = res.list[0].attrA;
// 如果 list 为空或[] 时,我们取值就会报错 Cannot read property 'attrA' of undefined[怎么解决呢?]
一般有两种方法:- 模拟一个这样的数据结构
1
2let item = (res.list || [])[0];
let attrA = (item || {}).attrA; - 用&&操作符 先判断为空的情况
1
2let item = res.list && res.list[0] // 使用与操作符 判断为空的情况
let attrA = item && item.attrA;
前端易错性代码出现可能会少些,后端就非常多
1
2
3
4
5
6
7
8
9
10
11
12
13
14// 在下载某个文件时,
// 1、文件存在,正常下载
// 2、文件不存在,就会出错了
// 如果不处理出错,后端服务可能就停了,那这样影响就非常严重了,
// 所以后端经常会看到这样的代码段
try{
// 易错代码
} catch (e){
// 记录日志
}
// 先把出错的信息记录下来,保证程序不会挂掉,
// 再通过日志分析出错原因- 模拟一个这样的数据结构
变量权限 这个就有点抽象了
变量应该不应该被篡改,能不能够被篡改
变量应不应该被读取,能不能够被读取,在哪里能被读取
这就是变量的权限
变量权限跟健壮性有什么关系呢?
1
2
3
4 // 还以Vue为例:
// 在使用vue-router的时候,有没有留意这样一个问题:
this.$router = {}; // 给$router赋值,但 this.$router的值不会变
[vue-router是怎么做的呢?]
1
2
3
4
5
6
7
8
9
10
11 // vue-router install 方法
function install(Vue){
let _router = {}; // 局部变量
// 屏蔽了set方法,使你给$router赋值时无效
Object.defineProperty(Vue.prototype, '$router', {
get: function get () {
// return this._routerRoot._router // 源码是这样的
return _router;
}
});
}
2、可读性
看到变量或方法就知道这个变量,这段代码是做什么用的
语义化
定义有意义的名字: 描述某个变量或方法在业务中的作用
1 | // 这些都是没有意义, 大量的这种命名,在后期阅读代码时,简直是个灾难 |
如何命名呢?
可以按这个思路来: 在业务中的作用 –> 用中文描述它的作用 –> 翻译成英文
尽量做到见名知义
命名规范
这个不多说了,
比如:js可以这样命名:
1、类命名:首字母大写
2、普通方法变量,小驼峰命名
3、常量: 全大写,
4、局部变量:_开头
结构清晰
if-else 问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25if(true){
if(true){
} else if(true){
} else {
}
} else if(true){
if(true){
} else if(true){
} else {
}
} else {
if(true){
} else if(true){
} else {
}
}回调函数问题
需求:一个操作需要调三个接口A,B,C,B依赖A的返回结果,C依赖B的返回结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 用jquery 实现就是这种样式
$.ajax({
url:'urlA',
params:{},
success: function(resA){
$.ajax({
url:'urlB',
params: resA,
success: function(resB){
$.ajax({
url:'urlC',
params: resB,
success: function(resC){
console.log(resC)
}
})
}
})
}
})ES6 之后引用了 promise async 主要就是为了解决回调问题 避免回调地狱
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29// promise then 方式解决
function request(url, params){
return new Promise((resolve,reject) => {
$.ajax({
url:url,
params: params,
success:function(res){
resolve(res);
},
error: function(err){
reject(err)
}
})
})
}
// 调用
request('urlA', {})
.then(res => request('urlB', res))
.then(res => request('urlC', res))
.catch(err => console.log(err));
// async
async function getData(url, params){
return axios.post(url, params);
}
//调用
let resA = await getData('urlA', {});
let resB = await getData('urlB', resA);
let resC = await getData('urlC', resB);3、可复用性
DRY原则 Don’t Repeat Yourself
写过一遍的操作,就不重复第二遍了
尽量不去写重复代码, 这可能带来一些维护上的负作用,这个就要把握一个度了
要看某段代码有没有提取的必要,比如重复了多少次,使用量大不大
逻辑复用,提取代码
针对局部某个操作,某个功能而言
重复的部分提取成一个公用的方法
创建公用模板
针对全局性的,创建公用模块:
layout | header | footer | common.css | util.js
4、可扩展性
程序上的一个终极难题
对开发人员的架构思维,模块思维要求是非常高的
产品经理不可能不修改他的需求,就像程序员不可能不写bug一样
如果在写代码的时候预先考虑到后期需求可能的变化,那在需求变化时,你就会非常舒服了
模块分明
积木式编程,随时可以插入、移除某个模块
耦合度低
划分低耦合的模块,并高效设计模块间的沟通 (架构层面讲)
比如你要开个饭店,你需要怎么设置你的组织架构?厨师模块,服务员模块,收银员模块(老板)
合适的扩展技巧
应用设计模式
二、设计模式概论
1、创建型设计模式
帮助我们优雅的创建对象
工厂模式
大量创建对象
Jquery时代,我们需要大量频繁的操作dom
1
2// $就是一个工厂,他批量生产jquery对象, 根据你传入的选择器,生成一批jquery对象
$('.className')[如何实现一个工厂模式呢?]
球类工厂需求:我们要生产不同类型的球:basketball footerball tennis(网球)…
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28// 球类工厂
function ballFactory(type){
switch(type){
case 'football':
return new football();
break;
case 'basketball':
return new basketball();
break;
case 'tennis':
return new tennis();
break;
default:
break;
}
// 可以在局部
function football(){}
function basketball(){}
function tennis(){}
}
// 可以在原型链上
ball.prototype.football = function(){}
ball.prototype.basketball = function(){}
ball.prototype.tennis = function(){}[再看看jquery是如何实现的]
Jquery如何实现工厂模式的1
2
3
4
5
6
7
8
9// 静态函数 jquery的实现
function jquery(params){
return new jquery.fn.init(params);
}
jquery.fn = {};
jquery.fn.init = function (params) {};
window.$ = jquery;
$('.className');// 实际是通过new jquery静态属性fn上的init方法jquery是通过挂载一个静态属性实现的
建造者模式
精细化组合一个对象:类似建房子
盖房子明, 先把砖,门,窗这些材料都准备好,再把这些材料搭在一起,就建成了.关键思想 就是先把相关的模块提前独立开发好,再把各个模块拼装集成到一起
明显的标识就是 传入了一大堆的配置信息
1
2
3
4
5
6
7
8
9
10
11
12
13//建造者模式: 一般实现方案
// 关键思想 先把相关的模块放一边开发好,再把各模块集成到一起
function F1(){
// 直接绑定
this.model1 = new Model1({});
this.model2 = new Model2({});
this.model3 = new Model3({});
}
function Model1(params){}
function Model2(params){}
function Model3(params){}vue 也是用建造者模式实现的, 但双是跟一般的方案又不太一样
混入方式1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// 明显的标识就是 传入了一大堆的配置信息
// 方法2 Vue 是怎么做的
// 先把各个模块独立开发好,之后再混合进实例里
function Vue (options) {
// 健壮性校验
if (!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);
}
// 下面就是各个模块的混入Vue原型上
initMixin(Vue);
stateMixin(Vue);
eventsMixin(Vue);
lifecycleMixin(Vue);
renderMixin(Vue);单例模式
全局只有一个,对象只能被创建一次
如何实现 单例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 常规类:靠挂在这个类上面的一个静态属性
function Class1(params){
if(Class1.instance){ // 查检有没有实现,有则直接返回该实例,无则创建实例,并增加标识
return Class1.instance;
} else {
this.name ='xxx'
this.attr = 'bbbb';
Class1.instance = this;
}
}
// 两个new 是同一个实例
let class11 = new Class1();
let class12 = new Class1();
console.log(class11 == class12)再看vuex vue-router
vuex: 全局只有一个,如果有多个,那页面的状态从哪个取也是问题
vue-router:全局中有一个,如果有多个的话,页面跳转的时候,选用哪个就是一个问题1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 以vue-router为例子
// vue插件时如何保证单例的 以vue-router为例
// 通过一个参数,变量,静态属性去标识这个类是否被new 过,已经new过的,不再去new
Vue.use(vueRouter);
// Vue.use 实际上是调用了插件的install方法
// vue-router
{
var _vue;
function install(vue){
if(_vue === vue && install.installed)
return;
_vue = vue;
install.installed = true;// 静态属性
}
}
问题:
需求1:实现一个消息提示弹框插件,[用什么模式?]
弹框提示插件
分析: 因为一个页面的消息提示可能会很多,添加成功,添加失败,网络出错
会需要频繁创建对象,所以考虑用工厂模式实现
1 | // 工厂模式实现 调用如下 |
需求2: 实现一个编辑器插件:有前进,后退,改变字体,大小,颜色功能,[用什么模式?]
编辑器插件
需求: 有前进,后退,改变字体,大小,颜色功能
分析:
1、一般一个页面只有一个,不需要频繁创建
2、编辑器可能还要涉及到复杂的配置,需要精细化创建
建造者模式
1 | // html 初始化--> 事件绑定 --> 前进后退模块 -->数据记录模块 --> 字体控制模块 --> 数据渲染模块 |
2、结构型设计模式
帮助我们更优雅的设计代码结构: 策略,享元
还以一个具体的需求为例:
需求:写一个表单验证工具,给我要验证的input 值 value 变化时,应用对应的规则,自动验证
分析:
1、首先是包含的模块:dom初始化模块 –> 事件绑定模块 –> 验证模块 –> 消息提示模块
2、一个页面上可能有多个需要验证的dom,所以适合工厂模式
1 | // 1、防JQuery实现 |
[优化后的验证工具]
表单验证工具
需求:写一个表单验证工具,给我要验证的input 值 value 变化时,应用对应的规则,自动验证
分析:
1、首先是包含的模块:dom初始化模块 –> 事件绑定模块 –> 验证模块 –> 消息提示模块
2、一个页面上可能有多个需要验证的dom,所以适合工厂模式
1 | // 1、防JQuery |
需求2:一个div 实现 上、下、左、右、左上、左下、….这样移动
moveDiv(‘left’) // 左移
moveDiv(‘left’,’top’) // 左上移
1 | function moveDiv(){ |
[状态模式实现moveDiv]
移动Div
需求2:一个div 实现 上、下、左、右、左上、左下、….这样移动
moveDiv(‘left’) // 左移
moveDiv(‘left’,’top’) // 左上移
1 | // 用状态模式实现 |
使用设计模式之后,这个模块的代码更简洁更易读了
享元模式
存在类似对象和类似代码块时,用于减少类似代码块
这个享元是提取 相同的内容,还是不同的内容?
享元模式里的享无是 代码中的不同点
把不同的提出来,剩下的就是相同的,这样代码块就由多化一了
与平时提取公共代码不是一个意思
以jquery.extend方法为例
extend实现的功能:
$.extend({a:1}) // 会把对象扩展到$对象上$.a = 1; 为jquery对象扩展方法属性使用
$.extend({a:1},{b:2}) // ==> {a:1,b:2}
1 | // 不用设计模式时,是这样实现的 |
[jquery的extend 享元模式实现]
jquery.extend 享元模式实现
extend实现的功能:
$.extend({a:1}) // 会把对象扩展到$对象上$.a = 1; 为jquery对象扩展方法属性使用
$.extend({a:1},{b:2}) // ==> {a:1,b:2}
1 | // 不用设计模式时,是这样实现的 |
总结应用场景: 两个if else 分支中,两段代码块非常相似时,就可以用享元模式了
3、行为型设计模式
模块间的行为模式的总结,帮助组织模块的沟通:装饰者 观察者
装饰者模式
目的:不重写方法的扩展方法
应用场景:当一个方法需要扩展,但又不方便或不能去修改方法时 是公共的方法 或他人的方法 或原生方法 或第三方模块的方法
简单理解 在不去修改原方法的情况下,扩展方法的功能
需求: 删除按钮–> 点击可以删除 —> 但没有提示 —> 好多删除按钮都是这样实现,产品需求是要给出删除提示
分析:在原来的删除功能基础上,扩展出提示功能1、全部改写 2、找到原来定义,修改原方法,增加提示
[你会选哪种方式实现?]
装饰者模式
目的:不重写方法的扩展方法
应用场景:当一个方法需要扩展,但又不方便或不能去修改方法时 是公共的方法 或他人的方法 或原生方法 或第三方模块的方法
简单理解 在不去修改原方法的情况下,扩展方法的功能
需求: 删除按钮–> 点击可以删除 —> 但没有提示 —> 好多删除按钮都是这样实现,产品需求是要给出删除提示
分析:在原来的删除功能基础上,扩展出提示功能1、全部改写 2、找到原来定义,修改原方法,增加提示
1 | function decorate(dom, fn){ |
vue2 的双向数据绑定
1 | // Vue的双向绑定 非数组属性 |
[vue2 对数组属性实现双向绑定]
Vue2 数组的双向绑定
1 |
|
**观察者模式** > 应用场景: > 1、异步模块沟通 >>a 异步模块;b 同步模块; b需要a处理完成后a的消息 观察者 类似事件监听
2、方便加入新的模块, 本来没有想到某个模块会突然要加入的情况
例如:聊天室的沟通:我和你在聊天室的沟通(聊天室就是观察者),这样别人加入更容易
根本没有考虑过你要加入时,你非要加入,实在没办法了,才会引入观察者模式;
虽然使用观察者容易做到某件事儿,但它也确实要花费一些开销
看这样一个抽奖的需求
需求:抽奖转盘,特点是越转越快,
模块分析: 奖品初始化html –> 最终结果选定 –> 转动控制 –> 转动效果转动控制模块 调用 转动效果模块,
转动效果只负责转动效果; 转完之后,询问控制模块接下来怎么转;
转动效果模块接收消息体{moveTime: 10, speed: 200} 多长时间内转动几个奖品 –> setInterval 异步了,
转动控制与转动效果的沟通问题在于:转动控制不知道什么时候转动效果结束,因为转动速度是不恒定的
1 | // 先定义好几个模块 |
[抽奖转盘 观察者模式]
观察者模式
需求:抽奖转盘,特点是越转越快,
模块分析: 奖品初始化html –> 最终结果选定 –> 转动控制 –> 转动效果转动控制模块 调用 转动效果模块,
转动效果只负责转动效果; 转完之后,询问控制模块接下来怎么转;
转动效果模块接收消息体{moveTime: 10, speed: 200} 多长时间内转动几个奖品 –> setInterval 异步了,
转动控制与转动效果的沟通问题在于:转动控制不知道什么时候转动效果结束,因为转动速度是不恒定的
1 | // 观察者三要素:队列,注册,触发 |
创建型设计模式 –> 创建对象阶段使用
结构型设计模式 –> 用在一个对象内部代码优化
行为型设计模式 –> 模块之间的沟通交互
三、设计模式解决问题
- 1、if-else模式
- 2、减少重复代码
- 3、更好的扩展方法
- 4、解耦模块