原文地址:The Open-Closed principle made simple
原文作者:Mihai Sandu
译者:maybelence
原文的示例代码都是采用的 Scala
语言,其实 Scala
本身和 java
的语法非常像,考虑到目前更多的开发者都是 java ,因此我把原文的代码都转成了 java 。一些和 java 语法有冲突的地方也都做了中文注释。
卡车是一种多功能的设备。他们可以根据车厢挂载的零件类型来执行不同的任务,如果载重允许的话,甚至可以
装载多个车厢。
简而言之,卡车可以通过不同的零件以进行扩展,但不能改装每个零件的内部单元(例如发动机或机舱)。可拓展的代码也应该像卡车挂载零件一样得心应手。
制冰机问题
假设我们必须要编写一个程序来为 Ted&Kelly 公司制作可可冰激凌。
1 | public class IceCreamMachine |
我们将程序交付给客户,虽然这段代码很简单,但是看上去没有什么问题,而且没有违反任何原则。客户目前是非常满意的。
但是事情并不是到这里就结束了,客户总是会有一些新的需求。这就意味着我们需要不断调整我们的代码。
添加新的口味
没想到程序刚上线就如此火爆,于是 Ted&Kelly 希望扩大规模。他们要求我们还要能够生产香草味的冰激凌。
短暂思考之后,我们给出了以下解决方案:
1 | public class IceCreamMachine |
我们修改了最初的代码,添加了一个参数以指定所需的口味,并添加了 if
语句以在逻辑之间切换。由于我们已经改变了最初的函数签名,因此调用我们代码的方法将被破坏,但是至少从现在开始,我们应该在不破坏更改的情况下支持其他口味的生产。
创建可可和香草组合
业务进展顺利,但是很快客户就提出了第二个需求,希望我们能够生产由可可和香草制成的冰淇淋。事情开始变得有点复杂,但是我们仍然能够处理。
1 | public class IceCreamMachine |
我们又添加了一个 if
语句,在这种情况下,生产冰淇淋的逻辑被复制到每个 if
内部。在实际的应用程序中,我可能会在单独的服务中提取生产的逻辑。但是,正如我们将看到的那样,提取服务并不总是最好的方案。
当 Ted&Kelly 要求支持生产更多口味时,会发生什么。如果他想进一步将它们结合起来怎么办?仅添加 if
子句并不是理想的解决方案。
这种解决方案产生的问题
每次我们添加新的口味或组合时,我们都必须更新 IceCreamMachine
类,这就意味着:
- 我们更新已经部署的代码
- 这个类变得越来越复杂,易读性变得越来越差。假设我们有 100 种口味,这个类很容易膨胀到 5000+ 行代码
- 我们可能会破坏现有的单元测试
现在回想一下开头卡车的类比,当你每次更换车厢挂载的零件时,你会每次都更换引擎吗?显然不会,看我们如何解决这个问题。
传统的可拓展性方法
Bertrand Meyer 是最早提出开闭原则这一术语的人,他定义 “软件实体(类,模块,函数,等) 应当对扩展开放,对修改关闭。”
换句话说,每当我们需要向旧对象添加新行为时,都可以根据需要继承和更新它们。开闭原则是那些易于理解却难以应用的原则之一。
让我们遵循这种方法重写刚刚的代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26public abstract class BaseIceCream {
public abstract void MakeIceCream();
}
public class CacaoIceCream extends BaseIceCream {
public void MakeIceCream() {
System.out.println("Cacao ice-cream");
}
}
public class VanillaIceCream extends BaseIceCream {
public void MakeIceCream() {
System.out.println("Vanilla ice-cream");
}
}
public class CacaoAndVanilla extends CacaoIceCream {
public void MakeIceCream() {
super.MakeIceCream();
System.out.println("Vanilla ice-cream"); //duplicate vanilla logic because we can't iherit both cacao and vanilla
}
}
原始类分为了四个类,每个类代表一种生产口味。通过此解决方案,我们解决了所有最初的问题:
- 每个类都短小精悍,可维护性和易读性都很高
- 当需要添加新口味时,我们只需要添加一个新的类
- 不会影响现有的单元测试
代码看起来已经没有任何问题了,我们完全可以到这里就停止思考,但是其实这种解决方案还是存在一些问题的:
- 不能继承多个类,因此对于两种口味的结合,我们必须复制一些代码或将逻辑提取到服务中
- 如果基类中的代码被更新,所有子类都会受到影响。假设基类通过构造函数注入了一些依赖关系,每当我们添加新的依赖关系时,所有子代都必须将该参数解析为基本构造函数
现在可拓展性的方法
当 Robert C. Martin 重申 Meyer 的开闭原则时,他做了一些更新。偏爱继承而不是继承。
组合对象时,我们可以自由地随意组合任意数量和所需的组合。而且,如果我们针对抽象类(接口)进行编程,则可以修改已有代码的行为而无需进行修改代码。让我们看看最终的解决方案:
1 | public interface IMakeIceCream { |
现在将原始的类分成了三个对象:
IMakeIceCream
接口定义了制作冰淇淋的通用抽象CacaoIceCream
实现了IMakeIceCream
VanillaIceCream
实现了IMakeIceCream
通过使用接口,我们将类和实现解耦。接口是封闭不可修改的,因此一旦我们定义了对象,就无法更改。但是我们可以为新的功能定义新接口并继承它们。这使得代码可扩展。
为什么要向每个构造函数添加 IMakeIceCream
参数?新代码是否具有旧方法或旧方法的所有功能?
答案是肯定的。可可香草组合仍在这里,但我们不需要 if 子句或专门针对它的类。我们可以利用构造函数参数。类似这样:
1 | var cacaoVanillaIceCream = new CacaoIceCream(new VanillaIceCream()); |
组合的好处在于,我们可以根据需要合成任意数量的对象。需要 4 种口味?只需编写一个构造函数链即可。这称为装饰器模式。你可以在 这里 读更多关于它的内容。
请注意, IMakeIceCream
参数是可选的。这使我可以组合使用或单独使用该类。
像这样编写代码会实现一个可拔插式的体系结构。这是非常 nice 的,因为我们可以通过编写代码来添加新功能,而不更改现有功能。任务完成。
...
...
Copyright by @maybelence.