编码最佳实践——开放封闭原则
2019-11-19

开放封闭原则定义

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

Meyer的定义

软件实体应该允许扩展,但禁止修改

​ ——《面向对象软件构造》

Martin的定义

”对于扩展是开放的。“ 这意味着模块的行为是可以扩展的。当应用程序的需求改变时,我们可以对其模块进行扩展,使其具有满足那些需求变更的新行为。换句话说,我们可以改变模块的功能。

“对于修改是封闭的。“ 对模块行为进行扩展时,不必改动该模块的源代码或二进制代码。模块的二进制可执行版本,无论是可链接的库、DLL或Java的.jar文件,都无需改动。

​ ——《敏捷软件开发:原则、模式与实践》

对于修改是封闭的

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

1.修复缺陷所做的改动

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

缺陷修复

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

客户端感知

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

对于扩展是开放的

扩展点

没有扩展点

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

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

虚方法

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

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

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

抽象方法

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

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

接口继承

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

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

防止变异

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

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

可预见的变化

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

一个稳定的接口

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

最后

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

参考

《C#敏捷开发实践》

作者:CoderFocus

微信公众号:

声明:本文为博主学习感悟总结,水平有限,如果不当,欢迎指正。如果您认为还不错,不妨点击一下下方的推荐按钮,谢谢支持。转载与引用请注明作者及出处。

, 1, 0, 9);