本文是设计模式系列的起点沿 **「SRP → OCP → LSP → ISP → DIP → LoD → CRP」** 这条主线系统梳理面向对象设计的七大原则为后续学习 23 种设计模式打下坚实基础。 --- ## 一、模块总览设计原则学什么 | 原则 | 缩写 | 核心思想 | 一句话总结 | 本文覆盖 | |------|------|---------|-----------|---------| | 单一职责原则 | SRP | 一个类只做一件事 | 职责单一易于维护 | ✅ | | 开闭原则 | OCP | 对扩展开放对修改关闭 | 新增功能不改旧代码 | ✅ | | 里氏替换原则 | LSP | 子类能替换父类 | 继承体系不崩塌 | ✅ | | 接口隔离原则 | ISP | 接口按需拆分 | 不强迫依赖不用的方法 | ✅ | | 依赖倒置原则 | DIP | 依赖抽象不依赖具体 | 面向接口编程 | ✅ | | 迪米特法则 | LoD | 最少知道只和直接朋友通信 | 控制信息传递范围 | ✅ | | 组合复用原则 | CRP | 优先组合而非继承 | Has-A 优于 Is-A | ✅ | ### 设计原则金字塔 开闭原则OCP← 核心目标扩展不改旧代码 ───────────── 单一职责SRP← 基础类职责清晰才能谈扩展 里氏替换LSP← 继承体系不崩塌 接口隔离ISP← 接口按需拆分 依赖倒置DIP← 模块间解耦 ───────────── 迪米特法则LoD← 最少知道 组合复用CRP← 优先组合而非继承 **面试加分句**OCP 是核心目标其他原则都是为实现 OCP 服务的手段。 --- ## 二、单一职责原则SRP ### 2.1 官方定义 **一个类应该只有一个引起它变化的原因。** —— Robert C. Martin - **职责Responsibility**引起类变化的原因 - **变化原因Reason to change**需求变更时需要修改这个类的理由 **通俗理解**一个类只做一件事且只做这一件事。 ### 2.2 在架构中的位置 SRP 是 SOLID 五大原则之首也是 7 大设计原则中最基础的一个。它是类的**内聚基准线**——如果一个类职责太多后续的开闭原则、依赖倒置都无从谈起。 ### 2.3 怎么判断是否违反 SRP **判断标准**如果需求变更时你需要修改这个类的**多个不相关的理由**那就违反了 SRP。 ### 2.4 反例违反 SRP java // ❌ 反例一个类承担了三个职责 public class UserService { public void register(String username, String password) { // 1. 校验用户名 if (username null || username.length() 6) { throw new IllegalArgumentException(用户名至少 6 位); } // 2. 密码加密 String encryptedPwd DigestUtils.md5DigestAsHex(password.getBytes()); // 3. 保存到数据库直接操作 JDBC Connection conn DriverManager.getConnection(jdbc:mysql://localhost:3306/db, root, 123456); PreparedStatement stmt conn.prepareStatement(INSERT INTO user (username, password) VALUES (?, ?)); stmt.setString(1, username); stmt.setString(2, encryptedPwd); stmt.executeUpdate(); // 4. 发送欢迎邮件 sendWelcomeEmail(username); // 5. 记录日志 System.out.println(用户注册成功 username); } } **问题分析** - 数据库连接方式变更 → 需要修改 UserService - 邮件发送方式变更 → 需要修改 UserService - 日志格式变更 → 需要修改 UserService - 三个变化原因违反 SRP ### 2.5 正例遵循 SRP java // ✅ 正例拆分为三个类各司其职 Service public class UserService { private final UserRepository userRepository; private final EmailService emailService; private final Logger logger LoggerFactory.getLogger(UserService.class); public UserService(UserRepository userRepository, EmailService emailService) { this.userRepository userRepository; this.emailService emailService; } public void register(String username, String password) { validateUsername(username); String encryptedPwd PasswordEncoder.encode(password); User user new User(username, encryptedPwd); userRepository.save(user); emailService.sendWelcomeEmail(username); logger.info(用户注册成功{}, username); } private void validateUsername(String username) { if (username null || username.length() 6) { throw new IllegalArgumentException(用户名至少 6 位); } } } Repository public class UserRepository { public void save(User user) { /* 数据库操作 */ } } Service public class EmailService { public void sendWelcomeEmail(String username) { /* 发送邮件 */ } } ### 2.6 易错点 | 易错点 | 说明 | |--------|------| | 过度拆分 | 不是类越小越好职责内聚即可 | | 职责模糊 | 用户管理算一个职责还是多个判断标准变化原因是否独立 | | 工具类滥用 | Utils 类容易变成垃圾桶什么方法都往里塞 | --- ## 三、开闭原则OCP ### 3.1 官方定义 **软件实体类、模块、函数应该对扩展开放对修改关闭。** —— Bertrand Meyer1988 - **对扩展开放Open for extension**允许通过新增代码来扩展功能 - **对修改关闭Closed for modification**不允许修改已有代码来改变行为 **通俗理解**新增功能时**只加代码不改旧代码**。 ### 3.2 在架构中的位置 开闭原则是 SOLID 五大原则的**核心目标**其他四个原则SRP、LSP、ISP、DIP都是为实现 OCP 服务的手段。 ### 3.3 反例违反 OCP java // ❌ 反例每次新增支付方式都要修改 PaymentService public class PaymentService { public void pay(String paymentType, double amount) { if (alipay.equals(paymentType)) { System.out.println(使用支付宝支付 amount 元); } else if (wechat.equals(paymentType)) { System.out.println(使用微信支付 amount 元); } else if (unionpay.equals(paymentType)) { // ❌ 每次新增支付方式都要改这个方法 System.out.println(使用银联支付 amount 元); } } } ### 3.4 正例遵循 OCP java // ✅ 正例通过接口 多态新增支付方式无需修改旧代码 public interface PaymentStrategy { String getType(); void pay(double amount); } Component public class AlipayStrategy implements PaymentStrategy { Override public String getType() { return alipay; } Override public void pay(double amount) { System.out.println(支付宝支付 amount); } } Component public class WechatPayStrategy implements PaymentStrategy { Override public String getType() { return wechat; } Override public void pay(double amount) { System.out.println(微信支付 amount); } } // 新增银联支付只加新类不改 PaymentService Component public class UnionPayStrategy implements PaymentStrategy { Override public String getType() { return unionpay; } Override public void pay(double amount) { System.out.println(银联支付 amount); } } Service public class PaymentService { private final Map strategyMap; public PaymentService(List strategies) { this.strategyMap strategies.stream() .collect(Collectors.toMap(PaymentStrategy::getType, Function.identity())); } public void pay(String paymentType, double amount) { PaymentStrategy strategy strategyMap.get(paymentType); if (strategy null) throw new IllegalArgumentException(不支持的支付方式 paymentType); strategy.pay(amount); } } ### 3.5 类比 **USB 接口**统一的 USB 接口 → 新增设备如 U 盘只需让 U 盘实现 USB 接口扩展不用改电脑。 --- ## 四、里氏替换原则LSP ### 4.1 官方定义 **子类对象应该能够替换父类对象而不会影响程序的正确性。** —— Barbara Liskov1987 - **替换Substitution**用子类对象替换父类对象 - **正确性Correctness**程序行为不变不会出错 **通俗理解**只要父类出现的地方替换成子类之后程序的行为应该保持不变。 ### 4.2 在架构中的位置 LSP 专门约束**继承体系**。如果继承体系崩塌开闭原则就无从谈起。LSP 与 OCP 的关系OCP 说对扩展开放——通过继承/实现接口来扩展LSP 说扩展必须合法——子类必须能替换父类否则 OCP 失效。 ### 4.3 核心约束 1. 子类**不能重写**父类的非抽象方法除非是抽象方法 2. 子类**不能扩大**父类方法的前置条件参数要求更严格 3. 子类**不能缩小**父类方法的后置条件返回值更少 ### 4.4 反例正方形继承矩形 java // ❌ 反例正方形继承矩形违反 LSP public class Rectangle { private int width, height; public void setWidth(int width) { this.width width; } public void setHeight(int height) { this.height height; } public int getWidth() { return width; } public int getHeight() { return height; } } public class Square extends Rectangle { Override public void setWidth(int width) { super.setWidth(width); super.setHeight(width); // 强制高 宽 } Override public void setHeight(int height) { super.setWidth(height); super.setHeight(height); // 强制宽 高 } } // 客户端 public static void testRectangle(Rectangle rectangle) { rectangle.setWidth(5); rectangle.setHeight(4); // 期望面积 5 * 4 20 int area rectangle.getWidth() * rectangle.getHeight(); } // 测试正方形替换矩形 → 面积 16不是 20 // ❌ 子类不能替换父类违反 LSP ### 4.5 正例重新设计继承体系 java // ✅ 正例按行为分类 public abstract class Bird { public abstract void move(); } public class FlyingBird extends Bird { Override public void move() { System.out.println(鸟在飞); } } public class Ostrich extends Bird { Override public void move() { System.out.println(鸵鸟在跑); } } // ✅ 所有鸟都能替换 Bird程序行为正确 --- ## 五、接口隔离原则ISP ### 5.1 官方定义 **客户端不应该被强迫依赖于它们不使用的接口。** —— Robert C. Martin - **客户端Client**使用接口的类 - **强迫依赖Forced to depend**不得不实现/依赖不需要的方法 **通俗理解**不要设计大而全的接口应该按职责拆分成多个小接口让客户端只依赖自己需要的方法。 ### 5.2 在架构中的位置 ISP 专门约束**接口设计**。如果接口太臃肿实现类会被迫实现不需要的方法违反单一职责。ISP 与 SRP 的关系SRP 说一个类只做一件事——从实现角度ISP 说一个接口只服务一类客户端——从接口角度。 ### 5.3 反例胖接口 java // ❌ 反例一个胖接口包含所有方法 public interface Vehicle { void start(); // 启动 void stop(); // 停止 void fly(); // 飞行 void swim(); // 游泳 void dive(); // 潜水 } // 汽车被迫实现 fly/swim/dive public class Car implements Vehicle { Override public void start() { System.out.println(汽车启动); } Override public void stop() { System.out.println(汽车停止); } Override public void fly() { throw new UnsupportedOperationException(汽车不会飞); } Override public void swim() { throw new UnsupportedOperationException(汽车不会游泳); } Override public void dive() { throw new UnsupportedOperationException(汽车不会潜水); } } ### 5.4 正例细粒度接口 java // ✅ 正例按职责拆分成多个小接口 public interface BasicFeature { void start(); void stop(); } public interface Movable { void move(); } public interface Flyable { void fly(); } public interface Swimmable { void swim(); } public interface Diviable { void dive(); } // 汽车只实现需要的接口 public class Car implements BasicFeature, Movable { Override public void start() { System.out.println(汽车启动); } Override public void stop() { System.out.println(汽车停止); } Override public void move() { System.out.println(汽车行驶); } } // 飞机只实现需要的接口 public class Airplane implements BasicFeature, Flyable { Override public void start() { System.out.println(飞机启动); } Override public void stop() { System.out.println(飞机停止); } Override public void fly() { System.out.println(飞机飞行); } } --- ## 六、依赖倒置原则DIP ### 6.1 官方定义 **高层模块不应该依赖低层模块两者都应该依赖抽象。抽象不应该依赖细节细节应该依赖抽象。** —— Robert C. Martin - **高层模块High-level modules**包含业务逻辑的模块如 OrderService - **低层模块Low-level modules**提供基础功能的模块如 MySQLRepository - **倒置Inversion**依赖方向从高层→低层变为高层→抽象←低层 **通俗理解**不要直接依赖具体实现要依赖抽象接口。具体实现可以随意替换只要符合接口约定。 ### 6.2 在架构中的位置 DIP 是实现 OCP 的重要途径。它降低了模块间的耦合度使系统更稳定。DIP 需要 ISP 配合接口要细粒度需要 LSP 保证子类必须能替换父类。 ### 6.3 反例直接依赖具体类 java // ❌ 反例高层模块直接依赖低层模块的具体实现 public class MySQLRepository { public void save(Order order) { System.out.println(使用 MySQL 保存订单 order.getId()); } } public class OrderService { private MySQLRepository repository; // ❌ 直接依赖具体类 public OrderService() { this.repository new MySQLRepository(); // ❌ 硬编码创建 } public void createOrder(Order order) { repository.save(order); } } **问题**换数据库必须改 OrderService无法 Mock 测试重用性差。 ### 6.4 正例依赖抽象接口 java // ✅ 正例高层模块和低层模块都依赖抽象 public interface OrderRepository { void save(Order order); } public class MySQLRepository implements OrderRepository { Override public void save(Order order) { System.out.println(MySQL 保存 order.getId()); } } public class PostgreSQLRepository implements OrderRepository { Override public void save(Order order) { System.out.println(PostgreSQL 保存 order.getId()); } } public class OrderService { private OrderRepository repository; // ✅ 依赖抽象接口 public OrderService(OrderRepository repository) { // ✅ 构造函数注入 this.repository repository; } public void createOrder(Order order) { repository.save(order); } } ### 6.5 DIP 与 DI 的关系 - **DIP**设计原则思想—— 依赖抽象接口不依赖具体实现 - **DI**实现方式手段—— 通过构造函数、Setter、接口注入依赖 - DIP 是要做什么DI 是怎么做 --- ## 七、迪米特法则LoD ### 7.1 官方定义 **一个对象应该对其他对象有最少的了解。只与直接的朋友通信不和陌生人说话。** —— Northeastern University1980s - **最少知识Least Knowledge**一个对象只了解自己直接相关的对象 - **直接朋友Direct Friends**通过参数、成员变量、返回值直接关联的对象 - **陌生人Strangers**通过链式调用间接访问的对象 **通俗理解**不要调用一个对象的方法然后通过返回值继续调用另一个对象的方法。只和直接朋友通信不要穿透对象去访问内部结构。**控制信息传递的范围和内容让每个模块都只聚焦在自己的直接职责上不跟无关的模块产生多余的关联。** ### 7.2 在架构中的位置 LoD 不属于 SOLID 五大原则但它是 7 大设计原则之一专门约束**对象间的通信方式**。它与 DIP 配合进一步降低耦合度。DIP 说依赖抽象——从依赖方向解耦LoD 说最少知道——从通信方式解耦。 ### 7.3 判断标准 - ✅ **直接朋友**成员变量、方法参数、返回值、集合中的元素 - ❌ **陌生人**通过链式调用访问的对象如 a.getB().getC() ### 7.4 反例链式调用穿透 java // ❌ 反例穿透对象访问内部结构 public class OrderService { public void printOrderCity(Order order) { // ❌ 违反 LoD链式调用穿透了 Order → Customer → Address String city order.getCustomer().getAddress().getCity(); System.out.println(订单城市 city); } } ### 7.5 正例封装内部结构 java // ✅ 正例每个对象只暴露必要的方法 public class Order { private Customer customer; public Customer getCustomer() { return customer; } // ✅ 新增方法封装获取城市的逻辑 public String getCity() { return customer.getAddress().getCity(); } } public class OrderService { public void printOrderCity(Order order) { // ✅ 只和直接朋友Order通信 String city order.getCity(); System.out.println(订单城市 city); } } --- ## 八、组合复用原则CRP ### 8.1 官方定义 **优先使用对象组合而不是通过继承来达到复用的目的。** - **组合Composition**强拥有关系整体与部分生命周期一致如人与手臂 - **聚合Aggregation**弱拥有关系部分可独立存在如人与人群 - **继承Inheritance**Is-A 关系子类继承父类 **通俗理解**尽量用有一个Has-A的关系而不是是一个Is-A的关系来复用代码。 ### 8.2 在架构中的位置 CRP 专门约束**代码复用方式**。它与 LSP 相辅相成两者都是开闭原则的具体实现规范。 ### 8.3 继承 vs 组合对比 | 维度 | 继承 | 组合 | |------|------|------| | 关系 | Is-A是一个 | Has-A有一个 | | 耦合度 | 高父类变化影响子类 | 低通过接口调用 | | 封装性 | 差白箱复用 | 好黑箱复用 | | 灵活性 | 差编译时确定 | 好运行时可替换 | | 扩展性 | 差需修改代码 | 好符合开闭原则 | | 适用场景 | 严格的 Is-A 关系 | Has-A 关系、功能复用 | ### 8.4 反例用继承复用 java // ❌ 反例用继承复用数据库连接功能 public class DBConnection { public String getConnection() { return MySQL 数据库连接; } } public class UserDao extends DBConnection { public void addUser() { String connection super.getConnection(); // ❌ 调用父类方法 System.out.println(connection); System.out.println(执行数据库操作); } } ### 8.5 正例用组合复用 java // ✅ 正例用组合复用数据库连接功能 public interface DBConnection { String getConnection(); } public class MySQLConnection implements DBConnection { Override public String getConnection() { return MySQL 数据库连接; } } public class OracleConnection implements DBConnection { Override public String getConnection() { return Oracle 数据库连接; } } public class UserDao { private DBConnection connection; // ✅ 组合关系 public UserDao(DBConnection connection) { this.connection connection; } public void addUser() { System.out.println(connection.getConnection()); System.out.println(执行数据库操作); } } ### 8.6 类比 **搭积木**用不同颜色的积木块搭建汽车 → 想改颜色换一块积木。想改动力源换一块积木。灵活组合随时替换。 --- ## 附录面向对象设计七大原则高频面试速答 **Q1. 什么是单一职责原则SRP** 一个类应该只有一个引起它变化的原因。通俗说就是一个类只做一件事。比如用户注册如果把数据库操作、邮件发送、日志记录都写在一个类里需求变更时就要改很多地方容易出错。正确做法是拆分成 UserService、UserRepository、EmailService各司其职互不影响。 **Q2. 什么是开闭原则OCP** 软件实体应该对扩展开放对修改关闭。通俗说就是新增功能时只加代码不改旧代码。比如支付系统如果把支付宝、微信、银联都写在一个 if-else 里新增支付方式就要改这个方法违反 OCP。正确做法是定义 PaymentStrategy 接口每个支付方式实现这个接口新增时只加新类不改旧代码。 **Q3. 什么是里氏替换原则LSP** 子类对象应该能够替换父类对象而不会影响程序的正确性。通俗说就是只要父类出现的地方替换成子类后程序行为不变。经典反例是正方形继承矩形矩形允许宽和高独立变化但正方形强制宽高导致替换后面积计算错误。正确做法是重新设计继承体系或用组合代替继承。 **Q4. 什么是接口隔离原则ISP** 客户端不应该被强迫依赖于它们不使用的接口。通俗说就是不要设计大而全的接口应该按职责拆分成多个小接口。比如交通工具接口如果把 start/stop/fly/swim/dive 都放在一个接口里汽车被迫实现 fly 和 swim只能抛异常。正确做法是拆分成 BasicFeature、Movable、Flyable、Swimmable 等小接口每个实现类只实现需要的方法。 **Q5. 什么是依赖倒置原则DIP** 高层模块不应该依赖低层模块两者都应该依赖抽象。通俗说就是不要直接依赖具体实现要依赖抽象接口。比如订单服务直接依赖 MySQLRepository换数据库就要改代码。正确做法是定义 OrderRepository 接口MySQL 和 PostgreSQL 都实现这个接口订单服务依赖接口通过构造函数注入具体实现。DIP 的核心是面向接口编程配合依赖注入DI使用。 **Q6. 什么是迪米特法则LoD** 一个对象应该对其他对象有最少的了解只和直接朋友通信不和陌生人说话。通俗说就是不要链式调用穿透对象访问内部结构。核心是控制信息传递的范围和内容让每个模块都只聚焦在自己的直接职责上不跟无关的模块产生多余的关联。比如订单服务要获取城市如果写 order.getCustomer().getAddress().getCity()就违反了 LoD。正确做法是在 Order 类里加一个 getCity() 方法订单服务只调用 order.getCity()只和直接朋友Order通信。 **Q7. 什么是组合复用原则CRP** 优先使用对象组合而不是继承来达到复用的目的。通俗说就是尽量用有一个Has-A的关系而不是是一个Is-A的关系。比如汽车有一个发动机用组合麻雀是一种鸟用继承。继承的缺点是破坏封装、高耦合、灵活性差组合的优点是维持封装、低耦合、灵活性高。实际开发中如果是严格的 Is-A 关系且遵循里氏替换原则可以用继承否则优先用组合。 **Q8. SOLID 五大原则是哪些** SSingle Responsibility单一职责、OOpen-Closed开闭、LLiskov Substitution里氏替换、IInterface Segregation接口隔离、DDependency Inversion依赖倒置。其中 OCP 是核心目标其他四个原则都是为实现 OCP 服务的手段。 **Q9. 单一职责和接口隔离有什么区别** SRP 从实现角度说一个类只做一件事ISP 从接口角度说一个接口只服务一类客户端。两者配合接口细粒度 → 实现类职责单一。 **Q10. 依赖倒置和依赖注入有什么区别** DIP 是设计原则思想—— 依赖抽象接口不依赖具体实现DI 是实现方式手段—— 通过构造函数、Setter、接口注入依赖。DIP 是要做什么DI 是怎么做。 **Q11. 什么时候用继承什么时候用组合** 如果是严格的 Is-A 关系且遵循里氏替换原则用继承否则优先用组合。继承的缺点是破坏封装、高耦合、灵活性差组合的优点是维持封装、低耦合、灵活性高。 **Q12. 开闭原则的核心是什么** 对扩展开放对修改关闭。新增功能时只加代码不改旧代码。通过抽象和多态实现比如定义接口新增功能时实现接口即可。 **Q13. 里氏替换原则的核心约束是什么** 子类不能重写父类的非抽象方法子类不能扩大父类方法的前置条件参数要求更严格子类不能缩小父类方法的后置条件返回值更少。 **Q14. 迪米特法则中的直接朋友和陌生人怎么区分** 直接朋友成员变量、方法参数、返回值、集合中的元素。陌生人通过链式调用访问的对象如 a.getB().getC()。 **Q15. 七大原则的终极目标是什么** 降低耦合提高内聚使系统更易扩展、更易维护。 --- ## 写在最后 本文系统梳理了面向对象设计的七大原则从 SOLID 五大原则到迪米特法则和组合复用原则每个原则都给出了定义、代码示例和易错点。 **推荐学习路径**本文 → 下一篇23 种设计模式 **下一篇预告**23 种设计模式分类与核心模式详解创建型、结构型、行为型模式的完整实践指南。
面向对象设计七大原则:从 SOLID 到迪米特法则的完整实践指南(附 15 道高频题)
发布时间:2026/7/13 1:08:21
本文是设计模式系列的起点沿 **「SRP → OCP → LSP → ISP → DIP → LoD → CRP」** 这条主线系统梳理面向对象设计的七大原则为后续学习 23 种设计模式打下坚实基础。 --- ## 一、模块总览设计原则学什么 | 原则 | 缩写 | 核心思想 | 一句话总结 | 本文覆盖 | |------|------|---------|-----------|---------| | 单一职责原则 | SRP | 一个类只做一件事 | 职责单一易于维护 | ✅ | | 开闭原则 | OCP | 对扩展开放对修改关闭 | 新增功能不改旧代码 | ✅ | | 里氏替换原则 | LSP | 子类能替换父类 | 继承体系不崩塌 | ✅ | | 接口隔离原则 | ISP | 接口按需拆分 | 不强迫依赖不用的方法 | ✅ | | 依赖倒置原则 | DIP | 依赖抽象不依赖具体 | 面向接口编程 | ✅ | | 迪米特法则 | LoD | 最少知道只和直接朋友通信 | 控制信息传递范围 | ✅ | | 组合复用原则 | CRP | 优先组合而非继承 | Has-A 优于 Is-A | ✅ | ### 设计原则金字塔 开闭原则OCP← 核心目标扩展不改旧代码 ───────────── 单一职责SRP← 基础类职责清晰才能谈扩展 里氏替换LSP← 继承体系不崩塌 接口隔离ISP← 接口按需拆分 依赖倒置DIP← 模块间解耦 ───────────── 迪米特法则LoD← 最少知道 组合复用CRP← 优先组合而非继承 **面试加分句**OCP 是核心目标其他原则都是为实现 OCP 服务的手段。 --- ## 二、单一职责原则SRP ### 2.1 官方定义 **一个类应该只有一个引起它变化的原因。** —— Robert C. Martin - **职责Responsibility**引起类变化的原因 - **变化原因Reason to change**需求变更时需要修改这个类的理由 **通俗理解**一个类只做一件事且只做这一件事。 ### 2.2 在架构中的位置 SRP 是 SOLID 五大原则之首也是 7 大设计原则中最基础的一个。它是类的**内聚基准线**——如果一个类职责太多后续的开闭原则、依赖倒置都无从谈起。 ### 2.3 怎么判断是否违反 SRP **判断标准**如果需求变更时你需要修改这个类的**多个不相关的理由**那就违反了 SRP。 ### 2.4 反例违反 SRP java // ❌ 反例一个类承担了三个职责 public class UserService { public void register(String username, String password) { // 1. 校验用户名 if (username null || username.length() 6) { throw new IllegalArgumentException(用户名至少 6 位); } // 2. 密码加密 String encryptedPwd DigestUtils.md5DigestAsHex(password.getBytes()); // 3. 保存到数据库直接操作 JDBC Connection conn DriverManager.getConnection(jdbc:mysql://localhost:3306/db, root, 123456); PreparedStatement stmt conn.prepareStatement(INSERT INTO user (username, password) VALUES (?, ?)); stmt.setString(1, username); stmt.setString(2, encryptedPwd); stmt.executeUpdate(); // 4. 发送欢迎邮件 sendWelcomeEmail(username); // 5. 记录日志 System.out.println(用户注册成功 username); } } **问题分析** - 数据库连接方式变更 → 需要修改 UserService - 邮件发送方式变更 → 需要修改 UserService - 日志格式变更 → 需要修改 UserService - 三个变化原因违反 SRP ### 2.5 正例遵循 SRP java // ✅ 正例拆分为三个类各司其职 Service public class UserService { private final UserRepository userRepository; private final EmailService emailService; private final Logger logger LoggerFactory.getLogger(UserService.class); public UserService(UserRepository userRepository, EmailService emailService) { this.userRepository userRepository; this.emailService emailService; } public void register(String username, String password) { validateUsername(username); String encryptedPwd PasswordEncoder.encode(password); User user new User(username, encryptedPwd); userRepository.save(user); emailService.sendWelcomeEmail(username); logger.info(用户注册成功{}, username); } private void validateUsername(String username) { if (username null || username.length() 6) { throw new IllegalArgumentException(用户名至少 6 位); } } } Repository public class UserRepository { public void save(User user) { /* 数据库操作 */ } } Service public class EmailService { public void sendWelcomeEmail(String username) { /* 发送邮件 */ } } ### 2.6 易错点 | 易错点 | 说明 | |--------|------| | 过度拆分 | 不是类越小越好职责内聚即可 | | 职责模糊 | 用户管理算一个职责还是多个判断标准变化原因是否独立 | | 工具类滥用 | Utils 类容易变成垃圾桶什么方法都往里塞 | --- ## 三、开闭原则OCP ### 3.1 官方定义 **软件实体类、模块、函数应该对扩展开放对修改关闭。** —— Bertrand Meyer1988 - **对扩展开放Open for extension**允许通过新增代码来扩展功能 - **对修改关闭Closed for modification**不允许修改已有代码来改变行为 **通俗理解**新增功能时**只加代码不改旧代码**。 ### 3.2 在架构中的位置 开闭原则是 SOLID 五大原则的**核心目标**其他四个原则SRP、LSP、ISP、DIP都是为实现 OCP 服务的手段。 ### 3.3 反例违反 OCP java // ❌ 反例每次新增支付方式都要修改 PaymentService public class PaymentService { public void pay(String paymentType, double amount) { if (alipay.equals(paymentType)) { System.out.println(使用支付宝支付 amount 元); } else if (wechat.equals(paymentType)) { System.out.println(使用微信支付 amount 元); } else if (unionpay.equals(paymentType)) { // ❌ 每次新增支付方式都要改这个方法 System.out.println(使用银联支付 amount 元); } } } ### 3.4 正例遵循 OCP java // ✅ 正例通过接口 多态新增支付方式无需修改旧代码 public interface PaymentStrategy { String getType(); void pay(double amount); } Component public class AlipayStrategy implements PaymentStrategy { Override public String getType() { return alipay; } Override public void pay(double amount) { System.out.println(支付宝支付 amount); } } Component public class WechatPayStrategy implements PaymentStrategy { Override public String getType() { return wechat; } Override public void pay(double amount) { System.out.println(微信支付 amount); } } // 新增银联支付只加新类不改 PaymentService Component public class UnionPayStrategy implements PaymentStrategy { Override public String getType() { return unionpay; } Override public void pay(double amount) { System.out.println(银联支付 amount); } } Service public class PaymentService { private final Map strategyMap; public PaymentService(List strategies) { this.strategyMap strategies.stream() .collect(Collectors.toMap(PaymentStrategy::getType, Function.identity())); } public void pay(String paymentType, double amount) { PaymentStrategy strategy strategyMap.get(paymentType); if (strategy null) throw new IllegalArgumentException(不支持的支付方式 paymentType); strategy.pay(amount); } } ### 3.5 类比 **USB 接口**统一的 USB 接口 → 新增设备如 U 盘只需让 U 盘实现 USB 接口扩展不用改电脑。 --- ## 四、里氏替换原则LSP ### 4.1 官方定义 **子类对象应该能够替换父类对象而不会影响程序的正确性。** —— Barbara Liskov1987 - **替换Substitution**用子类对象替换父类对象 - **正确性Correctness**程序行为不变不会出错 **通俗理解**只要父类出现的地方替换成子类之后程序的行为应该保持不变。 ### 4.2 在架构中的位置 LSP 专门约束**继承体系**。如果继承体系崩塌开闭原则就无从谈起。LSP 与 OCP 的关系OCP 说对扩展开放——通过继承/实现接口来扩展LSP 说扩展必须合法——子类必须能替换父类否则 OCP 失效。 ### 4.3 核心约束 1. 子类**不能重写**父类的非抽象方法除非是抽象方法 2. 子类**不能扩大**父类方法的前置条件参数要求更严格 3. 子类**不能缩小**父类方法的后置条件返回值更少 ### 4.4 反例正方形继承矩形 java // ❌ 反例正方形继承矩形违反 LSP public class Rectangle { private int width, height; public void setWidth(int width) { this.width width; } public void setHeight(int height) { this.height height; } public int getWidth() { return width; } public int getHeight() { return height; } } public class Square extends Rectangle { Override public void setWidth(int width) { super.setWidth(width); super.setHeight(width); // 强制高 宽 } Override public void setHeight(int height) { super.setWidth(height); super.setHeight(height); // 强制宽 高 } } // 客户端 public static void testRectangle(Rectangle rectangle) { rectangle.setWidth(5); rectangle.setHeight(4); // 期望面积 5 * 4 20 int area rectangle.getWidth() * rectangle.getHeight(); } // 测试正方形替换矩形 → 面积 16不是 20 // ❌ 子类不能替换父类违反 LSP ### 4.5 正例重新设计继承体系 java // ✅ 正例按行为分类 public abstract class Bird { public abstract void move(); } public class FlyingBird extends Bird { Override public void move() { System.out.println(鸟在飞); } } public class Ostrich extends Bird { Override public void move() { System.out.println(鸵鸟在跑); } } // ✅ 所有鸟都能替换 Bird程序行为正确 --- ## 五、接口隔离原则ISP ### 5.1 官方定义 **客户端不应该被强迫依赖于它们不使用的接口。** —— Robert C. Martin - **客户端Client**使用接口的类 - **强迫依赖Forced to depend**不得不实现/依赖不需要的方法 **通俗理解**不要设计大而全的接口应该按职责拆分成多个小接口让客户端只依赖自己需要的方法。 ### 5.2 在架构中的位置 ISP 专门约束**接口设计**。如果接口太臃肿实现类会被迫实现不需要的方法违反单一职责。ISP 与 SRP 的关系SRP 说一个类只做一件事——从实现角度ISP 说一个接口只服务一类客户端——从接口角度。 ### 5.3 反例胖接口 java // ❌ 反例一个胖接口包含所有方法 public interface Vehicle { void start(); // 启动 void stop(); // 停止 void fly(); // 飞行 void swim(); // 游泳 void dive(); // 潜水 } // 汽车被迫实现 fly/swim/dive public class Car implements Vehicle { Override public void start() { System.out.println(汽车启动); } Override public void stop() { System.out.println(汽车停止); } Override public void fly() { throw new UnsupportedOperationException(汽车不会飞); } Override public void swim() { throw new UnsupportedOperationException(汽车不会游泳); } Override public void dive() { throw new UnsupportedOperationException(汽车不会潜水); } } ### 5.4 正例细粒度接口 java // ✅ 正例按职责拆分成多个小接口 public interface BasicFeature { void start(); void stop(); } public interface Movable { void move(); } public interface Flyable { void fly(); } public interface Swimmable { void swim(); } public interface Diviable { void dive(); } // 汽车只实现需要的接口 public class Car implements BasicFeature, Movable { Override public void start() { System.out.println(汽车启动); } Override public void stop() { System.out.println(汽车停止); } Override public void move() { System.out.println(汽车行驶); } } // 飞机只实现需要的接口 public class Airplane implements BasicFeature, Flyable { Override public void start() { System.out.println(飞机启动); } Override public void stop() { System.out.println(飞机停止); } Override public void fly() { System.out.println(飞机飞行); } } --- ## 六、依赖倒置原则DIP ### 6.1 官方定义 **高层模块不应该依赖低层模块两者都应该依赖抽象。抽象不应该依赖细节细节应该依赖抽象。** —— Robert C. Martin - **高层模块High-level modules**包含业务逻辑的模块如 OrderService - **低层模块Low-level modules**提供基础功能的模块如 MySQLRepository - **倒置Inversion**依赖方向从高层→低层变为高层→抽象←低层 **通俗理解**不要直接依赖具体实现要依赖抽象接口。具体实现可以随意替换只要符合接口约定。 ### 6.2 在架构中的位置 DIP 是实现 OCP 的重要途径。它降低了模块间的耦合度使系统更稳定。DIP 需要 ISP 配合接口要细粒度需要 LSP 保证子类必须能替换父类。 ### 6.3 反例直接依赖具体类 java // ❌ 反例高层模块直接依赖低层模块的具体实现 public class MySQLRepository { public void save(Order order) { System.out.println(使用 MySQL 保存订单 order.getId()); } } public class OrderService { private MySQLRepository repository; // ❌ 直接依赖具体类 public OrderService() { this.repository new MySQLRepository(); // ❌ 硬编码创建 } public void createOrder(Order order) { repository.save(order); } } **问题**换数据库必须改 OrderService无法 Mock 测试重用性差。 ### 6.4 正例依赖抽象接口 java // ✅ 正例高层模块和低层模块都依赖抽象 public interface OrderRepository { void save(Order order); } public class MySQLRepository implements OrderRepository { Override public void save(Order order) { System.out.println(MySQL 保存 order.getId()); } } public class PostgreSQLRepository implements OrderRepository { Override public void save(Order order) { System.out.println(PostgreSQL 保存 order.getId()); } } public class OrderService { private OrderRepository repository; // ✅ 依赖抽象接口 public OrderService(OrderRepository repository) { // ✅ 构造函数注入 this.repository repository; } public void createOrder(Order order) { repository.save(order); } } ### 6.5 DIP 与 DI 的关系 - **DIP**设计原则思想—— 依赖抽象接口不依赖具体实现 - **DI**实现方式手段—— 通过构造函数、Setter、接口注入依赖 - DIP 是要做什么DI 是怎么做 --- ## 七、迪米特法则LoD ### 7.1 官方定义 **一个对象应该对其他对象有最少的了解。只与直接的朋友通信不和陌生人说话。** —— Northeastern University1980s - **最少知识Least Knowledge**一个对象只了解自己直接相关的对象 - **直接朋友Direct Friends**通过参数、成员变量、返回值直接关联的对象 - **陌生人Strangers**通过链式调用间接访问的对象 **通俗理解**不要调用一个对象的方法然后通过返回值继续调用另一个对象的方法。只和直接朋友通信不要穿透对象去访问内部结构。**控制信息传递的范围和内容让每个模块都只聚焦在自己的直接职责上不跟无关的模块产生多余的关联。** ### 7.2 在架构中的位置 LoD 不属于 SOLID 五大原则但它是 7 大设计原则之一专门约束**对象间的通信方式**。它与 DIP 配合进一步降低耦合度。DIP 说依赖抽象——从依赖方向解耦LoD 说最少知道——从通信方式解耦。 ### 7.3 判断标准 - ✅ **直接朋友**成员变量、方法参数、返回值、集合中的元素 - ❌ **陌生人**通过链式调用访问的对象如 a.getB().getC() ### 7.4 反例链式调用穿透 java // ❌ 反例穿透对象访问内部结构 public class OrderService { public void printOrderCity(Order order) { // ❌ 违反 LoD链式调用穿透了 Order → Customer → Address String city order.getCustomer().getAddress().getCity(); System.out.println(订单城市 city); } } ### 7.5 正例封装内部结构 java // ✅ 正例每个对象只暴露必要的方法 public class Order { private Customer customer; public Customer getCustomer() { return customer; } // ✅ 新增方法封装获取城市的逻辑 public String getCity() { return customer.getAddress().getCity(); } } public class OrderService { public void printOrderCity(Order order) { // ✅ 只和直接朋友Order通信 String city order.getCity(); System.out.println(订单城市 city); } } --- ## 八、组合复用原则CRP ### 8.1 官方定义 **优先使用对象组合而不是通过继承来达到复用的目的。** - **组合Composition**强拥有关系整体与部分生命周期一致如人与手臂 - **聚合Aggregation**弱拥有关系部分可独立存在如人与人群 - **继承Inheritance**Is-A 关系子类继承父类 **通俗理解**尽量用有一个Has-A的关系而不是是一个Is-A的关系来复用代码。 ### 8.2 在架构中的位置 CRP 专门约束**代码复用方式**。它与 LSP 相辅相成两者都是开闭原则的具体实现规范。 ### 8.3 继承 vs 组合对比 | 维度 | 继承 | 组合 | |------|------|------| | 关系 | Is-A是一个 | Has-A有一个 | | 耦合度 | 高父类变化影响子类 | 低通过接口调用 | | 封装性 | 差白箱复用 | 好黑箱复用 | | 灵活性 | 差编译时确定 | 好运行时可替换 | | 扩展性 | 差需修改代码 | 好符合开闭原则 | | 适用场景 | 严格的 Is-A 关系 | Has-A 关系、功能复用 | ### 8.4 反例用继承复用 java // ❌ 反例用继承复用数据库连接功能 public class DBConnection { public String getConnection() { return MySQL 数据库连接; } } public class UserDao extends DBConnection { public void addUser() { String connection super.getConnection(); // ❌ 调用父类方法 System.out.println(connection); System.out.println(执行数据库操作); } } ### 8.5 正例用组合复用 java // ✅ 正例用组合复用数据库连接功能 public interface DBConnection { String getConnection(); } public class MySQLConnection implements DBConnection { Override public String getConnection() { return MySQL 数据库连接; } } public class OracleConnection implements DBConnection { Override public String getConnection() { return Oracle 数据库连接; } } public class UserDao { private DBConnection connection; // ✅ 组合关系 public UserDao(DBConnection connection) { this.connection connection; } public void addUser() { System.out.println(connection.getConnection()); System.out.println(执行数据库操作); } } ### 8.6 类比 **搭积木**用不同颜色的积木块搭建汽车 → 想改颜色换一块积木。想改动力源换一块积木。灵活组合随时替换。 --- ## 附录面向对象设计七大原则高频面试速答 **Q1. 什么是单一职责原则SRP** 一个类应该只有一个引起它变化的原因。通俗说就是一个类只做一件事。比如用户注册如果把数据库操作、邮件发送、日志记录都写在一个类里需求变更时就要改很多地方容易出错。正确做法是拆分成 UserService、UserRepository、EmailService各司其职互不影响。 **Q2. 什么是开闭原则OCP** 软件实体应该对扩展开放对修改关闭。通俗说就是新增功能时只加代码不改旧代码。比如支付系统如果把支付宝、微信、银联都写在一个 if-else 里新增支付方式就要改这个方法违反 OCP。正确做法是定义 PaymentStrategy 接口每个支付方式实现这个接口新增时只加新类不改旧代码。 **Q3. 什么是里氏替换原则LSP** 子类对象应该能够替换父类对象而不会影响程序的正确性。通俗说就是只要父类出现的地方替换成子类后程序行为不变。经典反例是正方形继承矩形矩形允许宽和高独立变化但正方形强制宽高导致替换后面积计算错误。正确做法是重新设计继承体系或用组合代替继承。 **Q4. 什么是接口隔离原则ISP** 客户端不应该被强迫依赖于它们不使用的接口。通俗说就是不要设计大而全的接口应该按职责拆分成多个小接口。比如交通工具接口如果把 start/stop/fly/swim/dive 都放在一个接口里汽车被迫实现 fly 和 swim只能抛异常。正确做法是拆分成 BasicFeature、Movable、Flyable、Swimmable 等小接口每个实现类只实现需要的方法。 **Q5. 什么是依赖倒置原则DIP** 高层模块不应该依赖低层模块两者都应该依赖抽象。通俗说就是不要直接依赖具体实现要依赖抽象接口。比如订单服务直接依赖 MySQLRepository换数据库就要改代码。正确做法是定义 OrderRepository 接口MySQL 和 PostgreSQL 都实现这个接口订单服务依赖接口通过构造函数注入具体实现。DIP 的核心是面向接口编程配合依赖注入DI使用。 **Q6. 什么是迪米特法则LoD** 一个对象应该对其他对象有最少的了解只和直接朋友通信不和陌生人说话。通俗说就是不要链式调用穿透对象访问内部结构。核心是控制信息传递的范围和内容让每个模块都只聚焦在自己的直接职责上不跟无关的模块产生多余的关联。比如订单服务要获取城市如果写 order.getCustomer().getAddress().getCity()就违反了 LoD。正确做法是在 Order 类里加一个 getCity() 方法订单服务只调用 order.getCity()只和直接朋友Order通信。 **Q7. 什么是组合复用原则CRP** 优先使用对象组合而不是继承来达到复用的目的。通俗说就是尽量用有一个Has-A的关系而不是是一个Is-A的关系。比如汽车有一个发动机用组合麻雀是一种鸟用继承。继承的缺点是破坏封装、高耦合、灵活性差组合的优点是维持封装、低耦合、灵活性高。实际开发中如果是严格的 Is-A 关系且遵循里氏替换原则可以用继承否则优先用组合。 **Q8. SOLID 五大原则是哪些** SSingle Responsibility单一职责、OOpen-Closed开闭、LLiskov Substitution里氏替换、IInterface Segregation接口隔离、DDependency Inversion依赖倒置。其中 OCP 是核心目标其他四个原则都是为实现 OCP 服务的手段。 **Q9. 单一职责和接口隔离有什么区别** SRP 从实现角度说一个类只做一件事ISP 从接口角度说一个接口只服务一类客户端。两者配合接口细粒度 → 实现类职责单一。 **Q10. 依赖倒置和依赖注入有什么区别** DIP 是设计原则思想—— 依赖抽象接口不依赖具体实现DI 是实现方式手段—— 通过构造函数、Setter、接口注入依赖。DIP 是要做什么DI 是怎么做。 **Q11. 什么时候用继承什么时候用组合** 如果是严格的 Is-A 关系且遵循里氏替换原则用继承否则优先用组合。继承的缺点是破坏封装、高耦合、灵活性差组合的优点是维持封装、低耦合、灵活性高。 **Q12. 开闭原则的核心是什么** 对扩展开放对修改关闭。新增功能时只加代码不改旧代码。通过抽象和多态实现比如定义接口新增功能时实现接口即可。 **Q13. 里氏替换原则的核心约束是什么** 子类不能重写父类的非抽象方法子类不能扩大父类方法的前置条件参数要求更严格子类不能缩小父类方法的后置条件返回值更少。 **Q14. 迪米特法则中的直接朋友和陌生人怎么区分** 直接朋友成员变量、方法参数、返回值、集合中的元素。陌生人通过链式调用访问的对象如 a.getB().getC()。 **Q15. 七大原则的终极目标是什么** 降低耦合提高内聚使系统更易扩展、更易维护。 --- ## 写在最后 本文系统梳理了面向对象设计的七大原则从 SOLID 五大原则到迪米特法则和组合复用原则每个原则都给出了定义、代码示例和易错点。 **推荐学习路径**本文 → 下一篇23 种设计模式 **下一篇预告**23 种设计模式分类与核心模式详解创建型、结构型、行为型模式的完整实践指南。