设计模式 - 面向对象原则:SOLID最佳实践

文章目录

  • 深入理解 SOLID:用对原则,别把简单问题搞复杂
    • SOLID 原则概览
    • 1. 单一职责原则(SRP)
    • 2. 开闭原则(OCP)
    • 3. 里氏替换原则(LSP)
    • 4. 接口隔离原则(ISP)
    • 5. 依赖反转原则(DIP)
    • 原则之间的联系与平衡
  • 案例
    • 一、单一职责原则(SRP)—订单处理模块拆分
    • 二、开闭原则(OCP)—优惠策略引擎
    • 三、里氏替换原则(LSP)—图表渲染组件
    • 四、接口隔离原则(ISP)—外部服务集成
    • 五、依赖反转原则(DIP)—仓储层设计
    • 案例共性与最佳实践

在这里插入图片描述

深入理解 SOLID:用对原则,别把简单问题搞复杂

在面向对象编程的世界里,SOLID 原则几乎是每个程序员最熟悉的五个字母组合——但也是最容易被“滥用”或“误用”的设计准则。

很多同学往往将每一条原则孤立地、机械地应用,结果往往制造出十几二十个冗余类,把原本简单的需求复杂化。正如“有了锤子,就到处找钉子”,在并不必要的时候,你硬要用上 SOLID,就很可能把本该一刀搞定的小活儿,变成一场大型重构。

下面,我们就带着“如何正确理解与应用”这个目标,一起来复盘 SOLID 五条原则的来龙去脉。

SOLID 原则概览

2000 年,Robert C. Martin 在论文《设计原理和设计模式》中首次提出 SOLID 概念。过去二十年里,这五条原则帮助我们构建了更易维护、可扩展的系统:

  • Single Responsibility Principle (SRP):单一职责原则
  • Open–Closed Principle (OCP):开闭原则
  • Liskov Substitution Principle (LSP):里氏替换原则
  • Interface Segregation Principle (ISP):接口隔离原则
  • Dependency Inversion Principle (DIP):依赖反转原则

其核心价值在于——当团队规模扩大、多人协作时,我们需要低耦合、高内聚、可替换的模块。


1. 单一职责原则(SRP)

定义:一个类(或模块)应该只有一个“引起它变化的原因”。
误区:常被简单理解为“一个类只做一件事”、“一个接口只实现一次”“写好不能动”……而忘记“职责=变化的原因”这一核心。

示例

public class Book {private String title, author, text;public String replaceWord(String word){ /*…*/ }public boolean containsWord(String word){ /*…*/ }public void print(){ /*…*/ }    // ← 新增的打印责任public void read(){ /*…*/ }     // ← 新增的阅读责任
}

当打印逻辑变化时,你得修改 Book,当阅读流程变化时,又要修改它——职责不唯一,违反 SRP。

正确做法:抓住“职责”的边界,职责可由多个类共同完成,但要保证各自变化原因单一;例如把打印与阅读逻辑拆到 BookPrinterBookReader


2. 开闭原则(OCP)

定义:对扩展开放,对修改封闭。
误区:把它当作业务代码里的“金科玉律”,不管成本高低都要“零修改”;结果往往产生一大堆空壳类。

示例:Spring JDBC 的 AbstractDataSource,通过继承来扩展读写分离策略,而不修改框架源码,即是 OCP 在框架层面的典型应用。

public abstract class Demo  extends AbstractDataSource {private int readDsSize;@Overridepublic Connection getConnection() throws SQLException {return this.determineTargetDataSource().getConnection();}@Overridepublic Connection getConnection(String username, String password) throws SQLException {return this.determineTargetDataSource().getConnection(username, password);}protected DataSource determineTargetDataSource() {if (determineCurrentLookupKey() && this.readDsSize > 0){//读库做负载均衡(从库)return this.loadBalance();} else {//写库使用主库return this.getResolvedMasterDataSource();}}protected abstract boolean determineCurrentLookupKey();//其他代码省略}

思考:在业务代码里,需求迭代快,直接修改往往更高效;在框架、类库或架构层面,才更有必要遵循 OCP,以减少对核心组件的侵入式改动。


3. 里氏替换原则(LSP)

定义:子类必须能够替换父类,并保证行为一致性。
意义:保证多态下的可靠性,让调用者无需感知具体子类,就能正确工作。

示例:自定义 Spring 的 PropertyEditorSupport,遵循基类契约即可插入各种属性编辑器,URL 参数解析也能“无感”替换。

比如,Spring 中提供的自定义属性编辑器,可以解析 HTTP 请求参数中的自定义格式进行绑定并转换为格式输出。只要遵循基类(PropertyEditorSupport)的约束定义,就能为某种数据类型注册一个属性编辑器。我们先定义一个类 DefineFormat,具体代码如下:

public class DefineFormat{private String rawStingFormat;private String uid;private String toAppCode;private String fromAppCode;private Sting timestamp;// 省略构造函数和get, set方法
}

然后,创建一个 Restful API 接口,用于输入自定义的请求 URL。

@GetMapping(value = "/api/{d-format}", 
public DefineFormat parseDefineFormat (@PathVariable("d-format") DefineFormat defineFormat) {return defineFormat;
}

接下来,创建 DefineFormatEditor,实现输入自定义字符串,返回自定义格式 json 数据。

public class DefineFormatEditor extends PropertyEditorSupport {//setAsText() 用于将String转换为另一个对象@Overridepublic void setAsText(String text) throws IllegalArgumentException {if (StringUtils.isEmpty(text)) {setValue(null);} else {DefineFormat df = new DefineFormat();df.setRawStingFormat(text);String[] data = text.spilt("-");if (data.length == 4) {df.setUid(data[0]);df.setToAppCode(data[1]);df.setFromAppCode(data[2]);df.setTimestamp(data[3]);setValue(df);} else {setValue(null);}}}//将对象序列化为String时,将调用getAsText()方法@Overridepublic String getAsText() {DefineFormat defineFormat= (DefineFormat) getValue();return null == defineFormat ? "" :    defineFormat.getRawStingFormat();}
}

最后,输入 url: /api/dlewgvi8we-toapp-fromapp-zzzzzzz,返回响应。

{"rawStingFormat:"dlewgvi8we-toapp-fromapp-zzzzzz","uid:"dlewgvi8we","toAppCode":"toapp","fromAppCode":"fromapp","message":"zzzzzzz"
}

使用里氏替换原则(LSP)的本质就是通过继承实现多态行为,这在面向对象编程中是非常重要的一个技巧,对于提高代码的扩展性是很有帮助的。

要点:不仅要继承接口签名,还要遵守合同(前置条件不变、后置条件不减弱、异常行为不变化)。


4. 接口隔离原则(ISP)

定义:多个特定客户端接口胜过一个通用接口。
误区:只看单个接口中的方法数量,不考虑系统整体职责划分。

示例

interface ICRUD<T> { add(); update(); delete(); query(); sync(); }

当大多数业务并不需要 sync() 时,就被“强迫”实现,违反 ISP。正确的做法是拆分:

interface ICRUD<T> { add(); update(); delete(); query(); }
interface ISync { sync(); }

建议:整体系统层面思考职责,避免“一个接口—一个实现”式的过度拆分。


5. 依赖反转原则(DIP)

定义:高层模块不依赖底层模块,二者都依赖抽象;抽象不依赖细节,细节依赖抽象。
内涵:关注“好的抽象”——既能覆盖共性,又能灵活替换实现。

示例:JDBC 接口就是对关系型数据库读写操作的正确抽象,底层可插拔各种数据库驱动。

启示:多用接口/抽象类定义契约,把实现细节隐藏在抽象之后;但切忌为了“抽象”而抽象,仍要结合业务场景。


原则之间的联系与平衡

  1. SRP 是基础:唯有职责清晰,才有更好的扩展(OCP)、替换(LSP)、隔离(ISP)和抽象(DIP)能力。
  2. OCP 是归宿:真正做到修改关闭,需要在设计时结合 SRP、ISP、DIP 等原则为目标。
  3. DIP 更高层次:指导我们在架构级别“分离与替换”——借助抽象解耦模块。
  4. 取舍:过度拆分会违背 KISS 原则。最简单的折中方法是——控制接口数量,抽象通用职责,避免“一接口—一实现”的刻板模式。

在这里插入图片描述


专家:软件工程 > 面向对象设计专家
要求:VERBOSITY = V=2(详细),结合真实项目案例,分享 SOLID 原则在业务系统中的落地实践,语言为中文。

计划

  1. 简要说明选取案例的背景和项目类型
  2. 按原则分类,每个案例包含:项目背景、问题痛点、SOLID 原则应用、效果与代码片段
  3. 总结案例共性与最佳实践建议

案例

一、单一职责原则(SRP)—订单处理模块拆分

项目背景
在某电商平台的后台服务中,OrderService 类同时负责订单校验、库存扣减、支付调用、通知发送等多项功能,业务不断膨胀,迭代时常因小改动引发连锁故障。

问题痛点

  • 一处变动可能影响多个流程,回归测试成本高
  • 类方法过长、维护困难,责任边界不清晰

SRP 应用

  1. 拆分职责

    • OrderValidator:只做订单合法性校验
    • StockManager:只做库存预扣与回滚
    • PaymentProcessor:只负责与支付网关交互
    • NotificationSender:只负责订单状态变更通知
  2. 组合调用

    public class OrderService {private final OrderValidator validator;private final StockManager stockManager;private final PaymentProcessor paymentProcessor;private final NotificationSender notifier;public void placeOrder(Order order) {validator.validate(order);stockManager.reserve(order);paymentProcessor.pay(order);notifier.send(order);}
    }
    
  3. 效果

    • 各模块职责清晰,单元测试覆盖率提升至 90%
    • 修改通知逻辑时,无需回归库存或支付流程

二、开闭原则(OCP)—优惠策略引擎

项目背景
促销活动层出不穷,初期将 DiscountService 写成多重 if-else,每次上线新活动都要改这个类,风险极高。

问题痛点

  • 修改封闭,新增促销需频繁改动原有代码
  • 条件分支难以维护,代码臃肿

OCP 应用

  1. 抽象策略接口

    public interface DiscountStrategy {BigDecimal calculate(Order order);
    }
    
  2. 各活动实现

    @Component
    public class BlackFridayStrategy implements DiscountStrategy { /*…*/ }@Component
    public class NewUserStrategy implements DiscountStrategy { /*…*/ }
    
  3. 策略注册与调用

    @Component
    public class DiscountService {private final List<DiscountStrategy> strategies;public BigDecimal apply(Order order) {return strategies.stream().filter(s -> s.supports(order)).map(s -> s.calculate(order)).reduce(BigDecimal.ZERO, BigDecimal::add);}
    }
    
  4. 效果

    • 新增策略只需编写一个类并注入,无需改动 DiscountService
    • 代码体量更易扩展,回归风险大幅降低

三、里氏替换原则(LSP)—图表渲染组件

项目背景
在后台统计系统中,需要渲染不同类型的图表(折线图、柱状图、饼图)。最初用 Chart 抽象类配合 if (type) 逻辑,后来改用继承。

问题痛点

  • 部分子类没有实现所有方法,导致运行时抛出 UnsupportedOperationException
  • 修改父类抽象方法会破坏部分子类行为

LSP 应用

  1. 精炼抽象

    public interface ChartRenderer {void render(DataSet data);
    }
    
  2. 具体子类全力支持契约

    public class LineChartRenderer implements ChartRenderer { /*…*/ }
    public class PieChartRenderer  implements ChartRenderer { /*…*/ }
    
  3. 渲染调用无需分支

    rendererMap.get(type).render(data);
    
  4. 效果

    • 所有子类都能安全替换接口
    • 后续新增 RadarChartRenderer 无需改动核心逻辑

四、接口隔离原则(ISP)—外部服务集成

项目背景
一套 CRM 系统需要对接多家短信、邮件、推送服务,最初定义一个 MessagingClient 接口,包含 sendSmssendEmailsendPush,导致集成方只需邮件时也要实现短信、推送方法。

问题痛点

  • 实现类方法桩多,代码臃肿
  • 不同服务方复用率低

ISP 应用

  1. 拆分接口

    public interface SmsClient   { void sendSms(SmsMessage msg); }
    public interface EmailClient { void sendEmail(Email msg); }
    public interface PushClient  { void sendPush(PushMessage msg); }
    
  2. 各接入实现各自接口

    public class TwilioSmsClient implements SmsClient { /*…*/ }
    public class SendGridEmailClient implements EmailClient { /*…*/ }
    
  3. 按需注入

    @Service
    public class NotificationService {private final SmsClient sms;private final EmailClient email;public void notifyOrderCreated(Order o) {sms.sendSms(...);email.sendEmail(...);}
    }
    
  4. 效果

    • 避免“被迫”实现无关方法
    • 接口职责更聚焦,单元测试更简洁

五、依赖反转原则(DIP)—仓储层设计

项目背景
某金融系统最初直接在业务层 LoanServicenew JdbcLoanDao(),测试时需要配合真实数据库,耦合度高。

问题痛点

  • 测试难以模拟,业务层依赖底层实现
  • 更换存储方式需改动业务层

DIP 应用

  1. 抽象 DAO 接口

    public interface LoanRepository {Loan findById(String id);void save(Loan loan);
    }
    
  2. 业务层依赖接口

    public class LoanService {private final LoanRepository repo;public LoanService(LoanRepository repo){ this.repo = repo; }// … 调用 repo 方法
    }
    
  3. 底层实现注入

    @Repository
    public class JdbcLoanRepository implements LoanRepository { /*…*/ }
    
  4. 效果

    • 单元测试可注入内存或 Mock 实现
    • 切换到 JPA 或其它存储无业务层改动

案例共性与最佳实践

  1. 先识别“变化点”,再拆分职责或抽象接口。
  2. 不要为了原则而原则,关注业务痛点与演进成本。
  3. 测试驱动设计(TDD) 有助于发现违反 SOLID 的耦合点。
  4. KISS 平衡:遵循 SOLID 的同时,也要兼顾代码简洁与团队可读性。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/88713.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/88713.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Vue 3 中父组件内两个子组件相互传参的几种方法

方法一&#xff1a;通过父组件中转&#xff08;Props Emits&#xff09;<!-- ParentComponent.vue --> <template><ChildA :message-from-b"messageFromB" send-to-b"handleSendToB" /><ChildB :message-from-a"messageFromA&q…

三子棋游戏设计与实现(C 语言版)

一、需求分析目标&#xff1a;实现一个简单的人机对战三子棋&#xff0c;支持以下功能&#xff1a;初始化空棋盘&#xff0c;清晰展示落子状态。玩家通过坐标落子&#xff08;X 代表玩家&#xff09;&#xff0c;电脑随机落子&#xff08;O 代表电脑&#xff09;。实时判断胜负…

GD32 CAN1和TIMER0同时开启问题

背景&#xff1a;今天在一个项目调试的时候发现了一些问题&#xff0c;由此贴记录一下问题解决的过程。使用的芯片是GD32F305VE。使用到了CAN1和TIMER0。在使用这连个外设的时候发送了一些问题。单独使用CAN1。功能正常。单独使用TIMER0。配置为输出模式。功能正常。但是当两个…

剑指offer56_数组中唯一只出现一次的数字

数组中唯一只出现一次的数字在一个数组中除了一个数字只出现一次之外&#xff0c;其他数字都出现了三次。 请找出那个只出现一次的数字。 你可以假设满足条件的数字一定存在。 思考题&#xff1a; 如果要求只使用 O(n) 的时间和额外 O(1) 的空间&#xff0c;该怎么做呢&#xf…

从语音识别到智能助手:Voice Agent 的技术进化与交互变革丨Voice Agent 学习笔记

From Research AI&#xff1a; 最近看到 Andrew Ng 的一句话让我印象深刻&#xff1a;“While some things in AI are overhyped, voice applications seem underhyped right now.”&#xff08;尽管 AI 中有些领域被过度炒作&#xff0c;语音应用却似乎被低估了&#xff09;。…

什么是Jaccard 相似度(Jaccard Similarity)

文章目录✅ 定义&#xff1a;&#x1f4cc; 取值范围&#xff1a;&#x1f50d; 举例说明&#xff1a;&#x1f9e0; 应用场景&#xff1a;⚠️ 局限性&#xff1a;&#x1f4a1; 扩展概念&#xff1a;Jaccard 相似度&#xff08;Jaccard Similarity&#xff09; 是一种用于衡量…

ragflow_多模态文档解析与正文提取策略

多模态文档解析与正文提取策略 RAGflow的文档解析系统位于deepdoc/parser/目录下,实现了对多种文档格式的统一解析处理。该系统采用模块化设计,针对不同文档格式提供专门的解析器,并通过视觉识别技术增强解析能力。本文将深入探讨RAGflow的文档解析系统的设计原理、实现细节…

数据结构栈的实现(C语言)

栈的基本概念栈是一种特殊的线性存储结构&#xff0c;是一种操作受到限制的线性表&#xff0c;特殊体现在两个地方&#xff1a;1、元素进栈出栈的操作只能从同一端完成&#xff0c;另一端是封闭的&#xff0c;通常将数据进栈叫做入栈&#xff0c;压栈等&#xff0c;出栈叫做弹栈…

【springboot】IDEA手动创建SpringBoot简单工程(无插件)

大致步骤 创建Maven工程 引入依赖 提供启动类 详细教程 创建Maven工程 修改pom.xml文件 添加父节点 <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.5.3</…

独立开发第二周:构建、执行、规划

一 第二周的独立开发旅程落下帷幕。相较于第一周的适应&#xff0c;本周的核心词是“聚焦”与“执行”。 目标非常明确&#xff1a;在产品开发上取得进展&#xff1b;在个人工作节奏上&#xff0c;将上周初步形成的框架进行实践与固化。 同时&#xff0c;为至关重要的自媒体运营…

在YOLO-World中集成DeformConv、CBAM和Cross-Modal Attention模块的技术报告

在YOLO-World中集成DeformConv、CBAM和Cross-Modal Attention模块的技术报告 1. 引言 1.1 项目背景 目标检测是计算机视觉领域的核心任务之一,而YOLO(You Only Look Once)系列算法因其出色的速度和精度平衡而广受欢迎。YOLO-World是YOLO系列的最新发展,专注于开放词汇目标…

从UI设计到数字孪生实战应用:构建智慧金融的风险评估与预警平台

hello宝子们...我们是艾斯视觉擅长ui设计、前端开发、数字孪生、大数据、三维建模、三维动画10年经验!希望我的分享能帮助到您!如需帮助可以评论关注私信我们一起探讨!致敬感谢感恩!一、引言&#xff1a;传统金融风控的 “滞后困境” 与数字孪生的破局之道金融风险的隐蔽性、突…

【Linux】权限相关指令

前言&#xff1a; 上两篇文章我们讲到了&#xff0c;关于Linux中的基础指令。 【Linux】初见&#xff0c;基础指令-CSDN博客【Linux】初见&#xff0c;基础指令&#xff08;续&#xff09;-CSDN博客 本文我们来讲Linux中关于权限中的一些指令 shell命令 Linux严格来说是一个操…

前端学习3--position定位(relative+absolute+sticky)

继上一篇&#xff0c;做下拉菜单的时候&#xff0c;涉及到了position&#xff0c;这篇就来学习下~先看下position在下拉菜单中的应用&#xff1a;一、关键代码回顾&#xff1a;/* 下拉菜单容器 */ .dropdown {position: relative; /* ➊ 关键父级 */ }/* 下拉内容&#xff08;默…

APP Inventor使用指南

APP Inventor使用指南一、组件介绍二、逻辑设计设计方法&#xff1a;设计实例&#xff08;参考嘉立创教程&#xff09;点击跳转 &#xff1a; app inventor&#xff08;点不开的话需要&#x1fa84;&#x1fa84;&#x1fa84;&#x1fa84;&#x1fa84;&#xff09; 一、组…

SpringAI实现保存聊天记录到redis中

redis相关准备添加依赖我利用redission来实现<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.37.0</version> </dependency>添加配置文件spring:redis:database: 5host: 127.0.0.1…

Unity中使用EzySlice实现模型切割与UV控制完全指南

引言 在Unity中实现3D模型的动态切割是一个常见的需求&#xff0c;无论是用于游戏特效、建筑可视化还是医疗模拟。本文将全面介绍如何使用EzySlice插件实现高效的模型切割&#xff0c;并深入探讨如何通过Shader Graph精确控制切割面的UV映射。 第一部分&#xff1a;EzySlice基…

【c++学习记录】状态模式,实现一个登陆功能

状态模式建议为对象的所有可能状态新建一个类&#xff0c; 然后将所有状态的对应行为抽取到这些类中。 原始对象被称为上下文 &#xff08;context&#xff09;&#xff0c; 它并不会自行实现所有行为&#xff0c; 而是会保存一个指向表示当前状态的状态对象的引用&#xff0c;…

Docker 搭建 Harbor 私有仓库

1 部署 Harbor 注意&#xff1a;docker、docker-compose、Harbor的版本是否适配&#xff0c;这里使用的版本如下表&#xff1a; Docker版本Docker Compose版本Harbor版本v19.09.8v1.29.2v2.8.2 1.1 安装 docker-compose # 下载 docker-compose 1.29.2 版本 curl -L "h…

C++类模板继承部分知识及测试代码

目录 0.前言 1.类模板基本使用 2.类模板继承 2.1类模板继承过程中的模板参数 情况1&#xff1a;父类非模板&#xff0c;子类为模板 情况2&#xff1a;父类模板&#xff0c;子类为非模板 情况3&#xff1a;父类模板&#xff0c;子类为模板 3.STL中的模板类分析 3.1STL中…