开闭原则--设计模式


今天,我们来学习 SOLID 中的第二个原则:开闭原则。开闭原则是 SOLID 中最难理
解、最难掌握,同时也是最有用的一条原则。

开闭原则的英文全称是 Open Closed Principle,简写为 OCP。它的英文描述是:
software entities (modules, classes, functions, etc.) should be open for
extension , but closed for modification。我们把它翻译成中文就是:软件实体(模
块、类、方法等)应该“对扩展开放、对修改关闭”。
说人话就是,当我们需要添加一个新的功能时,应该在已有代码基础上扩展代码(新
增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。
以下是一个常见的生产环境中的例子,我们将展示一个简化的电商平台的订单折扣策
略。
你觉得下边的代码有问题吗?

class Order {
private double totalAmount;
public Order(double totalAmount) {
this.totalAmount = totalAmount;
}
// 计算折扣后的金额
public double getDiscountedAmount(String discountType) {
double discountedAmount = totalAmount;
if (discountType.equals("FESTIVAL")) {
discountedAmount = totalAmount * 0.9; // 节日折扣,9折
上述代码中, Order 类包含一个计算折扣金额的方法,它根据不同的折扣类型应用
折扣。当我们需要添加新的折扣类型时,就不得不需要修改
getDiscountedAmount 方法的代码,这显然是不合理的,这就违反了开闭原则。
遵循开闭原则的代码:
} else if (discountType.equals("SEASONAL")) {
discountedAmount = totalAmount * 0.8; // 季节折扣,8折
}
return discountedAmount;
}
}

上述代码中, Order 类包含一个计算折扣金额的方法,它根据不同的折扣类型应用
折扣。当我们需要添加新的折扣类型时,就不得不需要修改
getDiscountedAmount 方法的代码,这显然是不合理的,这就违反了开闭原则。
遵循开闭原则的代码:

这样的话对我们在实现了相关的功能之后对于后期进行相关的维护的时候,是十分的不方便的之后我们在后期进行相关的功能的进行相应的添加的时候,这个时候对于我们这个类中所实现的相关的方法就变的越来越庞大,使得我们这个代码的可读性和可修改性变得十分的糟糕

// 抽象折扣策略接口
interface DiscountStrategy {
double getDiscountedAmount(double totalAmount);
}
// 节日折扣策略
class FestivalDiscountStrategy implements DiscountStrategy {
@Override
public double getDiscountedAmount(double totalAmount) {
return totalAmount * 0.9; // 9折
}
}
// 季节折扣策略
class SeasonalDiscountStrategy implements DiscountStrategy {
@Override
public double getDiscountedAmount(double totalAmount) {
return totalAmount * 0.8; // 8折
}
}
class Order {
private double totalAmount;
private DiscountStrategy discountStrategy;
public Order(double totalAmount, DiscountStrategy discountStrategy) {
this.totalAmount = totalAmount;
this.discountStrategy = discountStrategy;
}

其实这样的代码把相关的实现功能做成了一个个的相关的接口,这样就会使得我们的方法在街口调用之后进行这个生成和重写,更加灵活

在遵循开闭原则的代码中,我们定义了一个抽象的折扣策略接口
DiscountStrategy ,然后为每种折扣类型创建了一个实现该接口的策略类。
Order 类使用组合的方式,包含一个 DiscountStrategy 类型的成员变量,以便
在运行时设置或更改折扣策略,(可以通过编码,配置、依赖注入等形式)。这样,
当我们需要添加新的折扣类型时,只需实现 DiscountStrategy 接口即可,而无需
修改现有的 Order 代码。这个例子遵循了开闭原则。

遵循开闭原则的代码:

 面向接口编程!!!

在开发实现相关的代码的过程中,我们可以去使用这个ArrayList去实现我们的相关的功能,但是为什么我编写代码的习惯却是使用

List<> list = new ArrayList<>();

这种方式去实现呢,这种方式就是我们的面向接口的相关的思想

2、修改代码就意味着违背开闭原则吗
开闭原则的核心思想是要尽量减少对现有代码的修改,以降低修改带来的风险和影
响。在实际开发过程中,完全不修改代码是不现实的。当需求变更或者发现代码中的
错误时,修改代码是正常的。然而,开闭原则鼓励我们通过设计更好的代码结构,使
得在添加新功能或者扩展系统时,尽量减少对现有代码的修改。
以下是一个简化的日志记录器的示例,展示了在适当情况下修改代码,也不违背开闭
原则。在这个例子中,我们的应用程序支持将日志输出到控制台和文件。假设我们需
要添加一个新功能,以便在输出日志时同时添加一个时间戳。
原始代码:
public void setDiscountStrategy(DiscountStrategy discountStrategy) {
this.discountStrategy = discountStrategy;
}
// 计算折扣后的金额
public double getDiscountedAmount() {
return discountStrategy.getDiscountedAmount(totalAmount);
}
}
interface Logger {
void log(String message);
}
class ConsoleLogger implements Logger {
@Override
public void log(String message) {
System.out.println("Console: " + message);

}
}
class FileLogger implements Logger {
@Override
public void log(String message) {
System.out.println("File: " + message);
// 将日志写入文件的实现省略
}
}

为了添加时间戳功能,我们需要修改现有的 ConsoleLogger 和 FileLogger 类。
虽然我们需要修改代码,但由于这是对现有功能的改进,而不是添加新的功能,所以
这种修改是可以接受的,不违背开闭原则。
修改后的代码:
}
}
class FileLogger implements Logger {
@Override
public void log(String message) {
System.out.println("File: " + message);
// 将日志写入文件的实现省略
}
}


}
}
class FileLogger implements Logger {
@Override
public void log(String message) {
System.out.println("File: " + message);
// 将日志写入文件的实现省略
}
}
interface Logger {
void log(String message);
}
class ConsoleLogger implements Logger {
@Override
public void log(String message) {
String timestamp =
LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
System.out.println("Console [" + timestamp + "]: " + message);
}
}
class FileLogger implements Logger {
@Override
public void log(String message) {
String timestamp =
LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
String logMessage = "File [" + timestamp + "]: " + message;
System.out.println(logMessage);
// 将日志写入文件的实现省略
}
}

在这个例子中,我们只是对现有的日志记录器类进行了适当的修改,以添加时间戳功
能。这种修改不会影响到其他部分的代码,因此不违背开闭原则。总之,适当的修改
代码并不一定违背开闭原则,关键在于我们如何权衡修改的影响和代码设计。
当我们遵循开闭原则时,其目的是为了让我们的代码更容易维护、更具可复用性,同
时降低了引入新缺陷的风险。但是,在某些情况下,遵循开闭原则可能会导致过度设
计,增加代码的复杂性。因此,在实际开发中,我们应该根据实际需求和预期的变化
来平衡遵循开闭原则的程度。写代码不是为了设计而设计,脱离需求谈设计都是耍流
氓,有些场景,比如项目的使用频率不高,修改的可能性很低,或者代码本来就很简
单使用了设计模式可能会增加开发难度,提升开发成本,反而得不偿失。

3、如何做到“对扩展开放、修改关闭”
开闭原则讲的就是代码的扩展性问题,是判断一段代码是否易扩展的“黄金标准”。如
果某段代码在应对未来需求变化的时候,能够做到“对扩展开放、对修改关闭”,那就
说明这段代码的扩展性比较好。事实上,我学习设计模式的很重要的一个目的就是写
出扩展性好的代码。
在讲具体的方法论之前,我们先来看一些更加偏向顶层的指导思想。为了尽量写出扩
展性好的代码,我们要时刻具备扩展意识、抽象意识、封装意识。这些“潜意识”可能
比任何开发技巧都重要。
如果我们给自己的定位是“工程师”,而非码农,那我们在写任何一段代码时都应该思
考一些问题:
我要写的这段代码未来可能有哪些需求变更、如何设计代码结构,事先留好扩展
点,以便在未来需求变更的时候,不需要改动代码整体结构、做到最小代码改动
的情况下,新的代码能够很灵活地插入到扩展点上,做到“对扩展开放、对修改
关闭”。
我们还要识别出代码可变部分和不可变部分,要将可变部分封装起来,隔离变
化,提供抽象化的不可变接口,给上层系统使用。当具体的实现发生变化的时
候,我们只需要基于相同的抽象接口,扩展一个新的实现,替换掉老的实现即
可,上游系统的代码几乎不需要修改。
聊完了如何思考,我们可以采用以下策略和设计模式:
1. 抽象与封装:通过定义接口或抽象类来封装变化的部分,将共性行为抽象出来。
当需要添加新功能时,只需要实现接口或继承抽象类,而不需要修改现有代码。
2. 组合/聚合:使用组合或聚合的方式,将多个不同功能模块组合在一起,形成一
个更大的系统。当需要扩展功能时,只需要添加新的组件,而不需要修改现有的

组件。
3. 使用依赖注入:
4. 使用设计模式:设计模式是针对某些特定问题的通用解决方案。很多设计模式都
是为了支持“对扩展开放、修改关闭”的原则。例如,策略模式、工厂模式、装饰
器模式等,都是为了实现这个原则。
5. 使用事件和回调:通过事件驱动和回调函数,可以让系统在运行时根据需要动态
地添加或修改功能,而无需修改现有代码。
6. 使用插件机制:通过插件机制,可以允许第三方开发者为系统添加新功能,而无
需修改系统的核心代码。这种机制常用于框架和大型软件系统中。
需要注意的是,遵循开闭原则并不意味着永远不能修改代码。在实际开发过程中,完
全不修改代码是不现实的。开闭原则的目标是要尽量降低修改代码带来的风险和影
响,提高代码的可维护性和可复用性。在实际开发中,我们应该根据项目需求和预期
的变化来平衡遵循开闭原则的程度。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值