黎清龙

2015-10-08 20:44

开放-封闭原则(OCP,Open - Closed Priciple)

本文作者:IMWeb 黎清龙 原文出处:IMWeb社区 未经同意,禁止转载

开放-封闭原则(OCP,Open - Closed Priciple)

1 前言

害羞地看完了《单一职责简述》,自然想到了另外一个重要的原则——开放&封闭原则

开放&封闭原则是程序设计的一个重要原则,相比于著名的SPR,这个原则可能不太容易被人们记住,但是这个原则却不容忽视

经典的设计模式都是基于C++/Java的OOP,相信读者都耳熟能详了

本文是基于JavaScript来的,同时也会提到OCP在前端程序中的应用与表现

2 什么是OCP?

OCP的核心如下:

Open for extension, Closed for modification

翻译过来是:对扩展开放,对修改封闭

需求总是变化的,面对变化,一个优秀的程序(类,组件)应该是通过扩展来适应新的变化,而不是通过修改

另一方面,也就是说,当一个程序(类,组件)写好之后,就不应该再修改它的代码(bug不算)

如果违反了OCP,当你发现自己经常在改一个类/组件的源代码的时候,那这个类/组件应该也违反SPR了

3 如何做到OCP?

根据经典的设计模式思想,要做到OCP,最优的途径是:对抽象编程

让类依赖抽象,当需要变化的时候,通过实现抽象来适应新的需求

对抽象编程,是利用了另外两大原则:

  1. 里氏代换原则(LSP,Liskov Substitution Principle)
  2. 合成/聚合复用原则(CARP,Composite Aggregate Reuse Principle)

4 OCP应用&思考

在前端领域,少有复杂的类体系出现,所以人们或许以为,在前端程序,OCP毫无用武之地

实则不然,OCP实质上是一种思想,这种优秀的思想可以指导我们写出优秀的代码

对于前端领域,没有类,但是有一个很重要的实体,那就是组件

一个优秀的组件实际上是应该遵循OCP的

4.1 初始例子

我们通过一个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');
    }
};

4.2 "对抽象编程"

直捣核心,先来讨论前端领域的“对抽象编程”

恩,组件工作得挺好,但是在体验的时候,设计觉得不好看,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组件应该是新增的!

4.3 何为抽象?

何为抽象?正如上面所说

哪里会出现变化,哪里就需要抽象

这句话和【变化就是抽象】是不一样的,上面那句话还带有预测的性质,具体讨论如下:

有完美的组件吗?没有

正如没有完美的软件一样,这个世界上没有银弹!

程序的世界一定会有变更,具体是怎么处理这些变更,怎么更好,更高效地适应变更

无论组件是多么的“封闭”,都会存在一些无法对之封闭的变化 既然不可能完全封闭,设计人员必须对于他设计的模块应该对哪种变化封闭做出选择,他必须先猜测出最有可能发生的变化种类,然后构造抽象来隔离那些变化 等到发生变化时立即采取行动,以应对发生更大的变化

组件是慢慢完善的,它遵循自然规则——成长进化

一开始写组件的时候,如果考虑太多的变化,想着自己要写一个完美的组件,到处抽象,那就会让组件很复杂,这样反而得不偿失,乱抽象也是一种错误

在第一次组件完成的时候,我们不应该特意去猜测哪里可能会出现变化,然后去做抽象,这个工作应该是写组件之前就设计好的

当出现变化的时候,我们要改组件代码,这个时候不要盲目就去改,而是要思考是什么原因导致这种变化,后续会不会有同类型的变化出现?经过思考再重新设计抽象,做出修改,以后出现同类型的改变时,就可以通过扩展,而不是修改代码来适应变更了

因此,在OCP的思想下,组件应该是这样迭代出来的

接着上面的例子,现在变更的是tabCnt,那么,tabBar的切换会不会也有动画效果呢?如果有,我们可以简单地处理,如下:

var tabBar = {
    // ...
    changeTo: function(index) {
        this.opts.changeTo && this.opts.changeTo(index);
        this.opts.cnt.changeTo(index);
    }
};

4.4 多变的"抽象"处理

"抽象"可大可小,在前端领域,类系统不多,传统的抽象也谈不上

4.4.1 通过参数

通过参数来扩展组件是很常见的,实际上大家都这么处理的 比如,现在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();
    },
    // ...
};

4.4.2 通过事件

在前端领域,事件系统(订阅者模式)非常灵活,它可以替代聚合,而且还有更多的特性存在

改成事件的代码如下:

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构建工具,定义构建的处理流程,提供插件扩展点 笔者不太喜欢类系统,因此更多的是使用类似建造者模式的结构实现

4.5 CSS的OCP

前端只有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种:

  1. display: none
  2. visibility: hidden
  3. height: 0

每种都有自己的特点以及适用场景,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自身并没有提供什么扩展机制,除非利用构建工具。。。

5 小结

虽然SRP和OCP是在OOP程序设计模式中发扬光大,但是笔者认为,这两大原则是两个优秀的程序设计思想,这两大思想可以指导程序员编写出灵活健壮的程序,让代码可扩展,可维护,易读

OCP思想提倡我们对抽象编程,拥抱变化,适应变化

不管是借鉴传统的设计模式还是独属于前端的设计模式,都离不开这两大核心原则,因此,作为一名前端攻城狮也需要稍微了解一下,才能在潜移默化中编写出高质量的代码

3条评论

    您需要 注册 一个IMWeb账号或者 才能进行评论。