编码最佳实践——开放封闭原则

开放封闭原则定义

开放与封闭原则有两种不同的定义,分别是20世纪80年代最原始的定义和后期一个更现代的定义,后者对前者进行更加详尽的阐述。

Meyer的定义

软件实体应该允许扩展,但禁止修改 ——《面向对象软件构造》

Martin的定义

”对于扩展是开放的。“ 这意味着模块的行为是可以扩展的。当应用程序的需求改变时,我们可以对其模块进行扩展,使其具有满足那些需求变更的新行为。换句话说,我们可以改变模块的功能。 “对于修改是封闭的。“ 对模块行为进行扩展时,不必改动该模块的源代码或二进制代码。模块的二进制可执行版本,无论是可链接的库、DLL或Java的.jar文件,都无需改动。 ——《敏捷软件开发:原则、模式与实践》

对于修改是封闭的

需要注意的是,“对于修改是封闭的”有两个例外:

1.修复缺陷所做的改动

2.客户端无法感知到的改动

缺陷修复

缺陷在软件中很常见,是不可能完全消除的。当缺陷出现时,就需要我们修复现有的代码。软件修复明显倾向于实用主义而不是坚持开放封闭原则。

客户端感知

如果一个类的改动会引起另一个类的改动,那么这两个类就是紧密耦合的。相反,如果一个类的修改总是独立的,并不会引起其他类的改动,那么这些类就是松散耦合的。我们要记住,任何情况下,松散耦合都比紧密耦合要好。如果我们对现有代码的修改不会影响客户端代码,那么也就谈不上违背开放封闭原则。

对于扩展是开放的

扩展点

没有扩展点

TradeProcessorClient类直接依赖TradeProcessor类。当接到一个需要改动TradeProcessor类的新需求时,为了不改变原有的类型,创建了一个新类型(TradeProcessor2)来实现需求提出的新功能。但是这种改动带来的副作用就是必须改动TradeProcessorClient类,这样才能依赖的新的TradeProcessor2类。

如果对现有代码的改动不会影响客户端,那就不需要创建新类型。但是如果对现有代码的改动改变了TradeProcessor类方法的签名,那就不是简单的对类实现的改动,而是对接口的改动了。因为客户端总是与服务的接口紧密耦合的,所以任何接口上的改动都会引起客户端代码的改动。

虚方法

TradeProcessor类的另一种实现包含了一个扩展点:ProcessTrades是个虚方法。

任何一个带有虚方法成员的类都是对外开放的,这种扩展是通过继承做到的。可以修改其子类的ProcessTrades方法而无需改变原有的TradeProcessor类源码。此时的TradeProcessorClient类也不需要做改动,可以使用多态向客户端提供新版本的TradeProcessor2类的实例。

但是使用虚方法能重新实现的范围是有一定限制的。在子类中可以访问基类,因此可以直接调用TradeProcessor类的ProcessTrades方法,但是无法改动该方法内的任何代码。要么在子类方法里调用基类同名方法并在其前后实现新的特性,要么完全重新实现子类的方法。虚方法没有中间状态。另外子类只能访问基类的受保护和公共成员,如果基类中有很多子类无权访问的私有成员,可能就需要修改基类的实现了。但是,这又会违背开放封闭原则。

抽象方法

另外一种使用实现继承的更加灵活的扩展点是抽象方法。

客户端依赖抽象基类,因此提供任何一个具体子类(或者用来支持新需求的子类)给客户端都不会违背开放封闭原则。

接口继承

最后一个扩展点是实现继承外的另外一种选项:接口继承。客户端委托接口取代了客户端对类的依赖。

接口继承要比实现继承好很多。基于实现继承,所有子类(现有的和将来的)都是基类的客户端。给继承图顶部节点添加新成员的改动会影响到该层级结构下的所有成员,而接口要比类灵活的多。这当然不是说代表实现继承的虚方法和抽象方法提供的扩展点没有一点用处,但是它们的确无法提供与接口一样强大的自适应能力。

防止变异

虽然我们已经知道了实现扩展点的方式,但是我们应该到处都留着扩展点吗?防止变异是另外一个跟开放封闭原则相关的重要准则:

识别可预见的变化点并围绕它们创建一个稳定的接口。

可预见的变化

要识别出很可能发生变更的需求或者实现起来特别麻烦的代码部分,然后将它们隐藏在扩展点之后。

一个稳定的接口

依赖接口的最大优势是接口变化的可能性要比实现小很多。用于表达扩展点的所有接口应该都是稳定的。因为客户端是直接依赖接口的,如果接口发生变化,客户端也必须做相应的改动。

最后

通过确保代码对扩展开放对修改封闭,可以有效阻止后期变化对现有类的修改,因为后面的编码人员只能在你预留的扩展点上挂靠新创建的类。代码可以很死板,几乎无法扩展和细化;代码也可以很流畅,带有足够的准备应对新需求的大量扩展点。两种选择都没有错,只是要在具体的场景进行选择和应用。

原文发布于微信公众号 - 撸码那些事(lumanxs)

原文发表时间:2018-09-05

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏顶级程序员

总算搞清楚了回车和换行的来历与区别

总算搞清楚”回车”(carriage return)和”换行”(line feed)这两个概念的来历和区别了。 在计算机还没有出现之前,有一种叫做电传打字机(...

3015
来自专栏编程

设计模式启示录(二)

设计模式启示录(二) 在【设计模式启示录 (一)】中,重点介绍了设计模式的精髓(抽象),设计模式的分类(按抽象的目的进行分类)。在本篇中,将按照前述的七大分类,...

1797
来自专栏资深Tester

测试流程之如何设计测试用例

1983
来自专栏C/C++基础

C++异常处理的开销

C++异常是C++有别于C的一大特性 ,异常处理机制给开发人员处理程序中可能出现的意外错误带来了极大的方便,但为了实现异常,编译器会引入额外的数据结构与处理机制...

862
来自专栏我的技术专栏

C++设计模式:Template Method

1093
来自专栏华仔的技术笔记

译:如何用Swift进行TDD(测试驱动开发)

39211
来自专栏tkokof 的技术,小趣及杂念

Sweet Snippet 系列之 Lua表排序

  作为Lua中实现各类数据结构的基石,表的使用想必是贯穿于整个项目的开发过程之中,其中对表内容的排序想必亦是常见的需求之一,Lua内置的Table函数库便提供...

864
来自专栏python学习路

一、代码风格 1、假定你的代码需要维护2、保持一致性3、考虑对象在程序中存在的方式,尤其是那些带有数据的对象4、不要做重复工作5、让注释讲故事6、奥卡姆剃刀原则1、简洁的规则2、文档字符串3、空行4、

刚开始学的时候就要注意编码规范了,所以整理了一下,以便养成一个编码好习惯。不然以后真的不好改。 代码被读的次数远大于被写的次数。 作为一名程序员(使用任何语言)...

2425
来自专栏信数据得永生

JavaScript 编程精解 中文第三版 八、Bug 和错误

31010
来自专栏西枫里博客

Python学习笔记一(Hello World)

2017年年终确定的从2018年开始学习一门新的语言。随着机器学习人工智能的日渐深入,是时候有必要掌握以下Python了。博客从今天开始会陆续更新下Python...

994

扫码关注云+社区

领取腾讯云代金券