SOLID设计原则:开闭原则

简介: 本文通过电商库存系统的例子,详细介绍了开闭原则(OCP)的实现方法,强调“对扩展开放,对修改关闭”的核心理念。文中展示了如何利用继承、接口和多态性避免频繁修改代码,并通过策略模式和装饰器模式等设计模式实现灵活扩展。通过具体案例分析,帮助读者理解开闭原则的应用场景与实践技巧,提升代码质量和可维护性。文章还鼓励开发者在日常业务开发中应用设计模式,以提高技术水平。

你好,我是猿java。

真实工作中,你是否是这样操作过:一个需求过来,把原来的代码改一遍,再一个需求过来,又把上一个需求的代码改一遍,很多重复的工作还是在日复一日的重复,有什么好的办法改善吗?

相信有经验的小伙伴一定听过:对扩展开放,对修改关闭。那么,你知道这句话的真正含义吗?今天我们就来聊聊开闭原则到底是怎么实现的。

什么是开闭原则?

开放封闭原则,英文是:Open–closed principle, 简称OCP,是该原则是 Bertrand Meyer 在1988年提出的,最后被 Robert C. Martin收录到 SOLID原则,开闭原则指出:

Software entities should be open for extension, but closed for modification.

软件实体应该对扩展开放,对修改关闭。

如何实现开闭原则?

"对扩展开放,对修改关闭",如何理解呢?我们先看一个案例,如下图,给出了电商领域库存系统库存变更的简易模型图,库存系统接收外部系统库存变更事件,然后对数据库中的库存进行修改。
stock.png

面对这个业务需求,很多人的代码会写出这样

public class Stock {
   
    public void updateStock(String event){
   

        if("outOfStock" == event){
   
            // todo 出库事件   库存操作

        }else if("warehousing" == event){
   
            // todo 入库事件   库存操作

        }
    }
}

这时,新的需求来了:WMS仓储系统内部会产生盘点事件(盘盈/盘亏),这些事件会导致变更库存。于是,代码就会发展成下面这样

public class Stock {
   

    public void updateStock(String event){
   

        if("outOfStock" == event){
   
            // todo 出库事件   库存操作

        }else if("warehousing" == event){
   
            // todo 入库事件   库存操作

        }else if("panSurplus" == event){
   
            // todo 盘盈事件   库存操作

        }else if("loss" == event){
   
            // todo 盘亏事件   库存操作
        }
    }
}

很显然,上述代码的实现,每来一个需求,就需要修改一次代码,在方法中增加一个 else if分支,因此 Stock类就一直处于变更中,不稳定。

有没有什么好的办法,可以使得这个代码不用被修改,但是又能够灵活的扩展,满足业务需求呢?

这个时候我们就要搬出 java的三大法宝:继承,实现,多态

我们发现整个业务模型是:事件导致库存变更。所以,能不能把事件抽离出来?把它抽象成一个接口,代码如下:

model.png

public interface Event {
   
    void updateStock(String event);
}

每种事件对应一种库存变更,抽象成一个具体的实现类,代码如下

入库事件

public class WarehousingEvent implements Event {
   
    public void updateStock(String event){
   
        // 业务逻辑
    }
}

出库事件

public class OutOfStockEvent implements Event {
   
    public void updateStock(String event){
   
        // 业务逻辑
    }
}

xxx事件

public class XXXEvent implements Event {
   
    public void updateStock(String event){
   
        // 业务逻辑
    }
}

最后,Stock类中 updateStock()库存变更逻辑就可以抽象成下面这样:


public class Stock {
   
    public void updateStock(String event){
   
        // 根据事件类型获取真实的实现类
        Event event = getEventInstance(event);
        // 库存变更操作
        event.updateStock();
    }
}

经过抽象、分离和改造之后,Stock.updateStock()类就稳定下来了,再也不需要每增加一个事件就需要增加一个 else if分支处理,这种抽象带来的好处也是很明显的:每次有新的库存变更事件,只需要增加一个实现类,其他的逻辑都不需要更改,当库存事件无效时只需要把实现类删除即可。

开闭原则是常见方式

在Java编程中,遵循开闭原则的常见方式有:使用抽象类和接口、使用策略模式、使用装饰器模式等。

抽象类和接口

抽象类和接口是 Java中实现 开闭原则的基础,通过定义抽象类或接口,程序员可以在不修改已有代码的情况下,通过继承或实现来扩展新功能。因此,我们强烈建议:面向接口编程。

策略模式

策略模式是一种行为设计模式,允许在运行时选择算法的实现,策略模式通过定义一系列算法,并将每个算法封装在独立的类中,使得它们可以相互替换。

在上面的示例讲解中,其实使用的就是策略模式,当后期有其他的库存事件时,我们只需要添加扩展类,而无需修改现有的代码。

装饰器模式

装饰器模式是一种结构设计模式,允许向一个对象动态添加行为。装饰器模式通过创建一个装饰器类来包装原始类,从而增加新的功能。示例代码:

// 定义一个接口
public interface Coffee {
   
    String getDescription();
    double getCost();
}

// 实现接口的具体类
public class SimpleCoffee implements Coffee {
   
    @Override
    public String getDescription() {
   
        return "Simple Coffee";
    }

    @Override
    public double getCost() {
   
        return 5.0;
    }
}

// 创建装饰器抽象类
public abstract class CoffeeDecorator implements Coffee {
   
    protected Coffee decoratedCoffee;

    public CoffeeDecorator(Coffee coffee) {
   
        this.decoratedCoffee = coffee;
    }

    @Override
    public String getDescription() {
   
        return decoratedCoffee.getDescription();
    }

    @Override
    public double getCost() {
   
        return decoratedCoffee.getCost();
    }
}

// 实现具体的装饰器类
public class MilkDecorator extends CoffeeDecorator {
   
    public MilkDecorator(Coffee coffee) {
   
        super(coffee);
    }

    @Override
    public String getDescription() {
   
        return decoratedCoffee.getDescription() + ", Milk";
    }

    @Override
    public double getCost() {
   
        return decoratedCoffee.getCost() + 1.5;
    }
}

public class SugarDecorator extends CoffeeDecorator {
   
    public SugarDecorator(Coffee coffee) {
   
        super(coffee);
    }

    @Override
    public String getDescription() {
   
        return decoratedCoffee.getDescription() + ", Sugar";
    }

    @Override
    public double getCost() {
   
        return decoratedCoffee.getCost() + 0.5;
    }
}

// 客户端代码
public class CoffeeShop {
   
    public static void main(String[] args) {
   
        Coffee coffee = new SimpleCoffee();
        System.out.println(coffee.getDescription() + " $" + coffee.getCost());

        coffee = new MilkDecorator(coffee);
        System.out.println(coffee.getDescription() + " $" + coffee.getCost());

        coffee = new SugarDecorator(coffee);
        System.out.println(coffee.getDescription() + " $" + coffee.getCost());
    }
}

在这个示例中,Coffee接口定义了获取描述和成本的方法,SimpleCoffee类实现了这个接口。CoffeeDecorator类是一个抽象类,实现了 Coffee接口,并持有一个 Coffee对象。MilkDecorator和SugarDecorator类分别继承了CoffeeDecorator类,并扩展了其功能。如果我们需要增加新的装饰器,只需要继承 CoffeeDecorator类并实现其方法即可,而无需修改现有的代码。

总结

本文通过一个电商中库存实例,演示了开闭原则的整个抽象和实现过程,并给出了开闭原则最常用的 3种实现方式。

开闭原则的核心是对扩展开放,对修改关闭,因此,当业务需求一直需要修改同一段代码时,我们就得多思考代码修改的理由是什么?它们之间是不是有一定的共同性?能不能把这些变更点分离出来,通过扩展来实现而不是修改代码?

其实在业务开发中还有很多类似的场景,比如:电商系统中的会员系统,需要根据用户不同的等级计算不同的费用;机票系统,根据用户不同的等级(普通,白金用户,黄金用户...)提供不同的售票机制;网关系统中,根据不同的粒度(接口,ip,服务,集群)来实现限流;

可能有小伙伴会反驳,业务场景有类似的场景,但是逻辑简单,几个 if-else就搞定了,没有必要去搞这么复杂的设计。

本人建议:功夫在平时,功夫在细节。

很多人总抱怨业务开发技术成长慢,特别是对于初级程序员,但是大部门人的起点都是业务的 CRUD,如果能在业务 CRUD过程中想办法"挖掘"某些
设计模式,通过这种长期的刻意练习,量变产生质变,慢慢就能领会这些经典设计原则的奥妙,终有一天也能写出让人赏心悦目的代码。

学习交流

如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注:猿java,持续输出硬核文章。

目录
相关文章
|
监控 负载均衡 测试技术
服务网格简介:探索现代微服务架构中的服务网格概念和价值
服务网格简介:探索现代微服务架构中的服务网格概念和价值
641 0
|
开发框架 安全 前端开发
[ 代码审计篇 ] Java web 代码审计 详解(一)
之前写了一篇关于代码审计流程的文章,有小伙伴私聊我说不太好理解,这里开始介绍 Java web 的代码审计,同样的来捋一捋审计思路,这就相对要具体一些。
2653 0
[ 代码审计篇 ] Java web 代码审计 详解(一)
|
3月前
|
安全
新硬盘或硬件坏掉?硬件检查工具,了解硬盘健康情况!
新硬盘或硬件坏掉?硬件检查工具,了解硬盘健康情况!
|
存储 分布式计算 资源调度
Hadoop【环境搭建 04】【hadoop-2.6.0-cdh5.15.2.tar.gz 基于ZooKeeper搭建高可用集群】(部分图片来源于网络)
【4月更文挑战第1天】Hadoop【环境搭建 04】【hadoop-2.6.0-cdh5.15.2.tar.gz 基于ZooKeeper搭建高可用集群】(部分图片来源于网络)
748 3
|
9月前
|
人工智能 缓存 算法
《人机协同的边界与价值:开放世界游戏系统重构中的AI工具实战指南》
本文复盘了开放世界游戏“动态实体调度系统”重构项目中,借助Cursor与CodeBuddy实现人机协同开发的30天实践。项目初期因代码耦合、性能不达标陷入技术死锁,团队通过“CodeBuddy全局架构拆解+Cursor局部编码优化”的组合模式,完成模块拆分、算法重构、资源泄漏排查与兼容性测试四大核心任务。AI工具在全局逻辑拆解、隐性问题定位、测试用例生成等方面效率提升显著,而人类聚焦业务规则定义、方案决策与细节优化,形成“AI搭框架、人类填细节”的协作模式。
361 12
|
安全 Ubuntu 网络安全
docker中主机模式(host)
【10月更文挑战第4天】
1342 1
|
测试技术
软考软件评测师——可靠性测试测试方法
软件可靠性是指软件在规定条件和时间内完成预定功能的能力,受运行环境、软件规模、内部结构、开发方法及可靠性投入等因素影响。失效概率指软件运行中出现失效的可能性,可靠度为不发生失效的概率,平均无失效时间(MTTF)体现软件可靠程度。案例分析显示,嵌入式软件需满足高可靠性要求,如机载软件的可靠度需达99.99%以上,通过定量指标评估其是否达标。
|
块存储
ceph集群的OSD设备扩缩容实战指南
这篇文章详细介绍了Ceph集群中OSD设备的扩容和缩容过程,包括如何添加新的OSD设备、如何准备和部署,以及如何安全地移除OSD设备并从Crushmap中清除相关配置。
846 4
|
存储 Docker 容器
ARM架构鲲鹏主机BClinux离线安装docker步骤
下载并安装适用于ARM架构的Docker CE二进制文件,解压后移动至/usr/bin目录。创建docker组,配置systemd服务脚本(docker.service、docker.socket、containerd.service),重载systemd配置,启动并启用docker服务。编辑daemon.json配置存储驱动、镜像加速地址等,最后拉取所需镜像。
1265 0
|
Linux 虚拟化 数据安全/隐私保护
使用VMware安装linux虚拟机
使用VMware安装linux虚拟机
使用VMware安装linux虚拟机