Javascript中的设计模式

设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。我们写代码总是会无意识的接触到别人的设计模式,例如每种语言都有自己的监听机制,这么看来其实设计模式从我们学编程起就一直伴随左右了。其实说到设计模式就离不开经验,当我们不断在一门语言里实践的过程中,总会用自己的理解组织代码。例如我每次重新写一个功能,都会总结哪里可以抽出来复用,哪里改动之后可以作为插件供其他人使用,这样就无意识的使用了一种设计模式。
使用设计模式都是为了让代码好维护,可读性更高,调用更合理。尤其是面向对象程序设计里,要遵循一些原则才具有良好的设计。

设计原则

面向对象程序设计有几个原则:单一职责原则 (Single Responsiblity Principle SRP)、里氏代换原则(Liskov Substitution Principle,LSP)、依赖倒转原则(Dependency Inversion Principle,DIP)、接口隔离原则(Interface Segregation Principle,ISP)、最小知识原则(Principle of Least Knowledge,PLK,也叫迪米特法则)、开闭原则(Open Closed Principle,OCP)。开闭原则具有理想主义的色彩,它是面向对象设计的终极目标。其他几条,则可以看做是开闭原则的实现方法。
我先简单介绍以下这几个原则的意思:

  • 单一职责原则:一个类只负责一项职责。在JS里即一个对象只做一件事情。
  • 里氏代换原则:类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。
  • 依赖倒转原则:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。
  • 接口隔离原则:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。
  • 最小知识原则:一个对象应该对其他对象保持最少的了解。也就是对象之间耦合度要小。
  • 开闭原则:一个软件实体如类、模块和函数应该可以扩展,但不可以修改。

接下来看看我们常用的设计模式是怎么实现这些原则的。

观察者模式

这个在实践中真的是随处可见。

1
2
dom.addEventListener("click",function(){ ...code... },false); //订阅
dom.click(); //发布

用的不能再多了吧,这个设计有什么好处呢?
假若,我们点击不同的DOM节点要触发一些事件,而事件和节点之间本身无关联,我们需要灵活的进行订阅和取消。

1
2
3
4
5
6
7
8
9
function callback1(){}
function callback2(){}
function callback3(){}
dom1.addEventListener("click",callback1,false);
dom1.addEventListener("click",callback3,false);
dom2.addEventListener("click",callback2,false);
dom2.addEventListener("click",callback3,false);
dom1.click();
dom2.click();

dom1并不需要关心dom2订阅过什么,dom1也不需要关心dom1之前订阅过什么,这样的模式在开发中优势就很明显了。

策略模式

表单验证写过不少吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
registerForm.onsubmit = function(){
if ( registerForm.userName.value === '' ){
alert ( '用户名不能为空' );
return false;
}
if ( registerForm.password.value.length < 6 ){
alert ( '密码长度不能少于6 位' );
return false;
}
if ( !/(^1[3|5|8][0-9]{9}$)/.test( registerForm.phoneNumber.value ) ){
alert ( '手机号码格式不正确' );
return false;
}
}

怎么样,现在还能看,但是当表单有100个input要一一验证怎么破?这样的话我们其实会调用一些组件库,像我正在用的组件库里其实有很多这样的判断字典:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var strategies = {
isNonEmpty: function( value, errorMsg ){ // 不为空
if ( value === '' ){
return errorMsg ;
}
},
minLength: function( value, length, errorMsg ){ // 限制最小长度
if ( value.length < length ){
return errorMsg;
}
},
isMobile: function( value, errorMsg ){ // 手机号码格式
if ( !/(^1[3|5|8][0-9]{9}$)/.test( value ) ){
return errorMsg;
}
}
};

然后在webapp中我们只需要向一个验证方法添加所需要的策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var validataFunc = function(){
var validator = new Validator(); // 创建一个validator 对象,代码略
/***************添加一些校验规则****************/
validator.add( registerForm.userName, 'isNonEmpty', '用户名不能为空' );
validator.add( registerForm.password, 'minLength:6', '密码长度不能少于6 位' );
validator.add( registerForm.phoneNumber, 'isMobile', '手机号码格式不正确' );
var errorMsg = validator.start(); // 获得校验结果
return errorMsg; // 返回校验结果
}
var registerForm = document.getElementById( 'registerForm' );
registerForm.onsubmit = function(){
var errorMsg = validataFunc(); // 如果errorMsg 有确切的返回值,说明未通过校验
if ( errorMsg ){
alert ( errorMsg );
return false; // 阻止表单提交
}
};

装饰者模式

还是表单验证,这是我们提交表单时的代码,怎么样,又有表单验证,又有ajax请求,符合单一职责原则吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
registerForm.onsubmit = function(){
if ( registerForm.userName.value === '' ){
alert ( '用户名不能为空' );
return false;
}
if ( registerForm.password.value.length < 6 ){
alert ( '密码长度不能少于6 位' );
return false;
}
if ( !/(^1[3|5|8][0-9]{9}$)/.test( registerForm.phoneNumber.value ) ){
alert ( '手机号码格式不正确' );
return false;
}
ajax(url,param);
}

当然,我们可以用钩子来优化代码,但随着功能的扩展,也可能出现if/else交替增加,又变成了噩梦。
那么我们可以用装饰者模式解决。

装饰者模式能够在不改变对象自身的基础上,在程序运行期间给对象动态地添加职责。跟继承相比,装饰者是一种更轻便灵活的做法,这是一种“即用即付”的方式,比如天冷了就多穿一件外套,需要飞行时就在头上插一支竹蜻蜓,遇到一堆食尸鬼时就点开AOE(范围攻击)技能。

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
30
31
Function.prototype.before = function( beforefn ){
var __self = this;
return function(){
if ( beforefn.apply( this, arguments ) === false ){
// beforefn 返回false 的情况直接return,不再执行后面的原函数
return;
}
return __self.apply( this, arguments );
}
}
var validata = function(){
if ( username.value === '' ){
alert ( '用户名不能为空' );
return false;
}
if ( password.value === '' ){
alert ( '密码不能为空' );
return false;
}
}
var formSubmit = function(){
var param = {
username: username.value,
password: password.value
}
ajax( 'http:// xxx.com/login', param );
}
formSubmit = formSubmit.before( validata );
submitBtn.onclick = function(){
formSubmit();
}

其实ES6新增的Promise就算是一个装饰者,不过它是后置装饰,形成了一个异步的包装链。
设计模式还有很多说不完的东西,边实践边总结吧。