软件设计模式大家或多或少都有接触和了解过,比如单例模式、工厂模式、观察者模式、MVC模式等等。大多数开发者在实际工作中其实也在不知不觉的用到某种设计模式,但是并没有明确的概念。那什么是设计模式?设计模式对我们有什么帮助呢?
下面我们就以JavaScript为例,通过代码的演进来切身了解设计模式。
一个不是设计模式的模式
首先我们来看个合唱团的例子,合唱团需要招募男女声演唱者,那我们可以如下代码实现:
var Recruit = function( person ){
if (person instanceof Men){
console.log('男人在唱歌');
}else if ( person instanceof Women ){
console.log('女人在唱歌');
}
};
var Men = function(){
//do sth.
};
var Women = function(){
//do sth.
};
Recruit( new Men() );//男人在唱歌
Recruit( new Women() );//女人在唱歌
上面的代码很容易的把男人女人都加进来了,但是如果现在要加一个童声,我们还需要在Recruit函数中添加一个逻辑判断分支,对代码维护和稳定性都带来了很大的影响。所以,我们对Recruit进行修改:
var Recruit = function( person ){
//只要具有sound方法的对象都可以加入合唱团
if( typeof person.sound === 'function' )
person.sound();
};
var Men = function(){
//do sth.
};
Men.prototype.sound = function(){
console.log('男人在唱歌');
};
var Women = function(){
//do sth.
};
Women.prototype.sound = function(){
console.log('女人在唱歌');
};
var Child = function(){
//do sth.
};
Child.prototype.sound = function(){
console.log('儿童在唱歌');
};
Recruit( new Men() );//男人在唱歌
Recruit( new Women() );//女人在唱歌
Recruit( new Child() );//儿童在唱歌
修改后的代码不光是儿童,只要能发声(具有sound方法)的对象都可以加入合唱团。
其实上面的例子还只是JS多态的使用,会JAVA的童鞋应该知道,JAVA里使用继承可以达到这个效果,但JS任性的是,不需要做类型检测,具备相同的方法或属性的不同类型对象都可以统一进行操作,这个特性称为鸭子类型。
这个例子并没有涉及具体的模式,但是我们已经可以感受到JS语法的灵活性。也正是因为这个原因,相较于其他语言,基于JS的前端开发更容易产生代码不统一、不连贯的问题。
引入模式带来的变化
接下来我们来看一个最常见的例子,某系统需要实现功能,未登录前界面没有显示用户信息、退出登录按钮等信息,登录后需要刷新相应的区域准确显示信息。
一般实现的代码如下:
var showInfo = function(){
//显示个人信息
};
var showLogout = function(){
//显示退出登录按钮
};
var doLogin = function(){
//前面登录过程略
showInfo();//显示个人信息
showLogout();//显示退出登录按钮
};
document.querySelector('#loginBtn')
.addEventListener('click', function(){
doLogin();//点击登录按钮调用登录方法
}, false);
这段代码看上去似乎很合理。其实有着跟合唱团一样的问题,如果现在要在界面增加显示个人权限模块的时候,我们得修改doLogin这个函数,同样破坏了代码的稳定性,增加维护难度。
我们试想一下用哪种模式来解决这个问题?先来看代码:
var Event = {
cache : {},
on : function(eventName, callback){//订阅事件
if( !this.cache[eventName] )
this.cache[eventName] = [];
this.cache[eventName].push(callback);
},
trigger : function(eventName){//发布事件
var callbacks = this.cache[eventName]||[];
for(var i=0,l=callbacks.length;i<l;i++){
callbacks[i]();
}
}
};
var showInfo = function(){
//显示个人信息
};
//为showInfo函数订阅doLoginEvent事件
Event.on('doLoginEvent', showInfo);
var showLogout = function(){
//显示退出登录按钮
};
//为showLogout函数订阅doLoginEvent事件
Event.on('doLoginEvent', showLogout);
var showModule = function(){
//显示个人权限模块
};
//为showModule函数订阅doLoginEvent事件
Event.on('doLoginEvent', showModule);
var doLogin = function(){
//前面登录过程略
//向doLoginEvent事件发布消息
Event.trigger('doLoginEvent');
};
document.querySelector('#loginBtn')
.addEventListener('click', function(){
doLogin();//点击登录按钮调用登录方法
}, false);
我们引入了一个Event对象来处理订阅与发布的逻辑,可以将登录后需要处理的函数都订阅到doLoginEvent事件中,当登录成功后发布doLoginEvent事件的消息。经过这样的处理,后面再添加的显示个人权限模块就不需要修改核心代码,只要同样订阅doLoginEvent事件即可。开发不同功能模块的开发者可以更专注于自己的业务逻辑。没错,这就是“发布-订阅模式”。
但如果需求变为,要处理的函数是按某个要求执行(不同的用户显示的信息或模块不同),订阅-发布模式显然不完全合适,可以考虑增加策略模式将不同的要求转化为策略。
总结
经过上面的两个例子,我们可以了解设计模式对我们编程的重要性:
1. 设计模式是为解决特定场景问题的方式和方法,设计模式为场景而生,场景变化可以促进代码重构(设计模式的变化、组合、创新),并沉淀下来;
2. 对设计模式有统一理解,容易形成解决方案的共识,从而提高团队的开发效率;
3. 正确使用设计模式,可以使代码结构化、模块化,提高代码的重用性、稳定性和可扩展性,降低代码的维护成本;
4. 学习设计模式后,就算不用模式中的方法,也会形成习惯采取更好的策略去解决问题。
诚然,使用设计模式也可能会增加代码的复杂度,毕竟可以用普通方法来解决的问题,使用设计模式后会带来额外的代码,而且使用不当,事情还会变得更糟。也会有初学者在刚学会一种模式的时候恨不得把所有的代码都用这个模式实现(笔者亲身经历),当然,模式应该用在正确的地方。
本文仅以JS作为引子,无论是使用什么开发语言,相信对我们大部分程序员来说,系统的学习设计模式并没有坏处,相反,你会在模式的学习过程收益匪浅。
最后给大家推荐Gof在1995年出版的《设计模式》一书,里面详细介绍了23种设计模式。