SOLID-开闭原则

单一职责原则:https://blog.csdn.net/dmk877/article/details/143447010

在前面我们学习了单一职责原则,今天来一起学习一下SOLID原则中的开闭原则(Open-Closed Principle, OCP)

通过本篇博客你将学到到以下内容

①什么是开闭原则

②如何实现开闭原则

③两个开闭原则的案例

一、什么是开闭原则

首先我们来看下开闭原则的定义:

Software entities like classes,modules and functions should be open for extension but closed for modifications.

软件实体如类、模块和函数应该对扩展开放,对修改关闭。

怎么理解这句话呢?这句话最重要的就是"对扩展开放,对修改关闭"

  • 对扩展开放:当有新的需求或变化时,可以对现有代码进行扩展,以适应新的需求
  • 对修改关闭:需求一旦开发完成,就可以独立完成其工作,而不要对已有代码做修改

二、如何实现开闭原则

一般用来提高扩展性的方法有:多态、依赖注入、面向抽象而非面向具体编程,怎么利用多态、依赖注入、面向抽象而非具体编程,来实现"对扩展开放、对修改关闭"呢?接下来我举两个例子,相信通过这两个例子你对开闭原则的理解会更加深入。

2.1 举例一

什么依赖注入,什么是面向抽象而非具体的编程

java 复制代码
// 将图形进行抽象
public interface Shape {//..}
// 圆形
public class Cirlce implements Shape {//..}
// 三角形
public class Triangle implements Shape {//..}
// 长方形
public class Rectangle implements Shape {//..}

public class Demo {
    private Shape shape; // 基于接口而非实现的编程
    // 依赖注入
    public Demo(Shape shape) {
        this.shape = shape;
    }
}

以上这段伪代码就是基于接口而非具体编程,将图形共同的属性和方法抽取到Shape接口中,然后针对不同的图形会分别实现接口中的功能。那么问题来了,为什么要这么做呢?来一个完整的例子,通过这个例子你就会明白为什么要这么做

2.2 举例二 电商支付系统

假如我们在开发电商平台的支付系统,当前支付的方式有Alipay、WechatPay,这个支付系统如何设计呢?首先可以定义个PayManager来统一管理支付。代码如下

java 复制代码
public class PayManager {
    public void pay(int payMode) {
        if (payMode == 1) {
            aliPay();
        } else if (payMode == 2) {
            wechatPay();
        }
    }

    private void aliPay() {
        System.out.println("调用 alipay 接口");
    }

    private void wechatPay() {
        System.out.println("调用 wechatpay 接口");
    }
}

可以看到在PayManager的pay方法里根据传递的参数来判断应该使用哪种支付方式,貌似没啥问题,但是随着业务的发展需要增加银行卡支付方式BankCardPay,应该怎么处理呢?需要修改两个地方

  • pay方法里增加一个if分支
  • 写一个bankcardPay方法

代码如下

java 复制代码
public class PayManager {
    public void pay(int payMode) {
        if (payMode == 1) {
            aliPay();
        } else if (payMode == 2) {
            wechatPay();
        } else if (payMode == 3) {
            // 修改点一:增加一个if分支
            bankcardPay();
        }
    }

    private void aliPay() {
        System.out.println("调用 alipay 接口");
    }

    private void wechatPay() {
        System.out.println("调用 wechatpay 接口");
    }
    
    // 修改点二:增加一个bankcardPay方法
    private void bankcardPay() {
        System.out.println("调用 bankcardpay 接口");
    }
}

有没有发现一个问题,PayManager里的pay方法进行了修改,假如后续还有其它支付方式的增加是不是每次都要增加一个if语句呢?这么做有什么弊端呢?

上述修改方式对现有的代码进行了修改,有潜在的危险,因为我们的pay方法在新增支付方式之前已经测试过并上线,你新增了一个支付方式对其进行了修改,是不是还要重新测试一遍呢?

另外这种写法其实违背了开闭原则即"对扩展开放,对修改关闭",哪里违背了呢?其实就是对扩展支持的不好,新增一种支付方式需要修改核心代码逻辑风险很大。那么我们应该怎么去设计它呢?案例一中可以了解到什么是面向抽象而非具体编程,同样Shape接口一样是不是可以对支付方式进行抽象呢?当然,可以定义一个接口Payment在其中抽象一个pay方法用来完成支付功能,它的代码如下

java 复制代码
public interface Payment {
    public void pay();
}

所有的支付方式都需要实现此接口,当前只支持AliPay和WechatPay这两种支付方式,它们的代码如下

java 复制代码
public class AliPay implements Payment {
    @Override
    public void pay() {
        System.out.println("调用 alipay 接口");
    }
}

public class WechatPay implements Payment {
    @Override
    public void pay() {
        System.out.println("调用 WechatPay 接口");
    }
}

PayManager的代码如下

java 复制代码
public class PayManager {
    // 依赖注入
    public void pay(Payment payment) {
        payment.pay();
    }
}

然后就是如何去调用

java 复制代码
public class TestPay {
    public static void main(String[] args) {
        Payment wechatPay = new WechatPay();
        PayManager.pay(wechatPay);
    }
}

可以看到第三行需要哪种支付方式直接创建其对象并将其传递个PayManager的pay方法即可。此时需要增加一个银行卡支付功能应该如何处理呢?只需要增加一个BankCardPay类并实现Payment接口即可,代码如下

java 复制代码
public class BankCardPay implements Payment {
    @Override
    public void pay() {
        System.out.println("调用 BankCardPay 接口");
    }
}

这样就已经修改完成,调用方法也跟上面一样,代码如下

java 复制代码
public class TestPay {
    public static void main(String[] args) {
        Payment wechatPay = new BankCardPay();
        PayManager.pay(wechatPay);
    }
}

可以看到增加一种BankCardPay支付方式并未对PayManager类做修改,因此对之前已经上线的功能没有影响。后续再增加其它支付方式比如京东支付、抖音支付等等都很容易扩展。

可能有些同学会说,这样修改增加很多个类可读性还没有之前好,确实如此,有些情况下代码的扩展性会跟可读性相冲突,比如上面我们重构代码之后,可读性没有之前的if分支好,所以很多时候我们要在扩展性和可读性之间做权衡,在某些场景下代码的可读性很重要,我们就牺牲一些扩展性,在某些场景下代码的扩展性很重要,我们就牺牲一些可读性。

比如上述的例子,如果项目一开始就说我们的支付方式只支持Alipay和WechatPay这两种支付方式,那刚开始的写法思路简单易读,它就是合理的。相反如果后续要增加很多种支付方式,那么我们重构之后的写法就是比较合理的,因此是否合理没有一个统一的标准,一定要结合业务需求、场景等来进行重构。

三、总结

1.定义

软件实体如类、模块和函数应该对扩展开放,对修改关闭。

开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代码来完成新的功能开发,低层模块的变更,必然要有高层模块进行耦合,否则就是一个独立无意义的代码片段。

2.为什么要使用开闭原则

(1)对于测试来讲,新增的方法不会对已有的方法造成影响,只需要保证新增的类、模块和函数是正确的就可以了

(2)提高可复用性,在面向对象的设计中,所有的逻辑都是从原子逻辑组合而来的,而不是在一个类中独立实现一个业务逻辑。只有这样代码才可以复用,粒度越小,被复用的可能性就越大。

(3)提高可维护性,从上述例子中可以看出来当对已有功能做修改时增加一个类即可,这是不是就是维护人员很乐意干的事,即增加一个类,而不是修改一个类。

3.如何做到"对扩展开放、对修改关闭"

添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。我们要时刻具备扩展意识、抽象意识、封装意识,在写代码的时候要多花点时间思考一下,未来可能的变更,以便在未来需求变更的时候,在不改变代码整体结构的情况下,将新的代码灵活的插入到项目中。

很多设计原则、设计思想、设计模式都是以提到代码的扩展性为最终目的。特别是23中经典设计模式,大部分都是为了解决代码的扩展性而总结出来的,都是以开闭原则为指导原则。最常用提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(装饰、策略、模版、责任链、状态)

好了本篇博客就到这里了,后续还会继续更新关于设计原则和设计模式相关的文章,如果觉得对你有用,帮忙点赞回复666

参考书籍:

《设计模式之禅》

《架构整洁之道》

相关推荐
wellc19 小时前
SpringBoot集成Flowable
java·spring boot·后端
Hui Baby20 小时前
springAi+MCP三种
java
hsjcjh20 小时前
【MySQL】C# 连接MySQL
java
敖正炀20 小时前
LinkedBlockingDeque详解
java
wangyadong31720 小时前
datagrip 链接mysql 报错
java
untE EADO20 小时前
Tomcat的server.xml配置详解
xml·java·tomcat
ictI CABL20 小时前
Tomcat 乱码问题彻底解决
java·tomcat
敖正炀20 小时前
DelayQueue 详解
java
敖正炀20 小时前
PriorityBlockingQueue 详解
java
shark222222221 小时前
Spring 的三种注入方式?
java·数据库·spring