目录
一、什么是开闭原则?
相信很多人都听说过这个原则,即使没有直接了解过“开闭原则”,也一定听过“对扩展开放,对修改关闭”这句话。这句话简洁地概括了开闭原则的核心思想。也即是,添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。
开闭原则(Open Closed Principle, OCP),它的英文表述是:software entities (modules, classes, functions, etc.) should be open for extension but closed for modification。翻译成中文,意思就是:软件实体(如模块、类、方法等)应当对扩展开放,而对于修改则应保持关闭。这也就是我们上面说的“对扩展开发,对修改关闭”。我们应当使系统更容易通过添加新代码来扩展其功能,而不是通过修改现有的代码来实现变化。这样不仅能够提高系统的灵活性和可维护性,还能有效减少因频繁更改原有代码导致引入错误的风险。
二、如何做到开闭原则?
1、面向接口或抽象类编程
面向接口或抽象类编程,这意味着我们应该尽量使用接口或抽象类,而不是具体的实现类。这样做的好处在于,我们可以在不修改现有代码的情况下,通过添加新的实现类来扩展新功能。此外,当我们需要替换接口方法的具体实现逻辑时,只需替换接口对应的引用对象即可,不需要修改引用和使用引用的地方。这样便提高了代码的可扩展性,并且在需要修改代码的时候,也可以做到尽可能少的修改。
假设我们有一个支付系统,需要支持多种支付方式(如信用卡支付、微信支付等)。我们可以定义一个支付接口 Payment
,然后为每种支付方式创建具体的实现类。
首先,定义一个支付接口:
//支付接口
public interface Payment {
void pay(double amount);
}
其次,为不同的支付方式创建具体的实现类:
// 信用卡支付
public class CreditCardPayment implements Payment {
@Override
public void pay(double amount) {
System.out.println("Paid " + amount + " using Credit Card.");
// 实际的信用卡支付逻辑
}
}
// 微信支付
public class WeChatPayment implements Payment {
@Override
public void pay(double amount) {
System.out.println("Paid " + amount + " using WeChat.");
// 实际的WeChat支付逻辑
}
}
最后,我们可以编写一个方法来处理支付,而无需关心具体的支付方式。
// 支付类
public class PaymentProcessor {
private Payment payment;
public PaymentProcessor(Payment payment) {
this.payment = payment;
}
public void processPayment(double amount) {
payment.pay(amount);
}
}
那么,此时,我们在客户端代码的时候,就可以灵活的选择不同的支付方式。
public class Main {
public static void main(String[] args) {
//根据指向的具体实现选择对应的支付方式,比如此时我们想要改成微信支付,就把
//CreditCardPayment换成WeChatPayment
Payment payment = new CreditCardPayment();
PaymentProcessor processor = new PaymentProcessor(payment);
processor.processPayment(100.0);
}
}
如果说此时,我们又需要支持支付宝支付,则此时,只需创建一个新的实现类ALiPayment
并实现 Payment
接口,并更改Payment
接口引用指向的对象为ALiPayment对象即可
。
那么,此时,肯定会有人说,这不是还是修改了原有代码吗?这不也是不满足开闭原则吗?
就像有句话说的好,水至清则无鱼,人至察则无徒。如果我们总是用放大镜来审视代码,那么很难找到完全满足开闭原则(OCP)的代码。让我们重新审视OCP的定义:软件实体(模块、类、函数等)应该对扩展开放,但对修改关闭。如果站在一个模块的角度来看,我们在这个模块内所做的所有修改似乎都违背了OCP。因此,判断是否满足开闭原则也需要看我们是从哪个角度来看待这个问题。
并且,我们也应该认识到添加一个新功能时,不可能完全不修改任何模块、类或方法的代码。类的创建、组装和初始化操作是构建可运行程序所必需的,这部分代码的修改是不可避免的。然而,我们的目标是尽量将修改操作集中化、减少修改的范围,并使其更上层化。这也就是说我们应该尽量让最核心、最复杂的那部分逻辑代码满足OCP。
2、依赖注入
提到依赖注入大家应该也都不陌生。因为,我们在学习spring框架的时候都会接触这个概念。依赖注入(Dependency Injection,简称DI)是一种设计模式,用于实现控制反转(Inversion of Control,简称IoC)。在传统的编程中,对象的创建和管理通常由对象自身负责,这会导致代码的耦合度较高,难以测试和维护。依赖注入通过将对象的依赖关系从对象内部转移到外部容器或框架来管理,从而降低了耦合度,提高了代码的灵活性和可维护性。
在我们上面举的示例中,PaymentProcessor
类中的支付方式接口 Payment
的具体实现便是通过DI来创建的,而不是在 PaymentProcessor
类内部自行创建。这正是依赖注入的核心所在:它允许我们灵活地控制对象的创建过程。这种设计不仅增强了代码的扩展性和灵活性,还使得我们的代码能够更好地遵循OCP。通过依赖注入,我们可以在不修改现有代码的情况下,轻松地引入新的支付方式实现,从而提升系统的适应性和可维护性。
3、单一职责原则
其实这个不难理解。当我们编写代码时,应尽量使每个类或方法都遵循单一职责原则。这样做的好处是,当需求发生变化时,我们只需修改尽可能少的代码,从而更容易满足OCP。其实,这也说明了很多设计原则、思想、模式都是相通的。其实,为了更好地遵循开闭原则,我们需要深入学习更多的设计模式思想。这些设计模式的核心理念正是在于实现对扩展开放,对修改关闭的原则。
三、是不是为了满足开闭原则就要一味的追求代码的扩展性?
显然不是!还是那句话,过犹不及。在我们为了OCP,一味的追求扩展性的时候,一定会使代码失去可读性,而可读性往往就会影响代码的可维护性。所以说,我们需要在可读性和可扩展性之间做一个平衡。在某些场景下,如果代码的扩展性至关重要,我们就可以适度牺牲一些可读性;而在另一些场景下,如果代码的可读性更为关键,我们则可以适当地牺牲一些扩展性。所以说,任何技术都应该在具体场景中进行讨论,脱离了实际应用场景来谈论技术是没有任何意义的!