害羞地看完了《单一职责简述》,自然想到了另外一个重要的原则——开放&封闭原则
开放&封闭原则是程序设计的一个重要原则,相比于著名的SPR,这个原则可能不太容易被人们记住,但是这个原则却不容忽视
经典的设计模式都是基于C++/Java的OOP,相信读者都耳熟能详了
本文是基于JavaScript来的,同时也会提到OCP在前端程序中的应用与表现
OCP的核心如下:
Open for extension, Closed for modification
翻译过来是:对扩展开放,对修改封闭
需求总是变化的,面对变化,一个优秀的程序(类,组件)应该是通过扩展来适应新的变化,而不是通过修改
另一方面,也就是说,当一个程序(类,组件)写好之后,就不应该再修改它的代码(bug不算)
如果违反了OCP,当你发现自己经常在改一个类/组件的源代码的时候,那这个类/组件应该也违反SPR了
根据经典的设计模式思想,要做到OCP,最优的途径是:对抽象编程
让类依赖抽象,当需要变化的时候,通过实现抽象来适应新的需求
对抽象编程,是利用了另外两大原则:
在前端领域,少有复杂的类体系出现,所以人们或许以为,在前端程序,OCP毫无用武之地
实则不然,OCP实质上是一种思想,这种优秀的思想可以指导我们写出优秀的代码
对于前端领域,没有类,但是有一个很重要的实体,那就是组件
一个优秀的组件实际上是应该遵循OCP的
我们通过一个tab组件作为例子,先来看看什么是tab组件,如下图所示:
很常见的一个tab布局组件
初始代码大致如下:
// 组件模板随意扩展,为简单起见,这里直接静态写死
var barTpl = '\
<ul class="tab-bar">\
<li class="tab-bar__item z-active">1</li>\
<li class="tab-bar__item">2</li>\
<li class="tab-bar__item">3</li>\
</ul>';
var cntTpl = '\
<ul class="tab-cnt">\
<li class="tab-cnt__item z-active">1</li>\
<li class="tab-cnt__item">2</li>\
<li class="tab-cnt__item">3</li>\
</ul>';
// 为简单起见,只写了一些核心代码,其它的就忽略了,比如重复初始化之类的
var tab = {
init: function(opts) {
this.opts = $.extend({}, opts);
this.$barBox.html(barTpl);
this.$cntBox.html(cntTpl);
this.$barItems = this.$barBox.find('.tab-bar__item');
this.$cntItems = this.$cntBox.find('.tab-cnt__item');
this.__bindEvent();
},
__bindEvent: function() {
var self = this;
this.$barBox.on('click', '.tab-bar__item', function() {
self.changeTo(self.$barItems.index($(this)));
});
},
changeTo: function(index) {
this.$barItems.removeClass('z-active').eq(index).addClass('z-active');
this.$cntItems.removeClass('z-active').eq(index).addClass('z-active');
}
};
直捣核心,先来讨论前端领域的“对抽象编程”
恩,组件工作得挺好,但是在体验的时候,设计觉得不好看,tab内容切换的时候要加上动画
好吧,我们再切换tab内容的时候加上动画咯,如下:
var tab = {
// ...
changeTo: function(index) {
this.$barItems.removeClass('z-active').eq(index).addClass('z-active');
this.__changeToCntWithAnimation(index);
},
__changeToCntWithAnimation: function(index) {
// 加上动画效果的切换
// ...
}
};
设计再次体验,还是不好看,要换一种动画效果!
没事,很简单呀,我改__changeToCntWithAnimation方法就可以啦,so easy!
"不行不行,还是不好看,再换这种试试~" "哦",继续改__changeToCntWithAnimation
"还是不好看,算了,还是用回第一种动画效果吧~" "!!!@@@&*&...",我忍,我还有svn代码回退!
上面的场景相信很常见,看着就是一把辛酸泪
设计的需求是可以理解的,有时候我们回避不了需求变更,但是我们有没有更好的方案去适应这些变更呢?
答案当然是有的,下面我们这样来改这个组件:
把tab组件拆分,分成tabBar组件和tabCnt组件,就是把tab页卡和tab容器分成两个组件对待
其实,通过tab组件的代码,相信读者已经发现了,很多地方的代码看起来很相似,唯一不同的只是处理的对象不一样而已,实际上,这个组件也违反了SRP原则,它做了两件事情!所以,分开是很自然而然的
但是,分开之后我们要怎么处理?如何设计可以很好的适应上述需求变化?答案是对抽象编程
具体怎么抽象,哪里是抽象?答案是哪里会出现变化,哪里就需要抽象
现在是tabCnt需要变化,因此,要对tabCnt进行抽象
然后我们再看下tabBar组件的代码:
var tpl = '\
<ul class="tab-bar">\
<li class="tab-bar__item z-active">1</li>\
<li class="tab-bar__item">2</li>\
<li class="tab-bar__item">3</li>\
</ul>';
var tabBar = {
init: function(opts) {
this.opts = $.extend({}, opts);
this.$box.html(tpl);
this.$items = this.$box.find('.tab-bar__item');
this.__bindEvent();
},
__bindEvent: function() {
var self = this;
this.$box.on('click', '.tab-bar__item', function() {
self.changeTo(self.$items.index($(this)));
});
},
changeTo: function(index) {
this.$items.removeClass('z-active').eq(index).addClass('z-active');
this.opts.cnt.changeTo(index); // mark
}
};
注意mark标注的那行代码,在切换tab的时候,组件在更新自身状态的同时,也让tab容器切换它的内容,具体的做法就是让tabBar组件聚合一个tab容器组件,然后调用tab容器组件的changeTo方法
注意,opts.cnt参数有规定内容是什么吗?有规定一定要一个什么tabCnt类的对象吗?No!它只有一个要求,就是需要是一个拥有changeTo方法的对象,这个方法接受一个index的数字参数,其它的随便
这,就是抽象,相信很多读者都会觉得熟悉,这个抽象就是一个仅有changeTo方法的接口而已
见下面的代码:
tabBar.init({
cnt: {
// 其他内容忽略
changeTo: function(index) {
this.$items.hide().eq(index).show();
}
}
});
// 改变来了,需要动画效果~
tabBar.init({
cnt: {
// 其他内容忽略
changeTo: function(index) {
this.changeToWithAnimation(index); // 带动画的切换
}
}
});
// 换新效果
tabBar.init({
cnt: {
// 其他内容忽略
changeTo: function(index) {
this.changeToWithNewAnimation(index); // 新动画的切换
}
}
});
不用改tabBar组件以及原来的tabCnt的代码(这里是新增了一个tabCnt对象,你可以看成是OOP的继承)就适应了需求变更,这就是OCP最简单直接的体现
当最后,需要切回第一个动画效果的时候,也很容易,因为原来的那个效果的tabCnt组件没有被覆盖,新效果的tabCnt组件应该是新增的!
何为抽象?正如上面所说
哪里会出现变化,哪里就需要抽象
这句话和【变化就是抽象】是不一样的,上面那句话还带有预测的性质,具体讨论如下:
有完美的组件吗?没有
正如没有完美的软件一样,这个世界上没有银弹!
程序的世界一定会有变更,具体是怎么处理这些变更,怎么更好,更高效地适应变更
无论组件是多么的“封闭”,都会存在一些无法对之封闭的变化 既然不可能完全封闭,设计人员必须对于他设计的模块应该对哪种变化封闭做出选择,他必须先猜测出最有可能发生的变化种类,然后构造抽象来隔离那些变化 等到发生变化时立即采取行动,以应对发生更大的变化
组件是慢慢完善的,它遵循自然规则——成长进化
一开始写组件的时候,如果考虑太多的变化,想着自己要写一个完美的组件,到处抽象,那就会让组件很复杂,这样反而得不偿失,乱抽象也是一种错误
在第一次组件完成的时候,我们不应该特意去猜测哪里可能会出现变化,然后去做抽象,这个工作应该是写组件之前就设计好的
当出现变化的时候,我们要改组件代码,这个时候不要盲目就去改,而是要思考是什么原因导致这种变化,后续会不会有同类型的变化出现?经过思考再重新设计抽象,做出修改,以后出现同类型的改变时,就可以通过扩展,而不是修改代码来适应变更了
因此,在OCP的思想下,组件应该是这样迭代出来的
接着上面的例子,现在变更的是tabCnt,那么,tabBar的切换会不会也有动画效果呢?如果有,我们可以简单地处理,如下:
var tabBar = {
// ...
changeTo: function(index) {
this.opts.changeTo && this.opts.changeTo(index);
this.opts.cnt.changeTo(index);
}
};
"抽象"可大可小,在前端领域,类系统不多,传统的抽象也谈不上
通过参数来扩展组件是很常见的,实际上大家都这么处理的 比如,现在tab的初始化位置要抽象出来,那就提供一个参数呗,如下:
var tabBar = {
init: function(opts) {
this.opts = $.extend({}, opts);
this.$box.html(tpl);
this.$items = this.$box.find('.tab-bar__item');
this.changeTo(this.opts.index || 0); // 通过新增参数,提供默认值来扩展功能
this.__bindEvent();
},
// ...
};
在前端领域,事件系统(订阅者模式)非常灵活,它可以替代聚合,而且还有更多的特性存在
改成事件的代码如下:
var tabBar = {
// ...
changeTo: function(index) {
this.opts.changeTo && this.opts.changeTo(index);
// this.opts.cnt.changeTo(index);
$(document).trigger('changeTab', [index]);
}
};
var tabCnt = {
// ...
__bindEvent: function() {
$(document).on('changeTab', function(e, index) {
// ...
});
}
}
var tb = tabBar.init({...});
var tc1 = $.extend({}, tabCnt).init({...});
var tc2 = $.extend({v, tabCnt).init({...});
在前端,通过事件来解耦是很常用的手段了,这里也不多说
利用事件,还可以实现用同一个tabBar,同时控制多个tabCnt的效果
还有一种抽象处理是只定义过程/逻辑,具体的行为,比如创建节点,展现,销毁等等都抽象处理 举个例子:类似组件系统的基类那样,定义组件的生命周期,具体每个结点的处理由子类实现 还有一些需要提供插件扩展能力的组件/系统,它们也是这样的设计,例如fis构建工具,定义构建的处理流程,提供插件扩展点 笔者不太喜欢类系统,因此更多的是使用类似建造者模式的结构实现
前端只有js吗?不,还有css
样式的改变也是经常有的事,同样,它们也要遵循OCP,才能更好的适应变化
回到之前tab的例子,之前的截图中看到,那个tab是横排的,现在页面重构,改成了纵排的tab怎么办?如下图:
实际上,tabBar组件也可以是nav组件,不是吗?
css本身的特性很好的支持扩展
直接改tab-bar样式就好了,那如果页面有两个tabBar组件呢?
就像一般的样式组件化思想那样,添加扩展类才是正途,那这个就需要js的配合了,如下:
var tabBar = {
init: function(opts) {
this.opts = $.extend({}, opts);
this.$box.html(tpl).addClass(this.opts.cls || ''); // 添加扩展样式,mark
this.$items = this.$box.find('.tab-bar__item')/*.addClass(this.opts.itemCls || '')*/;
this.changeTo(this.opts.index || 0); // 通过新增参数,提供默认值来扩展功能
this.__bindEvent();
},
// ...
};
tabBar.init({
cls: 'my-tab-bar-cls'
});
这种处理实在太常用,以致于笔者写的组件基本都有这么一句,这坏习惯是改不了了。。。
在容器添加扩展类,还是会依赖原来的结构,如果要完全解耦合结构扩展,可能需要在每个关键节点上添加类 具体要不要这么麻烦,就看设计者的选择了
最后一个例子了:
var com = {
// ...
show: function() {
this.$box.show();
},
hide: function() {
this.$box.hide();
}
};
也是一个很常见的代码:处理组件显示隐藏
我们知道,控制元素隐藏有很多种方式,最常用的3种:
每种都有自己的特点以及适用场景,show和hide方法实际上是用第一种方式
如果写死了,到时候要改变这里就麻烦了,因此通过类来处理会更好,方案如下:
// 方案1
var com = {
// ...
show: function() {
this.$box.addClass('z-show');
},
hide: function() {
this.$box.removeClass('z-show');
}
};
// 方案2
var com = {
// ...
show: function() {
this.$box.removeClass('z-show z-hide').addClass('z-show');
},
hide: function() {
this.$box.removeClass('z-show z-hide').addClass('z-hide');
}
};
// css:
// .z-show { display: block; }
// .z-hide { display: none; }
方案2的好处是可以更好地使用动画!
在css中,类可以扩展,因此也是抽象点 html自身并没有提供什么扩展机制,除非利用构建工具。。。
虽然SRP和OCP是在OOP程序设计模式中发扬光大,但是笔者认为,这两大原则是两个优秀的程序设计思想,这两大思想可以指导程序员编写出灵活健壮的程序,让代码可扩展,可维护,易读
OCP思想提倡我们对抽象编程,拥抱变化,适应变化
不管是借鉴传统的设计模式还是独属于前端的设计模式,都离不开这两大核心原则,因此,作为一名前端攻城狮也需要稍微了解一下,才能在潜移默化中编写出高质量的代码