原文: https://howtodoinjava.com/design-patterns/open-closed-principle/
开闭原则(OCP)指出,模块应该可以扩展,但可以关闭以进行修改。 它是著名的 5 条实体原则和非常重要的面向对象设计原则之一。
1.开闭原则的定义
有两种流行的定义来描述这一原则:
1.1 Mayer 的定义
伯特兰·梅耶(Bertrand Mayer)在其 1988 年的著作面向对象的软件构造(Prentice Hall)中,定义了开闭原则(OCP)如下:
软件实体应为扩展而开放,但应为修改而封闭。
1.2 Martin 的定义
Robert C. Martin 在他的书《敏捷软件开发:原则,模式和实践》(Prentice Hall,2003 年)中定义了 OCP,如下所示:
开放扩展 – 这意味着可以扩展模块的行为。 随着应用需求的变化,我们能够通过满足这些变化的新行为来扩展模块。 换句话说,我们能够更改模块的功能。
封闭修改 – 扩展模块的行为不会导致模块的源代码或二进制代码更改。 模块的二进制可执行版本,无论是在可链接库,DLL 还是 Java .jar 中,都保持不变。
2.讨论
根据上面引用的定义,对于要开放扩展的代码,开发人员必须能够响应不断变化的需求并支持新功能。 尽管模块无法修改,但仍必须做到这一点。 开发人员必须支持新功能,而无需编辑源代码或现有模块的已编译程序集。
2.1 例外情况
请注意,在少数情况下,绝对有必要修改代码,并且无法避免。
这样的例子之一就是模块中存在的缺陷。 在修复缺陷的情况下,允许更改模块代码及其相应的测试用例。
我们可以使用 TDD 方法来解决代码中的这些问题。 修正错误后,您必须确保没有其他测试会因副作用而失败。另一个允许的例外情况是,允许对现有代码进行任何更改,只要它也不需要更改该代码的任何客户端即可。 这允许使用新的语言功能升级模块版本。 例如,Spring 5 支持并使用 Java8 lambda 语法,但要使用它,我们不需要更改我们的客户端应用代码。
一个模块,其中的类是松散耦合的,在不强迫其他类更改的情况下更改了类,这就是鼓励松散耦合的原因。 如果您允许对现有代码进行修改而不会强制对客户端进行进一步更改,则保持松散的耦合将限制 OCP 的影响。
2.2 如何设计开闭原则
为了实现模块的扩展,我们可以采用两种(通常使用的)机制中的任何一种。
2.2.1 实现继承
实现继承使用抽象类和方法。 您可以将扩展点定义为抽象方法。
该抽象类可以由几个具有大多数常见场景的预定义实现的类扩展。 对于特定于客户的场景,开发人员必须扩展抽象类并提供特定的实现逻辑。 这将有助于保留 OCP。
模板方法模式非常适合这些用例。 在这种模式下,由于可以委托抽象方法,因此可以自定义一般步骤。 实际上,基类将流程的各个步骤委托给子类。
2.2.2 接口继承
在接口继承中,客户端对类的依赖关系被替换为接口。 与抽象方法相比,这实际上是首选方法。 这呼应了这样的建议,即首选组合而不是继承,并保持继承层次结构较浅,并且子分类层很少。
设计继承或禁止继承。 – Effective Java(Addison-Wesley,2008 年),约书亚·布洛赫(Joshua Bloch)
3.开闭原则示例
如果要查看开闭原则的真实示例,只需看一下 Spring 框架即可。 Spring 的设计和实现非常精美,因此您可以扩展其功能的任何部分,并立即将自定义实现注入。 它经过了很好的时间测试,并且像今天一样完美无缺。
3.1 没有 OCP 的计算器程序
假设我们正在创建一个简单的计算器模块,其中仅包含两个加减运算。 该模块的代码如下。
public interface IOperation {
}
public class Addition implements IOperation
{
private double firstOperand;
private double secondOperand;
private double result = 0.0;
public Addition(double firstOperand, double secondOperand) {
this.firstOperand = firstOperand;
this.secondOperand = secondOperand;
}
//Setters and getters
}
public class Substraction implements IOperation
{
private double firstOperand;
private double secondOperand;
private double result = 0.0;
public Substraction(double firstOperand, double secondOperand) {
this.firstOperand = firstOperand;
this.secondOperand = secondOperand;
}
//Setters and getters
}
public interface ICalculator {
void calculate(IOperation operation);
}
public class SimpleCalculator implements ICalculator
{
@Override
public void calculate(IOperation operation)
{
if(operation == null) {
throw new InvalidParameterException("Some message");
}
if(operation instanceof Addition) {
Addition obj = (Addition) operation;
obj.setResult(obj.getFirstOperand() + obj.getSecondOperand());
} else if(operation instanceof Substraction) {
Addition obj = (Addition) operation;
obj.setResult(obj.getFirstOperand() - obj.getSecondOperand());
}
}
}
上面的模块代码看起来不错并达到目的。 但是,在客户端应用中时,开发人员想要增加乘法功能 – 除了更改方法calculate()
中的SimpleCalculator
类代码外,他别无选择。 此代码不符合 OCP。
3.2 符合 OCP 的代码
请记住,抽象功能是应用中的变化。 在此计算器程序中,calculate
方法中的代码将随每个传入的新操作支持请求而变化。 因此,我们需要在此方法中添加抽象。
解决方案是委派在操作本身内部提供计算逻辑的责任。 每个操作必须具有自己的逻辑才能获取结果和操作数。 现在查看修改后的代码。
public interface IOperation {
void performOperation();
}
public class Addition implements IOperation
{
private double firstOperand;
private double secondOperand;
private double result = 0.0;
public Addition(double firstOperand, double secondOperand) {
this.firstOperand = firstOperand;
this.secondOperand = secondOperand;
}
//Setters and getters
@Override
public void performOperation() {
result = firstOperand + secondOperand;
}
}
public class Substraction implements IOperation
{
private double firstOperand;
private double secondOperand;
private double result = 0.0;
public Substraction(double firstOperand, double secondOperand) {
this.firstOperand = firstOperand;
this.secondOperand = secondOperand;
}
//Setters and getters
@Override
public void performOperation() {
result = firstOperand - secondOperand;
}
}
public interface ICalculator {
void calculate(IOperation operation);
}
public class SimpleCalculator implements ICalculator
{
@Override
public void calculate(IOperation operation)
{
if(operation == null) {
throw new InvalidParameterException("Some message");
}
operation.performOperation();
}
}
现在,我们可以根据需要添加任意数量的操作,而无需更改原始模块代码。 任何新的操作都将轻松实现。 例如,乘法运算将这样写,并且可以正常工作。
public class Multiplication implements IOperation
{
private double firstOperand;
private double secondOperand;
private double result = 0.0;
public Multiplication(double firstOperand, double secondOperand) {
this.firstOperand = firstOperand;
this.secondOperand = secondOperand;
}
//Setters and getters
@Override
public void performOperation() {
result = firstOperand * secondOperand;
}
}
4. 结论
开闭原则是类和接口的总体设计以及开发人员如何构建允许随时间变化的代码的指南。
现在,当大多数组织都在采用敏捷实践时,每次经过 sprint 时,新的需求都是不可避免的,应该被接受。 如果您生成的代码不是为了进行更改而构建的,则更改将很困难,耗时,容易出错且成本很高。
通过确保代码可以扩展但不能修改,可以有效地禁止将来对现有类和程序集进行更改,这迫使程序员创建可以插入扩展点的新类。
建议的方法是确定需求中可能更改的部分或难以实现的部分,并将这些部分排除在扩展点之后。
学习愉快!
阅读更多: