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

[复制链接]
发表于 2025-7-4 02:35:12 | 显示全部楼层 |阅读模式
摘要

本文详细先容了计划模式中的六大根本原则,包括单一职责原则、开放封闭原则、里氏替换原则、接口隔离原则、依赖倒置原则和合成复用原则。每个原则都通过界说、明白、示例三个部门进行叙述,旨在帮助开发者进步代码的可维护性和灵活性。通过具体代码示例,文章展示了怎样在现实项目中应用这些原则,以优化软件计划。


1. 单一职责原则

1.1. ✅ 界说:

一个类只负责一件事有且仅有一个引起它变革的原因
1.2. ✅ 明白:

这是单一职责原则(SRP)背后的核心动机:变革的原因越多,类的稳固性就越差,维护资本也就越高。假如一个类负担了太多职责,当其中一个职责变革时,大概会影响其他功能
1.3. ✅ 示例

1.3.1. 职责=变革的原因

一个“职责”,本质上代表的是一个变革的原因。假如一个类负担了多种职责,它就会被多种不同的变革触发修改。
举例说明:
  1. class ReportService {
  2.     public void generateReport() {
  3.         // 业务逻辑
  4.     }
  5.     public void saveToFile(String content) {
  6.         // IO 文件保存逻辑
  7.     }
  8.     public void sendEmail(String content) {
  9.         // 邮件发送逻辑
  10.     }
  11. }
复制代码
这个类负担了 三种职责


  • 报表生成(业务变革时要改)
  • 文件保存(存储方式变革时要改)
  • 邮件发送(邮件战略变革时要改)
1.3.2. 职责之间强耦合,牵一发动全身

假如某天要变更邮件发送战略(如改用 Kafka 异步通知),你大概会:


  • 改 sendEmail 方法;
  • 但假如修改失误或测试不足,大概会影响 generateReport 或 saveToFile 方法的逻辑。
这就带来了:


  • 不必要的风险(改一处,误伤其他);
  • 不利于复用(不能只复用报表逻辑而不带邮件逻辑);
  • 影响可测试性(一个测试类测试了多个功能)。
1.3.3. 职责分离后好处:解耦 + 高内聚

将不同职责分离成不同类:
  1. class ReportGenerator {
  2.     public String generateReport() { ... }
  3. }
  4. class FileStorage {
  5.     public void saveToFile(String content) { ... }
  6. }
  7. class EmailNotifier {
  8.     public void sendEmail(String content) { ... }
  9. }
复制代码
好处:


  • 每个类只受一种变革影响
  • 修改一个模块不会误伤其他;
  • 更容易测试、复用和维护;
  • 符合高内聚、低耦合的计划理念。
2. 开放封闭原则(OCP:Open Closed Principle)

2.1. ✅ 界说:

对扩展开放对修改封闭——答应对类举动的扩展,但不答应修改原有代码
2.2. ✅ 明白:

通过接口、抽象类和多态机制,新增功能时不动旧代码,提拔体系稳固性。也就是说,体系应该答应在不修改已有代码的条件下添加新功能,以提拔稳固性、可维护性和可扩展性。通过抽象(接口/抽象类)界说稳固的扩展点,新功能只需新增实现类,通过多态机制接入,无需改动原有逻辑。抽象(接口/抽象类)+ 多态 = 构建扩展点,新增不改旧,体系更稳固。这是一种高质量、可持续演进的体系计划战略。
2.3. ✅ 示例:

假设你正在开发一个付出体系,最开始只支持微信付出
  1. // 早期版本
  2. public class PaymentService {
  3.     public void pay(String type) {
  4.         if ("wechat".equals(type)) {
  5.             System.out.println("微信支付");
  6.         }
  7.     }
  8. }
复制代码
缺点:


  • 每增长一个付出方式(如付出宝、银行卡等),就要改动 pay 方法
  • 增长逻辑风险,测试资本进步,稳固性降落。
2.3.1. 面向抽象编程(重构)

  1. // 抽象接口
  2. public interface PayStrategy {
  3.     void pay();
  4. }
  5. // 微信支付实现
  6. public class WeChatPay implements PayStrategy {
  7.     public void pay() {
  8.         System.out.println("微信支付");
  9.     }
  10. }
  11. // 支付宝支付实现
  12. public class AlipayPay implements PayStrategy {
  13.     public void pay() {
  14.         System.out.println("支付宝支付");
  15.     }
  16. }
  17. // 上层调用
  18. public class PaymentService {
  19.     private PayStrategy payStrategy;
  20.     public PaymentService(PayStrategy payStrategy) {
  21.         this.payStrategy = payStrategy;
  22.     }
  23.     public void execute() {
  24.         payStrategy.pay();
  25.     }
  26. }
复制代码
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 的示例(反例)

场景:计划一个“矩形”和“正方形”的类。
  1. class Rectangle {
  2.     protected int width;
  3.     protected int height;
  4.     public void setWidth(int w) { this.width = w; }
  5.     public void setHeight(int h) { this.height = h; }
  6.     public int getArea() {
  7.         return width * height;
  8.     }
  9. }
复制代码
正方形继承矩形:
  1. class Square extends Rectangle {
  2.    
  3.     @Override
  4.     public void setWidth(int w) {
  5.         this.width = w;
  6.         this.height = w; // 强行同步宽高
  7.     }
  8.     @Override
  9.     public void setHeight(int h) {
  10.         this.height = h;
  11.         this.width = h; // 强行同步宽高
  12.     }
  13. }
复制代码
问题点:
  1. Rectangle r = new Square();
  2. r.setWidth(4);
  3. r.setHeight(5);
  4. System.out.println(r.getArea());  // 原预期是 4 * 5 = 20,但实际输出 25!
复制代码
以为你用的是 Rectangle,但举动却是 Square 强行同步宽高,导致语义变革,替换失败,这就违背了 LSP。
3.3.2. 符合 LSP 的示例(正例)

办理方式是:将 Rectangle 和 Square 分开计划,不要利用继承,而是将“正方形”作为特别矩形逻辑的聚合或组合
  1. interface Shape {
  2.     int getArea();
  3. }
  4. class Rectangle implements Shape {
  5.     protected int width;
  6.     protected int height;
  7.     public Rectangle(int w, int h) {
  8.         this.width = w;
  9.         this.height = h;
  10.     }
  11.     public int getArea() {
  12.         return width * height;
  13.     }
  14. }
  15. class Square implements Shape {
  16.     private int side;
  17.     public Square(int side) {
  18.         this.side = side;
  19.     }
  20.     public int getArea() {
  21.         return side * side;
  22.     }
  23. }
复制代码
如许你就不会被继承关系“误导”。总结一句话:不要为了代码复用而继承,假如子类不能完美遵守父类的举动左券,就不应该继承它。符合里氏替换原则能带来:继承布局的结实性;多态替换的可靠性;体系运行的划一性。
4. 接口隔离原则(ISP:Interface Segregation Principle)

4.1. ✅ 界说:

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

一个接口最好不要太“大” —— 拆分成小而精的多个接口,制止“胖接口”。
“胖接口”是指一个接口中界说了过多的方法,导致:


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

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

  1. public interface Animal {
  2.     void eat();
  3.     void fly();
  4.     void swim();
  5.     void run();
  6. }
复制代码
假如我们要实现一个 Dog:
  1. public class Dog implements Animal {
  2.     public void eat() { System.out.println("吃"); }
  3.     public void fly() { } // 狗不会飞
  4.     public void swim() { System.out.println("狗刨"); }
  5.     public void run() { System.out.println("跑"); }
  6. }
复制代码
这就是接口污染:被迫实现不必要的 fly() 方法,不符合 ISP。
4.3.2. ✅ 正例:拆分为多个小接口

  1. public interface Eater {
  2.     void eat();
  3. }
  4. public interface Flyer {
  5.     void fly();
  6. }
  7. public interface Swimmer {
  8.     void swim();
  9. }
  10. public interface Runner {
  11.     void run();
  12. }
复制代码
实现类只依赖本身关心的接口:
  1. public class Dog implements Eater, Swimmer, Runner {
  2.     public void eat() { System.out.println("吃"); }
  3.     public void swim() { System.out.println("狗刨"); }
  4.     public void run() { System.out.println("跑"); }
  5. }
复制代码
如许:


  • 每个接口职责单一;
  • 实现类更清晰;
  • 体系更易扩展、测试、维护。
4.3.3. ✅ 现实应用场景举例

场景
粗接口(不保举)
拆分小接口(保举)
文件操纵工具类
FileHandler有 read、write、delete、copy
ReadableFile, WritableFile, DeletableFile
用户权限管理接口
UserService
同时包含注册、登录、授权、查询
RegisterService, LoginService, AuthService
Spring Data Repository
假如某个 DAO 接口包含不常用的高级查询方法
利用继承自 JpaRepository、PagingAndSortingRepository
5. 依赖倒置原则(DIP:Dependency Inversion Principle)

5.1. ✅ 界说:

高层模块不应该依赖低层模块,二者都应该依赖抽象;抽象不应该依赖细节,细节应该依赖抽象。
5.2. ✅ 明白:

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


  • 程序中模块之间通过“抽象”来交互;
  • 不要在高层业务代码中直接依赖具体实现类
  • 通过接口/抽象类界说举动,由具体类实现。
为什么必要依赖倒置?


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

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

  1. class MySQLUserDao {
  2.     public void save(String name) {
  3.         System.out.println("保存用户到MySQL:" + name);
  4.     }
  5. }
  6. class UserService {
  7.     private MySQLUserDao dao = new MySQLUserDao(); // 直接依赖实现类
  8.     public void createUser(String name) {
  9.         dao.save(name);
  10.     }
  11. }
复制代码


  • UserService 只能利用 MySQLUserDao;
  • 无法替换成其他数据源(如 Redis、Mongo);
  • 单位测试困难。
5.3.2. ✅ 正例:依赖接口

  1. // 抽象
  2. public interface UserDao {
  3.     void save(String name);
  4. }
  5. // 实现
  6. public class MySQLUserDao implements UserDao {
  7.     public void save(String name) {
  8.         System.out.println("保存用户到MySQL:" + name);
  9.     }
  10. }
  11. // 高层只依赖接口
  12. public class UserService {
  13.    
  14.     private final UserDao dao;
  15.     public UserService(UserDao dao) {
  16.         this.dao = dao;
  17.     }
  18.     public void createUser(String name) {
  19.         dao.save(name);
  20.     }
  21. }
复制代码
如许 UserService 就与实现无关了,你可以注入任何实现:
  1. new UserService(new MySQLUserDao());
  2. new UserService(new MockUserDao());
复制代码
5.3.3. ✅ Spring 中的依赖倒置实践

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


  • 通过@Autowired、构造器注入等,注入接口而非实现;
  • 利用 IOC 容器控制实现类选择;
  • 通过配置文件/注解进行举动替换,无需改动业务代码。
  1. @Service
  2. public class UserService {
  3.     @Autowired
  4.     private final UserRepository userRepository;
  5. }
复制代码
6. 合成复用原则(CARP:Composition Over Inheritance)

6.1. ✅ 界说:

优先利用“组合”或“聚合”来复用代码,而不是继承。
6.2. ✅ 明白:

继承是强耦合,组合更加灵活,符合“变革点隔离”的计划思想。
机制
特点简述
继承
是“is-a”关系,子类拥有父类所有举动,强耦合,不灵活
组合
是“has-a”关系,通过属性组合对象,松耦合,更灵活
聚合
是“has-a”的一种特别情况,组合关系中对象生命周期独立
6.3. ✅ 示例:

6.3.1. ❌ 继承的问题



  • 子类会继承父类的所有方法,哪怕有些不必要;
  • 一旦父类修改,所有子类大概都会受影响;
  • Java 不支持多继承,扩展受限;
  • 难以满足将来需求的变革。
例:
  1. class Animal {
  2.     void walk() { System.out.println("动物走"); }
  3. }
  4. class Bird extends Animal {
  5.     void fly() { System.out.println("鸟飞"); }
  6. }
复制代码
现在你想做一个企鹅(企鹅不会飞),怎么办?继承 Bird 显然不合适,但重新写又代码重复。
6.3.2. ✅ 组合的长处



  • 可以灵活地引入所需能力;
  • 符合变革点隔离原则,不同功能独立演化;
  • 可以更好地应对业务场景变革。
6.3.3. ✅ 示例:用组合代替继承

1. 把举动抽象成接口
  1. interface Flyable {
  2.     void fly();
  3. }
  4. interface Walkable {
  5.     void walk();
  6. }
复制代码
2. 抽离举动实现类
  1. class NormalWalk implements Walkable {
  2.     public void walk() { System.out.println("用两条腿走"); }
  3. }
  4. class NoFly implements Flyable {
  5.     public void fly() { System.out.println("我不会飞"); }
  6. }
复制代码
3. 组合举动到企鹅类中
  1. class Penguin {
  2.    
  3.     private Walkable walkBehavior;
  4.    
  5.     private Flyable flyBehavior;
  6.     public Penguin(Walkable walk, Flyable fly) {
  7.         this.walkBehavior = walk;
  8.         this.flyBehavior = fly;
  9.     }
  10.     public void walk() {
  11.         walkBehavior.walk();
  12.     }
  13.     public void fly() {
  14.         flyBehavior.fly();
  15.     }
  16. }
复制代码
4. 利用
  1. Penguin penguin = new Penguin(new NormalWalk(), new NoFly());
  2. penguin.walk(); // 用两条腿走
  3. penguin.fly();  // 我不会飞
复制代码
6.3.4. 🔧 总结对比

对比点
继承
组合
耦合度
高(父类变,子类易受影响)
低(只依赖接口/对象)
灵活性
不支持多继承
可以组合多个不同功能
可测试性
不易 mock
易于注入和 mock
改动影响面
广
局部可控
计划哲学
强制共享举动
按需装配功能
7. 项目实践怎么遵照计划原则

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

从上面六个原则的讲解中,应该体会到软件的计划是一个循规蹈矩,逐步优化的过程。颠末一次次的逻辑分析,一层层的布局调整和优化,最终得出一个较为公道的计划图。整个动物世界的类图如下:


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

  • 单一职责原则告诉我们实现类要职责单一。用于类的计划,增长一个类时利用 SRP 原则来查对该类的计划是否纯粹干净,也就是让一个类的功能尽大概单一,不要想着一个类包揽所有功能。
  • 里氏替换原则告诉我们不要破坏继承体系。用于引导类继承的计划,计划类之间的继承关系时,利用 LSP 原则来判断这种继承关系是否公道。只要父类能出现的地方子类就能出现(就可以用子类来替换他),反之则不一定成立。
  • 依赖倒置原则告诉我们要面向接口编程。用于引导怎样抽象,即要依赖抽象和接口编程,不要依赖具体的实现。
  • 接口隔离原则告诉我们在计划接口的时候要精简单一。用于引导接口的计划,当发现一个接口过于臃肿时,就要对这个接口进行适当的拆分。
  • 开放封闭原则告诉我们要对扩展开放,对修改关闭。开闭原则可以说是整个计划的最终目标和原则!开闭原则是总纲,其他4个原则是对这个原则具体解释。
计划原则是进行软件计划的核心思想和规范。那在现实的项目开发中,是否一定要遵照原则?答案不总是肯定,要视情况而定。因为在现实的项目开发中,必须要安时按量地完成使命。项目标进度受时间资本,测试资源的影响,而且程序一定要保存稳固可以。
还记得我们在单一职责原则中提到一个例子吗?面对需求的变更,我们有三种办理方式:

  • 方法一:直接改原有的函数(方法),这种方式最快速,但后期维护最困难,而且未便拓展;这种方式一定是要杜绝的。
  • 方法二:增长一个新方法,不修改原有的方法,这在方法级别是符合单一职责原则的;但对上层的调用会增长不少麻烦。在项目比力复杂,类比力庞大,而且测试资源比力紧缺的时候,不失为一种快速和稳妥的方式。因为假如要进行大范围的代码重构,势必要对影响到的模块进行全覆盖的测试回归,才气确保体系的稳固可靠。
  • 方法三:增长一个新的类来负责新的职责,两个职责分离,这是符合单一职责原则的。在项目初次开发,或逻辑相对简单的情况下,必要采用这种方式。
在现实的项目开发中,我们要尽大概地遵照这些计划原则。但并不是要 100% 地遵从,必要结果现实的时间资本、测试资源、代码改动难度等情况进行综合评估,适当取舍,采用最高效公道的方式。
博文参考

《软件计划模式》

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
继续阅读请点击广告

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
回复

使用道具 举报

快速回复 返回顶部 返回列表