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,持续输出硬核文章。

目录
相关文章
|
23天前
|
弹性计算 人工智能 架构师
阿里云携手Altair共拓云上工业仿真新机遇
2024年9月12日,「2024 Altair 技术大会杭州站」成功召开,阿里云弹性计算产品运营与生态负责人何川,与Altair中国技术总监赵阳在会上联合发布了最新的“云上CAE一体机”。
阿里云携手Altair共拓云上工业仿真新机遇
|
16天前
|
存储 关系型数据库 分布式数据库
GraphRAG:基于PolarDB+通义千问+LangChain的知识图谱+大模型最佳实践
本文介绍了如何使用PolarDB、通义千问和LangChain搭建GraphRAG系统,结合知识图谱和向量检索提升问答质量。通过实例展示了单独使用向量检索和图检索的局限性,并通过图+向量联合搜索增强了问答准确性。PolarDB支持AGE图引擎和pgvector插件,实现图数据和向量数据的统一存储与检索,提升了RAG系统的性能和效果。
|
20天前
|
机器学习/深度学习 算法 大数据
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
2024“华为杯”数学建模竞赛,对ABCDEF每个题进行详细的分析,涵盖风电场功率优化、WLAN网络吞吐量、磁性元件损耗建模、地理环境问题、高速公路应急车道启用和X射线脉冲星建模等多领域问题,解析了问题类型、专业和技能的需要。
2575 22
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
|
18天前
|
人工智能 IDE 程序员
期盼已久!通义灵码 AI 程序员开启邀测,全流程开发仅用几分钟
在云栖大会上,阿里云云原生应用平台负责人丁宇宣布,「通义灵码」完成全面升级,并正式发布 AI 程序员。
|
3天前
|
JSON 自然语言处理 数据管理
阿里云百炼产品月刊【2024年9月】
阿里云百炼产品月刊【2024年9月】,涵盖本月产品和功能发布、活动,应用实践等内容,帮助您快速了解阿里云百炼产品的最新动态。
阿里云百炼产品月刊【2024年9月】
|
2天前
|
存储 人工智能 搜索推荐
数据治理,是时候打破刻板印象了
瓴羊智能数据建设与治理产品Datapin全面升级,可演进扩展的数据架构体系为企业数据治理预留发展空间,推出敏捷版用以解决企业数据量不大但需构建数据的场景问题,基于大模型打造的DataAgent更是为企业用好数据资产提供了便利。
162 2
|
20天前
|
机器学习/深度学习 算法 数据可视化
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
2024年中国研究生数学建模竞赛C题聚焦磁性元件磁芯损耗建模。题目背景介绍了电能变换技术的发展与应用,强调磁性元件在功率变换器中的重要性。磁芯损耗受多种因素影响,现有模型难以精确预测。题目要求通过数据分析建立高精度磁芯损耗模型。具体任务包括励磁波形分类、修正斯坦麦茨方程、分析影响因素、构建预测模型及优化设计条件。涉及数据预处理、特征提取、机器学习及优化算法等技术。适合电气、材料、计算机等多个专业学生参与。
1575 16
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
|
22天前
|
编解码 JSON 自然语言处理
通义千问重磅开源Qwen2.5,性能超越Llama
击败Meta,阿里Qwen2.5再登全球开源大模型王座
968 14
|
3天前
|
Linux 虚拟化 开发者
一键将CentOs的yum源更换为国内阿里yum源
一键将CentOs的yum源更换为国内阿里yum源
211 2
|
17天前
|
人工智能 开发框架 Java
重磅发布!AI 驱动的 Java 开发框架:Spring AI Alibaba
随着生成式 AI 的快速发展,基于 AI 开发框架构建 AI 应用的诉求迅速增长,涌现出了包括 LangChain、LlamaIndex 等开发框架,但大部分框架只提供了 Python 语言的实现。但这些开发框架对于国内习惯了 Spring 开发范式的 Java 开发者而言,并非十分友好和丝滑。因此,我们基于 Spring AI 发布并快速演进 Spring AI Alibaba,通过提供一种方便的 API 抽象,帮助 Java 开发者简化 AI 应用的开发。同时,提供了完整的开源配套,包括可观测、网关、消息队列、配置中心等。
731 10