前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >开放-封闭原则(OCP,Open - Closed Priciple)

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

作者头像
IMWeb前端团队
发布2017-12-29 18:30:20
1.2K0
发布2017-12-29 18:30:20
举报
文章被收录于专栏: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布局组件

初始代码大致如下:

代码语言:javascript
复制
// 组件模板随意扩展,为简单起见,这里直接静态写死
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内容的时候加上动画咯,如下:

代码语言:javascript
复制
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组件的代码:

代码语言:javascript
复制
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方法的接口而已

见下面的代码:

代码语言:javascript
复制
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的切换会不会也有动画效果呢?如果有,我们可以简单地处理,如下:

代码语言:javascript
复制
var tabBar = {
    // ...
    changeTo: function(index) {
        this.opts.changeTo && this.opts.changeTo(index);
        this.opts.cnt.changeTo(index);
    }
};

4.4 多变的"抽象"处理

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

4.4.1 通过参数

通过参数来扩展组件是很常见的,实际上大家都这么处理的 比如,现在tab的初始化位置要抽象出来,那就提供一个参数呗,如下:

代码语言:javascript
复制
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 通过事件

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

改成事件的代码如下:

代码语言:javascript
复制
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的配合了,如下:

代码语言:javascript
复制
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'
});

这种处理实在太常用,以致于笔者写的组件基本都有这么一句,这坏习惯是改不了了。。。

在容器添加扩展类,还是会依赖原来的结构,如果要完全解耦合结构扩展,可能需要在每个关键节点上添加类 具体要不要这么麻烦,就看设计者的选择了

最后一个例子了:

代码语言:javascript
复制
var com = {
    // ...
    show: function() {
        this.$box.show();
    },
    hide: function() {
        this.$box.hide();
    }
};

也是一个很常见的代码:处理组件显示隐藏

我们知道,控制元素隐藏有很多种方式,最常用的3种:

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

每种都有自己的特点以及适用场景,show和hide方法实际上是用第一种方式

如果写死了,到时候要改变这里就麻烦了,因此通过类来处理会更好,方案如下:

代码语言:javascript
复制
// 方案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思想提倡我们对抽象编程,拥抱变化,适应变化

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

本文参与?腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客?前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体同步曝光计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 开放-封闭原则(OCP,Open - Closed Priciple)
    • 1 前言
      • 2 什么是OCP?
        • 3 如何做到OCP?
          • 4 OCP应用&思考
            • 4.1 初始例子
            • 4.2 "对抽象编程"
            • 4.3 何为抽象?
            • 4.4 多变的"抽象"处理
            • 4.5 CSS的OCP
          • 5 小结
          相关产品与服务
          容器服务
          腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
          http://www.vxiaotou.com