Loading

敏捷开发-SOLID-开放与封闭原则

开放与封闭原则介绍

定义

Robert C.Martin定义的开放与封闭原则:

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

  • 开发人员必须能够响应需求变更并支持新的特性
  • 尽管模块对修改是封闭的。开发人员必须在不改动已有模块源代码或程序集的前提下支持新的功能

“对于修改是封闭的”一句也有两个例外:修复缺陷所做的改动以及客户端无法感知到的改动

缺陷修复

修复缺陷流程的两个步骤:

(1) 针对缺陷编写失败的单元测试和/或集成测试。

​ 前提条件是要有稳定的让代码失败的问题复现步骤。根据前面章节提到的单元测试的布置、动作和断言模式定义的语法,你需要能先布置
好测试目标系统以便让它处在能触发缺陷的状态,然后执行指定的包含缺陷的动作,最后对期望的行为进行断言。

​ 这种单元测试开始时总是失败的。这就说明了所有问题实际上是由缺乏测试引起的。

​ 如果有测试用于捕获这个缺陷,那么该测试会是失败的,当然,如果使用了持续集成系统,它也会报错。

(2) 修改后的源代码才可以通过单元测试。

​ 在这种特定情况下,违背开放与封闭原则的缺陷修复也是很有必要的,因为如果没有它,你将无法修改任何现有代码。

​ 通过修改测试目标系统的实现能让失败的单元测试由红色失败状态变为绿色成功状态。当你能确认没有产生任何副作用,也就是不会导致其他任何测试失败时,这个缺陷就已经被成功修复了。

客户端感知

​ 一个更加违背“对于修改是封闭的”规则的例外情况是:允许对现有代码做任意改动,只要它不会引起对任意客户端代码的改动。它着重强调软件模块在所有粒度级别上如何耦合关联,包括类之间、程序集之间以及子系统之间。

​ 如果一个类的改动会引起另外一个类的改动,那么这两个类就是紧密耦合的。相反,如果一个类的改动是独立的,并不会引起其他类的改动,那么这些类就是松散耦合的。

​ 任何情况下,松散耦合都比紧密耦合要好。如果你对现有代码的修改不会影响客户端代码,那么维护这样松散耦合的代码对开放与封闭原则的影响是有限的。

扩展点

没有扩展点的代码

如果没有扩展点,则会强制客户端进行更改
image

分析:

TradeProcessorClient类直接依赖TradeProcessor类。当你接到一个需要改动TradeProcessor类的新需求时,为了不改变原有类型,创建了一个新版本(TradeProcessorVersion2)来包含新需求提出的新功能。

​ 客户端直接依赖TradeProcessor类并且该类没有提供任何扩展点,因此你需要将新功能安排在新的类中。这种改动的副作用就是:必须改动TradeProcessorClient类,这样才能依赖新的TradeProcessorVersion2

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

虚方法

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

客户端依赖TradeProcessor类,该类可以通过继承进行扩展
image

任何带有一个虚方法成员的类都是对扩展开放的。这种扩展是通过实现继承做到的。

TradeProcessor类的新特性需求到来时,你可以修改其子类的ProcessTrades方法而无需改变原有的TradeProcessor类源代码。

然而,你能重新实现的范围是有一定限制的。在新的子类中,你依然能够访问基类,因此可以直接调用TradeProcessor类的ProcessTrades方法,但是无法改动该方法内的任何代码。

你要么在子类方法里调用基类同名方法并且在其前或后实现新的特性,要么你完全重新实现子类的方法。虚方法并没有中间状态。

记住,子类只能访问基类的受保护和公共成员。如果TradeProcessor类带有很多你无权访问的私有成员,你就需要修改该基类的实现了,当然,这样就会违背开放与封闭原则。

抽象方法

使用实现继承的更加灵活的扩展点是抽象方法

在这种情况下,TradeProcessor类是一个定义了公共ProcessTrades方法的抽象类,该方法会委托三个抽象的保护方法来完成交易处理算法的工作。客户端对这三个保护方法并不知情,因为它们都是没有具体实现的抽象方法。

抽象方法为将来的子类提供了扩展点
image

提供了两个版本的交易处理器。它们都从抽象基类中直接继承了ProcessTrades方法,也都为三个抽象方法提供了各自的实现。客户端依赖抽象基类,因此提供任何一个具体子类(或者用来支持新需求的新子类)给客户端都不会违背开放与封闭原则。

接口继承

最后一个扩展点是实现继承外的另外一种选项:接口继承

接口继承要比实现继承好很多。基于实现继承,所有子类(现有的和将来的)都是基类的客户端。这会影响后续的修改,因为子类也都是依赖基类实现的。

所有对实现的改动都会是客户端可能察觉到的改动。因此相对于继承,通常会建议优先选择组合,如果必须要使用继承,也要尽量使用只有少量分层的浅继承层次结构。给继承图顶部节点添加新成员的改动会影响到该层次结构下的所有成员。

接口也是一个比较好的扩展点,因为可以根据不同的上下文给接口修饰丰富的功能。

接口也是一个比较好的扩展点,因为可以根据不同的上下文给接口修饰丰富的功能。接口要比类灵活得多。虽然这并不代表类继承的虚方法和抽象方法提供的扩展点没有一点用处,但是它们的确无法提供与接口一样强大的自适应能力。

客户端依赖接口而不是类
image

“为继承设计或禁止继承”

为继承设计和撰写文档,或者禁止使用继承。

如果你选择使用实现继承作为扩展点,就必须恰当地设计该类并为此编写清楚的文档,以便让后续要扩展该类的编程人员清楚原始的设计。类的继承会比较复杂,因为新的子类会以一种不可预期的方式破坏现有代码。

切记,任何没有标记sealed关键字的类都提供了继承能力。类并不是必须要有虚方法或者抽象方法才能够派生子类。使用new关键字可以隐藏被继承的成员,但是这种方式会影响多态能力的使用,显然违背了我们的期望。

通过密封类可以清楚地告诉其他可能使用该类的编程人员:该类并不支持继承。这样他们就会重新寻找替代方案。

防止变异

跟开放与封闭原则相关的重要准则:防止变异(protected variation)

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

该原则本身叫作防止变异,而上面的定义引用了术语可预见的变化(predicted variation),这多少会让人产生一些疑惑。尽管如此,我脑海里中仍然认为“可预见的变化”会更加合适一些。

可预见的变化

  • 单个类的需求应该直接与客户端的一个业务需求关联起来。
  • 如果忽略了这种关联,这个类就很有可能不是在为客户端要求的业务目标服务的。
  • 冲刺过程中,开发人员从冲刺积压工作上去除用户故事,然后与产品负责人沟通故事相关的事情。
  • 此时,开发人员就可以提出有关将来的、潜在的相关需求。这样构造得到的可预见变化是可以直接转化为扩展点的。

一个稳定的接口

即使你只委托接口,客户端仍然依赖这些接口。

如果接口发生变化,客户端也必须做相应的改动。

依赖接口的最大优势是接口变化的可能性要比实现小很多

如果你按照阶梯模式将接口和实现分别组织在不同的程序集中,那么二者能够独立变化而不相互影响,而且实现的变动也不会影响到接口的客户端。

用于表达扩展点的所有接口应该是稳定的,接口改变的可能性和频率应该很低,否则你会需要通知所有客户端使用新版本的接口了。

足够的自适应能力

只在合适的位置上包含恰当数目的扩展点的代码也被称为代码的“宜居带”,它能适应代码后续的变更需求,同时又不会增加复杂度和过度设计方案。对于任何具体问题而言,代码的自适应能力要么不够,要么过度,要么正好。

总结

开放与封闭是一个面向类和接口设计的整体原则,它指导开发人员如何才能编写出很好地自适应变更的代码。每个冲刺都需要拥抱那些不期而至的新需求。然而,承认并接受变更只是解决问题的第一步。如果你的代码直到出现变更时才发现它并没有为变更做好准备,此时为了适应变更要做的工作会更困难、更费时、更易出错,代价也更高。

通过确保代码对扩展开放以及对修改封闭,你有效地阻止了后期变化对现有类和程序集的修改,因为后面的编码人员只能在你预留的扩展点上挂靠新创建的类。可用的扩展点主要有两种:实现继承接口继承。虚方法和抽象方法允许你创建子类来定制基类的方法。如果你选择将类委托给接口,那就可以应用很多优秀的模式来更加灵活地创建和使用扩展点。

尽管如此,只知道要在代码中预留扩展点是不够的,你还需要知道应用它们的恰当时机。防止变异的概念建议你先识别出很可能发生变更的需求或者实现起来特别麻烦的代码部分,然后将它们隐藏在扩展点之后。代码可以很死板,几乎无法扩展和细化;代码也可以很流畅,带有足够的准备应对新需求的大量扩展点。两种选择并没有对错,只是要根据具体的场景和上下文进行应用。

posted @ 2022-04-28 22:31  F(x)_King  阅读(80)  评论(0编辑  收藏  举报