开闭原则:正确扩展实体

在SOLID首字母缩略词“封闭式原理”中包含的所有设计原理中,可能是最难理解且最经常破解的。 毕竟,如果您想向实体中添加功能,为什么不直接将其添加到实体的现有实现中呢? 作为新手程序员,我肯定会感到这种感觉。 本文的目的是首先讨论“开放式封闭原则”背后的原理,然后研究三种正确扩展实体的技术。

什么是开放式封闭原则,为何如此重要?

让我们从Bertrand Meyer最初编写的定义开始:

软件实体(类,模块,功能等)应打开以进行扩展,但应关闭以进行修改

本质上,我们是在谈论以某种扩展方式向实体添加任何新属性或方法,而不是将它们直接放入实体中(然后进行修改)。 那是有道理的,但是为什么我们要在这个麻烦上走? 这是一些原因…

遏制错误的引入

假设我正在编写一个供第三方使用的软件包,并决定要向现有实体添加一些新功能。 从表面上看,这似乎很好。 但是,当我进行这些更改时,我并未考虑系统中其他实体与我刚刚修改的实体进行交互的所有不同方式。 我的修改会产生意想不到的影响,导致系统其他部分出现错误吗? 通常,我们希望逐步开发我们现有的代码,以使其尽可能高效且无错误。 在此过程中引入新的属性或方法可能会取消数月甚至数年的迭代改进。

单独关注

当我向现有实体介绍新功能时,很可能会打破SOLID原则中的不止一项。 如果新功能使我的实体超出其原始用途的范围,我还将打破“单一责任原则”。 为了代码可维护性,我应该保留原始实现,并使用其他方法对其进行扩展。

设计更好的实体

在采用开放式封闭原则的所有原因中,最重要的是它鼓励我们通过使用泛型和面向协议的编程(POP)原则来创建具有良好抽象平衡的实体。 试想一下,如果我知道我不能回到自己的实体来更改类型,那么我将非常谨慎地以能够提供最大灵活性的方式进行设计。 结果是我们最终可以编写更少的代码,并使我们编写的代码适用于更多情况。 简而言之,我们编码更好。

显然,这里可能会有更多的讨论,但是为了继续学习一些有用的技术,让我们考虑最后一个大问题……

我应该严格遵循开放式原则吗?

遵守开放式封闭原则是一个主观选择,开发人员通常在何时扩展实体上有不同的看法。 毕竟,您的实体可能被认为是“完成”或“进行中的工作”,最终您作为开发者是做出这种区分的仲裁者。 以我的经验,如果您决定实体需要您想要实现的功能来履行其单一职责,那么请继续添加它们(无罪感!)。 否则,这里有一些有关如何正确扩展它们的想法。

扩展技术

在下一部分中,我们将介绍三种扩展技术并对其进行批判。 事不宜迟,让我们熟悉在以下示例中将要使用的惊人实体:

是的,这真是好人。

技术#1:协议扩展(OK)

假设我希望我的“ Thingy”能够摆动(不要问我为什么……)。 我的第一个直觉可能是这样做:

显然,这违反了我们的开放式封闭原则。 因此,一种方法可能是创建一个声明该方法的协议,然后将其实现放入我们的struct的扩展中:

很好。 请注意,方法的声明发生在协议中(第9行),其实现发生在实体的扩展中(第15行)。 我们可能考虑的一种变体是将方法的实现放入协议扩展中。 这将使我们能够在符合协议的任何实体上使用相同的方法实现。 看起来像这样:

这里要注意的最大区别是,我们的摆动方法的声明和实现现在都在协议的扩展中(第9-15行)。 当实体符合协议时(第17行),现在无需实现该方法。

因此,通过第一组示例,我们从技术上扩展了我们的实体,而没有对其进行修改,但是请注意! 首先,这种技术仅适用于方法,不适用于属性,因为我们无法在扩展中存储任何内容。 其次,也许还有一个更大(但不太明显)的缺点是,我们不再能够像添加摆动方法之前那样使用“ Thingy”的原始实现。

技术#2:类继承(OK)

现在,我要给“ Thingy”一个size属性。 一种常见的方法是使用类继承。

在这种情况下,派生类称为“ SizedThingy”,它从基类“ Thingy”(第17行)继承name属性。 这样做的好处是我们仍然可以使用未修改的基类。 与我们的第一个技术相比,这是一个巨大的进步,所有关于关注点分离的担忧都可以得到满足。 但是,这仍然不是最好的方法,因为当我们可能需要值类型(结构)时,它迫使我们在系统(类)中使用引用类型。 类的分配也更加昂贵,并且当存在大量继承和重写时,动态调度会变得相当混乱和混乱。 回到绘图板…

技术#3:装饰器模式(最佳)

最好的方法是使用Decorator模式,该模式充当我们现有实体的包装,并在其中实现其他属性和方法。 我们可以提供与基础实体相同的接口,并对其进行调用。 这是带有静态实例化的实现:

在此示例中,我们用协议“ Thing”描述接口,然后在我们的实体“ Thingy”(第9行)中将其遵循。 您会注意到该协议具有一个初始化程序(第3行),并且我们实体中的name变量具有默认值“ Thingy”(第11行),因为我们希望在初始化装饰器(第52行)时静态地传入类型,让它初始化自己,成为装饰器中的基础实体(第34行)。 为了访问基础实体内的name属性,同时为客户提供统一的接口,我们可以使用计算属性(第25–32行)。 为了将新的属性“ size”添加到装饰器中,我们创建了一个“ Sized”协议(第15行),然后让装饰器遵循它(第21行)。

与我们先前的类继承示例相比,这是一个重大改进,因为我们可以保留我们的值类型。 但是,如果我们要求初始化更加动态,则需要进行一些修改…

变体:动态装饰器模式

这再次是相同的示例,但是这次使用了动态初始化程序:

主要区别在于协议“ Thing”不再需要初始化程序,因此我们的实体“ Thingy”不再需要默认值(第9行)。 现在,我们可以使用依赖项注入来创建装饰器。 您会注意到,装饰器的初始化程序(第34行)现在有两个输入参数,第一个是我们的基础实体。 还要注意,输入类型是“ Thing”(协议)而不是“ Thingy”(struct)。 这意味着我们有一个很好的抽象层。

包起来

在本文中,我们讨论了开放式封闭原则,它是什么以及为什么应遵守该原则,并探讨了扩展实体而不直接修改它们的三种技术。 希望本文中对这些技术的批评将有助于您在扩展现有实体时做出自己的决策。 装饰器模式已成为我编码的主要内容,并且具有静态和动态初始化选项,通常可以找到满足我需要的解决方案。

我希望与您保持联系的另一个重要点是,遵循开放式封闭原则的最大好处是,它鼓励我们创建具有适当抽象平衡的实体,以提高实用性并最终编写更少的代码。 在我的新文章“用协议掌握泛型:规范模式”中探讨了这个想法。 感谢您的阅读!