设计模式——面向对象设计六大原则

摘要

本文详细介绍了设计模式中的六大基本原则,包括单一职责原则、开放封闭原则、里氏替换原则、接口隔离原则、依赖倒置原则和合成复用原则。每个原则都通过定义、理解、示例三个部分进行阐述,旨在帮助开发者提高代码的可维护性和灵活性。通过具体代码示例,文章展示了如何在实际项目中应用这些原则,以优化软件设计。

1. 单一职责原则

1.1. ✅ 定义:

一个类只负责一件事有且仅有一个引起它变化的原因

1.2. ✅ 理解:

这是单一职责原则(SRP)背后的核心动机:变化的原因越多,类的稳定性就越差,维护成本也就越高。如果一个类承担了太多职责,当其中一个职责变化时,可能会影响其他功能。

1.3. ✅ 示例

1.3.1. 职责=变化的原因

一个“职责”,本质上代表的是一个变化的原因。如果一个类承担了多种职责,它就会被多种不同的变化触发修改。

举例说明:

class ReportService {public void generateReport() {// 业务逻辑}public void saveToFile(String content) {// IO 文件保存逻辑}public void sendEmail(String content) {// 邮件发送逻辑}
}

这个类承担了 三种职责

  • 报表生成(业务变化时要改)
  • 文件保存(存储方式变化时要改)
  • 邮件发送(邮件策略变化时要改)

1.3.2. 职责之间强耦合,牵一发动全身

如果某天要变更邮件发送策略(如改用 Kafka 异步通知),你可能会:

  • sendEmail 方法;
  • 但如果修改失误或测试不足,可能会影响 generateReportsaveToFile 方法的逻辑。

这就带来了:

  • 不必要的风险(改一处,误伤其他);
  • 不利于复用(不能只复用报表逻辑而不带邮件逻辑);
  • 影响可测试性(一个测试类测试了多个功能)。

1.3.3. 职责分离后好处:解耦 + 高内聚

将不同职责分离成不同类:

class ReportGenerator {public String generateReport() { ... }
}class FileStorage {public void saveToFile(String content) { ... }
}class EmailNotifier {public void sendEmail(String content) { ... }
}

好处:

  • 每个类只受一种变化影响
  • 修改一个模块不会误伤其他;
  • 更容易测试、复用和维护;
  • 符合高内聚、低耦合的设计理念。

2. 开放封闭原则(OCP:Open Closed Principle)

2.1. ✅ 定义:

对扩展开放对修改封闭——允许对类行为的扩展,但不允许修改原有代码

2.2. ✅ 理解:

通过接口、抽象类和多态机制,新增功能时不动旧代码,提升系统稳定性。也就是说,系统应该允许在不修改已有代码的前提下添加新功能,以提升稳定性、可维护性和可扩展性。通过抽象(接口/抽象类)定义稳定的扩展点,新功能只需新增实现类,通过多态机制接入,无需改动原有逻辑。抽象(接口/抽象类)+ 多态 = 构建扩展点,新增不改旧,系统更稳定。这是一种高质量、可持续演进的系统设计策略。

2.3. ✅ 示例:

假设你正在开发一个支付系统,最开始只支持微信支付

// 早期版本
public class PaymentService {public void pay(String type) {if ("wechat".equals(type)) {System.out.println("微信支付");}}
}

缺点:

  • 每增加一个支付方式(如支付宝、银行卡等),就要改动 pay 方法
  • 增加逻辑风险,测试成本提高,稳定性下降。

2.3.1. 面向抽象编程(重构)

// 抽象接口
public interface PayStrategy {void pay();
}// 微信支付实现
public class WeChatPay implements PayStrategy {public void pay() {System.out.println("微信支付");}
}// 支付宝支付实现
public class AlipayPay implements PayStrategy {public void pay() {System.out.println("支付宝支付");}
}// 上层调用
public class PaymentService {private PayStrategy payStrategy;public PaymentService(PayStrategy payStrategy) {this.payStrategy = payStrategy;}public void execute() {payStrategy.pay();}
}

2.3.2. 重构后好处:

  • 新增支付方式,只需实现新的 PayStrategy 子类;
  • PaymentService 不需要改动,遵循 开放-封闭原则
  • 利用了接口+多态,实现功能扩展与旧代码解耦。

2.3.3. 应用场景

场景

抽象化方式

多态实现

示例说明

日志记录

Logger 接口

FileLogger, DBLogger

新增日志方式无需修改旧逻辑

排序策略

Comparator<T> 接口

自定义 compare方法

支持多种排序方式

消息推送

PushService 接口

EmailPush, SmsPush

扩展渠道不影响已有逻辑

业务规则引擎

Rule 抽象类或接口

各种规则类

增加规则时只需新增类,不动主流程代码

3. 里氏替换原则(LSP:Liskov Substitution Principle)

3.1. ✅ 定义:

子类必须能够替换父类,程序逻辑的正确性不被破坏。

3.2. ✅ 理解:

子类继承父类时,不应改变父类原有功能的语义,否则违背了替换原则。子类在继承父类时,不能违背父类原有的语义和行为约定,否则就破坏了继承的正确性。如果一个子类违背了父类的行为预期,那么它就不能被替换为父类使用,会导致系统运行异常或逻辑错误。

3.3. ✅ 示例:

3.3.1. 不符合 LSP 的示例(反例)

场景:设计一个“矩形”和“正方形”的类。

class Rectangle {protected int width;protected int height;public void setWidth(int w) { this.width = w; }public void setHeight(int h) { this.height = h; }public int getArea() {return width * height;}
}

正方形继承矩形:

class Square extends Rectangle {@Overridepublic void setWidth(int w) {this.width = w;this.height = w; // 强行同步宽高}@Overridepublic void setHeight(int h) {this.height = h;this.width = h; // 强行同步宽高}
}

问题点:

Rectangle r = new Square();
r.setWidth(4);
r.setHeight(5);
System.out.println(r.getArea());  // 原预期是 4 * 5 = 20,但实际输出 25!

以为你用的是 Rectangle,但行为却是 Square 强行同步宽高,导致语义变化,替换失败,这就违反了 LSP。

3.3.2. 符合 LSP 的示例(正例)

解决方式是:将 RectangleSquare 分开设计,不要使用继承,而是将“正方形”作为特殊矩形逻辑的聚合或组合

interface Shape {int getArea();
}class Rectangle implements Shape {protected int width;protected int height;public Rectangle(int w, int h) {this.width = w;this.height = h;}public int getArea() {return width * height;}
}class Square implements Shape {private int side;public Square(int side) {this.side = side;}public int getArea() {return side * side;}
}

这样你就不会被继承关系“误导”。总结一句话:不要为了代码复用而继承,如果子类不能完美遵守父类的行为契约,就不应该继承它。符合里氏替换原则能带来:继承结构的健壮性;多态替换的可靠性;系统运行的一致性。

4. 接口隔离原则(ISP:Interface Segregation Principle)

4.1. ✅ 定义:

不应该强迫客户端依赖它不需要的接口;一个接口最好只包含客户端所需的方法。

4.2. ✅ 理解:

一个接口最好不要太“大” —— 拆分成小而精的多个接口,避免“胖接口”。

“胖接口”是指一个接口中定义了过多的方法,导致:

  • 实现类必须实现一些无关方法
  • 实现代码中出现大量的空方法、无意义实现;
  • 模块间耦合度增加,影响代码可维护性、扩展性。

4.3. ✅ 示例:

4.3.1. ❌ 反例:一个“胖接口”

public interface Animal {void eat();void fly();void swim();void run();
}

如果我们要实现一个 Dog

public class Dog implements Animal {public void eat() { System.out.println("吃"); }public void fly() { } // 狗不会飞public void swim() { System.out.println("狗刨"); }public void run() { System.out.println("跑"); }
}

这就是接口污染:被迫实现不需要的 fly() 方法,不符合 ISP。

4.3.2. ✅ 正例:拆分为多个小接口

public interface Eater {void eat();
}public interface Flyer {void fly();
}public interface Swimmer {void swim();
}public interface Runner {void run();
}

实现类只依赖自己关心的接口:

public class Dog implements Eater, Swimmer, Runner {public void eat() { System.out.println("吃"); }public void swim() { System.out.println("狗刨"); }public void run() { System.out.println("跑"); }
}

这样:

  • 每个接口职责单一;
  • 实现类更清晰;
  • 系统更易扩展、测试、维护。

4.3.3. ✅ 现实应用场景举例

场景

粗接口(不推荐)

拆分小接口(推荐)

文件操作工具类

FileHandler有 read、write、delete、copy

ReadableFile, WritableFile, DeletableFile

用户权限管理接口

UserService

同时包含注册、登录、授权、查询

RegisterService, LoginService, AuthService

Spring Data Repository

如果某个 DAO 接口包含不常用的高级查询方法

使用继承自 JpaRepositoryPagingAndSortingRepository

5. 依赖倒置原则(DIP:Dependency Inversion Principle)

5.1. ✅ 定义:

高层模块不应该依赖低层模块,二者都应该依赖抽象;抽象不应该依赖细节,细节应该依赖抽象。

5.2. ✅ 理解:

依赖“抽象”(接口或抽象类),不要直接依赖具体实现类,利于扩展与测试。

  • 程序中模块之间通过“抽象”来交互;
  • 不要在高层业务代码中直接依赖具体实现类
  • 通过接口/抽象类定义行为,由具体类实现。

为什么需要依赖倒置?

  • 增强可扩展性:替换或扩展底层实现时,不需要修改上层代码;
  • 便于测试:接口更容易被 mock,实现单元测试;
  • 解耦:高层和低层只通过抽象耦合。

5.3. ✅ 示例:

5.3.1. ❌ 反例:高层直接依赖低层实现

class MySQLUserDao {public void save(String name) {System.out.println("保存用户到MySQL:" + name);}
}class UserService {private MySQLUserDao dao = new MySQLUserDao(); // 直接依赖实现类public void createUser(String name) {dao.save(name);}
}
  • UserService 只能使用 MySQLUserDao
  • 无法替换成其他数据源(如 Redis、Mongo);
  • 单元测试困难。

5.3.2. ✅ 正例:依赖接口

// 抽象
public interface UserDao {void save(String name);
}// 实现
public class MySQLUserDao implements UserDao {public void save(String name) {System.out.println("保存用户到MySQL:" + name);}
}// 高层只依赖接口
public class UserService {private final UserDao dao;public UserService(UserDao dao) {this.dao = dao;}public void createUser(String name) {dao.save(name);}
}

这样 UserService 就与实现无关了,你可以注入任何实现:

new UserService(new MySQLUserDao());
new UserService(new MockUserDao());

5.3.3. ✅ Spring 中的依赖倒置实践

Spring 框架本身就是依赖倒置原则的典范:

  • 通过@Autowired、构造器注入等,注入接口而非实现;
  • 利用 IOC 容器控制实现类选择;
  • 通过配置文件/注解进行行为替换,无需改动业务代码。
@Service
public class UserService {@Autowiredprivate final UserRepository userRepository;
}

6. 合成复用原则(CARP:Composition Over Inheritance)

6.1. ✅ 定义:

优先使用“组合”或“聚合”来复用代码,而不是继承。

6.2. ✅ 理解:

继承是强耦合,组合更加灵活,符合“变化点隔离”的设计思想。

机制

特点简述

继承

是“is-a”关系,子类拥有父类所有行为,强耦合,不灵活

组合

是“has-a”关系,通过属性组合对象,松耦合,更灵活

聚合

是“has-a”的一种特殊情况,组合关系中对象生命周期独立

6.3. ✅ 示例:

6.3.1. ❌ 继承的问题

  • 子类会继承父类的所有方法,哪怕有些不需要;
  • 一旦父类修改,所有子类可能都会受影响;
  • Java 不支持多继承,扩展受限;
  • 难以满足未来需求的变化。

例:

class Animal {void walk() { System.out.println("动物走"); }
}class Bird extends Animal {void fly() { System.out.println("鸟飞"); }
}

现在你想做一个企鹅(企鹅不会飞),怎么办?继承 Bird 显然不合适,但重新写又代码重复。

6.3.2. ✅ 组合的优点

  • 可以灵活地引入所需能力;
  • 符合变化点隔离原则,不同功能独立演化;
  • 可以更好地应对业务场景变化。

6.3.3. ✅ 示例:用组合代替继承

1. 把行为抽象成接口

interface Flyable {void fly();
}interface Walkable {void walk();
}

2. 抽离行为实现类

class NormalWalk implements Walkable {public void walk() { System.out.println("用两条腿走"); }
}class NoFly implements Flyable {public void fly() { System.out.println("我不会飞"); }
}

3. 组合行为到企鹅类中

class Penguin {private Walkable walkBehavior;private Flyable flyBehavior;public Penguin(Walkable walk, Flyable fly) {this.walkBehavior = walk;this.flyBehavior = fly;}public void walk() {walkBehavior.walk();}public void fly() {flyBehavior.fly();}
}

4. 使用

Penguin penguin = new Penguin(new NormalWalk(), new NoFly());
penguin.walk(); // 用两条腿走
penguin.fly();  // 我不会飞

6.3.4. 🔧 总结对比

对比点

继承

组合

耦合度

高(父类变,子类易受影响)

低(只依赖接口/对象)

灵活性

不支持多继承

可以组合多个不同功能

可测试性

不易 mock

易于注入和 mock

改动影响面

广

局部可控

设计哲学

强制共享行为

按需装配功能

7. 项目实践怎么遵循设计原则

7.1. 软件设计是一个逐步优化的过程

从上面六个原则的讲解中,应该体会到软件的设计是一个循序渐进,逐步优化的过程。经过一次次的逻辑分析,一层层的结构调整和优化,最终得出一个较为合理的设计图。整个动物世界的类图如下:

我们对上面五个原则做一个总结:

  1. 单一职责原则告诉我们实现类要职责单一。用于类的设计,增加一个类时使用 SRP 原则来核对该类的设计是否纯粹干净,也就是让一个类的功能尽可能单一,不要想着一个类包揽所有功能。
  2. 里氏替换原则告诉我们不要破坏继承体系。用于指导类继承的设计,设计类之间的继承关系时,使用 LSP 原则来判断这种继承关系是否合理。只要父类能出现的地方子类就能出现(就可以用子类来替换他),反之则不一定成立。
  3. 依赖倒置原则告诉我们要面向接口编程。用于指导如何抽象,即要依赖抽象和接口编程,不要依赖具体的实现。
  4. 接口隔离原则告诉我们在设计接口的时候要精简单一。用于指导接口的设计,当发现一个接口过于臃肿时,就要对这个接口进行适当的拆分。
  5. 开放封闭原则告诉我们要对扩展开放,对修改关闭。开闭原则可以说是整个设计的最终目标和原则!开闭原则是总纲,其他4个原则是对这个原则具体解释。

设计原则是进行软件设计的核心思想和规范。那在实际的项目开发中,是否一定要遵循原则?答案不总是肯定,要视情况而定。因为在实际的项目开发中,必须要安时按量地完成任务。项目的进度受时间成本,测试资源的影响,而且程序一定要保存稳定可以。

还记得我们在单一职责原则中提到一个例子吗?面对需求的变更,我们有三种解决方式:

  1. 方法一:直接改原有的函数(方法),这种方式最快速,但后期维护最困难,而且不便拓展;这种方式一定是要杜绝的。
  2. 方法二:增加一个新方法,不修改原有的方法,这在方法级别是符合单一职责原则的;但对上层的调用会增加不少麻烦。在项目比较复杂,类比较庞大,而且测试资源比较紧缺的时候,不失为一种快速和稳妥的方式。因为如果要进行大范围的代码重构,势必要对影响到的模块进行全覆盖的测试回归,才能确保系统的稳定可靠。
  3. 方法三:增加一个新的类来负责新的职责,两个职责分离,这是符合单一职责原则的。在项目首次开发,或逻辑相对简单的情况下,需要采用这种方式。

在实际的项目开发中,我们要尽可能地遵循这些设计原则。但并不是要 100% 地遵从,需要结果实际的时间成本、测试资源、代码改动难度等情况进行综合评估,适当取舍,采用最高效合理的方式。

博文参考

《软件设计模式》

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

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

相关文章

使用 So-VITS-SVC 实现明星声音克隆与视频音轨替换实战全流程

本文展示如何使用开源项目 so-vits-svc 实现声音克隆与视频音轨替换流程&#xff0c;适用于 AI 音频工程、声音合成等学习场景。所述内容仅限技术交流&#xff0c;禁止用于非法用途。 一、项目背景 此项目采用 so-vits-svc 4.1 开源框架&#xff0c;实现了“用明星声音替换视频…

【学习记录】深入解析 AI 交互中的五大核心概念:Prompt、Agent、MCP、Function Calling 与 Tools

&#x1f4cc; 引言 随着大语言模型&#xff08;LLM&#xff09;的发展&#xff0c;AI 已经不再只是“回答问题”的工具&#xff0c;而是可以主动执行任务、调用外部资源、甚至构建完整工作流的智能系统。 为了更好地理解和使用这些能力&#xff0c;我们需要了解 AI 交互中几…

纹理压缩格式优化

🎯 Unity 项目纹理压缩格式优化终极指南 ——不同平台、不同手机型号,如何正确选择 🧩 什么是纹理压缩(Texture Compression)? Texture压缩 = 减小显存占用,提升加载速度,减轻GPU负担纹理是游戏中最大资源,占用50%+内存正确压缩:减少GPU Bandwidth,提高渲染性能错…

Docker轻松搭建Neo4j+APOC环境

Docker轻松搭建Neo4jAPOC环境 一、简介二、Docker部署neo4j三、Docker安装APOC插件四、删除数据库/切换数据库 一、简介 Neo4j 是一款高性能的 原生图数据库&#xff0c;采用 属性图模型 存储数据&#xff0c;支持 Cypher查询语言&#xff0c;适用于复杂关系数据的存储和分析。…

NGINX `ngx_stream_core_module` 模块概览

一、模块定位与功能 通用 TCP/UDP 代理 支持同时处理 TCP 和 UDP 流量&#xff0c;透明转发请求到后端服务器组&#xff08;upstream&#xff09;。可作为四层负载均衡&#xff0c;根据客户端 IP、权重、最少连接等策略将连接分发给后端。 预读&#xff08;preread&#xff09…

JVM类加载高阶实战:从双亲委派到弹性架构的设计进化

前言 作为Java开发者&#xff0c;我们都知道JVM的类加载机制遵循"双亲委派"原则。但在实际开发中&#xff0c;特别是在金融支付、插件化架构等场景下&#xff0c;严格遵循这个原则反而会成为系统扩展的桎梏。本文将带你深入理解双亲委派机制的本质&#xff0c;并分享…

MATLAB | 绘图复刻(十九)| 轻松拿捏 Nature Communications 绘图

hello这次真的是好久不见了&#xff0c;前段时间确实太忙&#xff0c;后台都忙到没时间看&#xff0c;对不住大家的热情&#xff0c;这期复刻两个 Nature Communications 绘图&#xff0c;主要都和弦图有关&#xff1a; 原图 1 复刻图 1 原图 2 复刻图 2 这次绘图使用我自己开…

群晖NAS如何在虚拟机创建飞牛NAS

套件中心下载安装Virtual Machine Manager 创建虚拟机 配置虚拟机 飞牛官网下载 https://iso.liveupdate.fnnas.com/x86_64/trim/fnos-0.9.2-863.iso 群晖NAS如何在虚拟机创建飞牛NAS - 个人信息分享

设计模式(代理设计模式)

代理模式解释清楚&#xff0c;所以如果想对一个类进行功能上增强而又不改变原来的代码情况下&#xff0c;那么只需要让这个类代理类就是我们的顺丰&#xff0c;对吧?并行增强就可以了。具体增强什么?在哪方面增强由代理类进行决定。 代码实现就是使用代理对象代理相关的逻辑…

Flask + ECharts+MYSQL全球贸易数字化大屏

核心功能: 全球贸易热力图:展示中国与各国的贸易关系强度 贸易指标卡片:实时显示贸易总额、投资额等关键指标 贸易伙伴排名:展示中国前10大贸易伙伴 贸易类型分布:展示各类商品的贸易占比 全球实时动态:滚动显示全球贸易、投资等实时事件 技术亮点: 使用WebSocket实现实…

wpf Behaviors库实现支持多选操作进行后台绑定数据的ListView

<ListView ItemsSource"{Binding SchemeItems}" SelectionMode"Extended" VerticalAlignment"Stretch" HorizontalAlignment"Stretch"><ListView.ContextMenu><ContextMenu><MenuItem Header"删除" …

50个JAVA常见代码大全:学完这篇从Java小白到架构师

50个JAVA常见代码大全&#xff1a;学完这篇从Java小白到架构师 Java&#xff0c;作为一门流行多年的编程语言&#xff0c;始终占据着软件开发领域的重要位置。无论是初学者还是经验丰富的程序员&#xff0c;掌握Java中常见的代码和概念都是至关重要的。本文将列出50个Java常用…

【Linux手册】冯诺依曼体系结构

目录 前言 五大组件 数据信号 存储器&#xff08;内存&#xff09;有必要吗 常见面试题 前言 冯诺依曼体系结构是当代计算机基本架构&#xff0c;冯诺依曼体系有五大组件&#xff0c;通过这五大组件直观的描述了计算机的工作原理&#xff1b;学习冯诺依曼体系可以让给我们更…

10_聚类

描述 聚类&#xff08;clustering&#xff09;是将数据集划分成组的任务&#xff0c;这些组叫作簇&#xff08;cluster&#xff09;。其目标是划分数据&#xff0c;使得一个簇内的数据点非常相似且不同簇内的数据点非常不同。与分类算法类似&#xff0c;聚类算法为每个数据点分…

【SSM】SpringBoot学习笔记1:SpringBoot快速入门

前言&#xff1a; 文章是系列学习笔记第9篇。基于黑马程序员课程完成&#xff0c;是笔者的学习笔记与心得总结&#xff0c;供自己和他人参考。笔记大部分是对黑马视频的归纳&#xff0c;少部分自己的理解&#xff0c;微量ai解释的内容&#xff08;ai部分会标出&#xff09;。 …

国产高性能pSRAM选型指南:CSS6404LS-LI 64Mb QSPI伪静态存储器

一、芯片基础特性 核心参数 容量 &#xff1a;64Mb&#xff08;8M 8bit&#xff09;电压 &#xff1a;单电源供电 2.7-3.6V &#xff08;兼容3.3V系统&#xff09;接口 &#xff1a;Quad-SPI&#xff08;QPI/SPI&#xff09;同步模式封装 &#xff1a; SOP-8L (150mil) &#…

Cilium动手实验室: 精通之旅---4.Cilium Gateway API - Lab

Cilium动手实验室: 精通之旅---4.Cilium Gateway API - Lab 1. 环境准备2. API 网关--HTTP2.1 部署应用2.2 部署网关2.3 HTTP路径匹配2.4 HTTP头匹配 3. API网关--HTTPS3.1 创建TLS证书和私钥3.2 部署HTTPS网关3.3 HTTPS请求测试 4. API网关--TLS 路由4.1 部署应用4.2 部署网关…

20250605在微星X99主板中配置WIN10和ubuntu22.04.6双系统启动的引导设置

rootrootrootroot-X99-Turbo:~$ sudo apt-get install boot-repair rootrootrootroot-X99-Turbo:~$ sudo add-apt-repository ppa:yannubuntu/boot-repair rootrootrootroot-X99-Turbo:~$ sudo apt-get install boot-repair 20250605在微星X99主板中配置WIN10和ubuntu22.04.6双…

MyBatis之测试添加功能

1. 首先Mybatis为我们提供了一个操作数据库的会话对象叫Sqlsession&#xff0c;所以我们就需要先获取sqlsession对象&#xff1a; //加载核心配置文件 InputStream is Resources.getResourceAsStream("mybatis-config.xml"); //获取sqlSessionFactoryBuilder(是我…

[论文阅读] 人工智能+软件工程 | MemFL:给大模型装上“项目记忆”,让软件故障定位又快又准

【论文解读】MemFL&#xff1a;给大模型装上“项目记忆”&#xff0c;让软件故障定位又快又准 论文信息 arXiv:2506.03585 Improving LLM-Based Fault Localization with External Memory and Project Context Inseok Yeo, Duksan Ryu, Jongmoon Baik Subjects: Software Engi…