六大设计原则深度解析
面试官:你了解 SOLID 原则吗?能说说它们的核心思想吗?
你:SOLID 是五个面向对象设计原则的首字母缩写:单一职责、开闭、里氏替换、接口隔离、依赖倒置。
面试官:好,那你能结合 Spring 框架,具体说说依赖倒置原则是如何体现的吗?
这个问题考察的是对原则的真正理解。能结合框架源码说明,是这类问题的满分答案。
链式追问一:单一职责原则(SRP)
Section titled “链式追问一:单一职责原则(SRP)”Q1:单一职责原则是什么?如何判断一个类是否违反 SRP?必考
Section titled “Q1:单一职责原则是什么?如何判断一个类是否违反 SRP?”定义:一个类应该只有一个引起它变化的原因。即一个类只做一件事。
判断标准:
- 是否有多个职责?
- 是否有多个变化的理由?
- 类名能否准确描述其功能?
违反 SRP 的典型案例:
// ❌ 违反 SRP:UserService 承担了太多职责public class UserService { public void register(User user) { // 职责1:用户业务逻辑 userRepository.save(user);
// 职责2:发送邮件(不应该在这里!) String emailBody = "欢迎注册," + user.getName(); emailService.send(user.getEmail(), emailBody);
// 职责3:记录日志(也不应该在这里!) logger.info("用户注册:" + user.getId());
// 职责4:发送短信通知 smsService.send(user.getPhone(), "注册成功"); }}符合 SRP 的重构:
// ✅ 符合 SRP:UserService 只负责用户业务public class UserService { private final EventPublisher eventPublisher;
public void register(User user) { userRepository.save(user); // 通过事件机制解耦 eventPublisher.publish(new UserRegisteredEvent(user)); }}
// ✅ 邮件监听器:只负责发送邮件@Componentpublic class EmailListener { @EventListener public void onUserRegistered(UserRegisteredEvent event) { emailService.sendWelcomeEmail(event.getUser()); }}
// ✅ 短信监听器:只负责发送短信@Componentpublic class SmsListener { @EventListener public void onUserRegistered(UserRegisteredEvent event) { smsService.sendNotification(event.getUser()); }}职责拆分对比:
| 维度 | 违反 SRP | 符合 SRP |
|---|---|---|
| 类的职责 | 多个(业务+邮件+日志+短信) | 单一(只有用户业务) |
| 变化原因 | 多个(邮件模板变了要改、日志格式变了要改) | 单一(只有用户业务逻辑变化) |
| 测试复杂度 | 高(需要 mock 多个依赖) | 低(只需测试核心业务) |
| 可维护性 | 低(改一处可能影响其他功能) | 高(职责清晰,改动风险小) |
Q2:过度拆分会不会导致类爆炸?如何权衡?高频
Section titled “Q2:过度拆分会不会导致类爆炸?如何权衡?”关键原则:看变化的频率和方向。
┌─────────────────────────────────────────────┐│ 类的职责判断决策树 │├─────────────────────────────────────────────┤│ ││ 两个职责是否总是一起变化? ││ │ ││ ├── 是 → 可以放在一起 ││ │ ││ └── 否 → 是否有不同的变化频率? ││ │ ││ ├── 是 → 应该拆分 ││ │ ││ └── 否 → 暂时可以放在一起 ││ │└─────────────────────────────────────────────┘实战案例:订单服务的设计
// ❌ 拆分过度:每个操作都拆成独立类public class OrderCreateService { }public class OrderUpdateService { }public class OrderDeleteService { }public class OrderQueryService { }// 问题:这些操作总是一起变化,拆分反而增加复杂度
// ✅ 合理拆分:按真正的职责划分public class OrderService { // 订单业务 public Order create() { } public Order update() { } public void cancel() { }}
public class OrderPaymentService { // 支付业务(独立变化) public void pay() { } public void refund() { }}
public class OrderDeliveryService { // 物流业务(独立变化) public void ship() { } public void track() { }}本质一句话:SRP 不是追求类的数量最少,而是让每个类只有一个变化的理由。
链式追问二:开闭原则(OCP)
Section titled “链式追问二:开闭原则(OCP)”Q3:开闭原则是什么?如何在不修改代码的情况下扩展功能?必考
Section titled “Q3:开闭原则是什么?如何在不修改代码的情况下扩展功能?”定义:对扩展开放,对修改关闭。新增功能通过扩展实现,而不是修改已有代码。
违反 OCP 的典型场景:
// ❌ 违反 OCP:每次新增形状都要修改这个方法public class AreaCalculator { public double calculate(Shape shape) { if (shape.getType().equals("circle")) { return Math.PI * shape.getRadius() * shape.getRadius(); } else if (shape.getType().equals("rectangle")) { return shape.getWidth() * shape.getHeight(); } else if (shape.getType().equals("triangle")) { return 0.5 * shape.getBase() * shape.getHeight(); } // 每次新增形状都要修改这里!违反 OCP throw new IllegalArgumentException("未知形状"); }}符合 OCP 的设计:
// ✅ 符合 OCP:通过多态扩展public interface Shape { double area(); // 每种形状自己实现}
public class Circle implements Shape { private final double radius;
@Override public double area() { return Math.PI * radius * radius; }}
public class Rectangle implements Shape { private final double width; private final double height;
@Override public double area() { return width * height; }}
// 新增形状:只需新增类,无需修改已有代码public class Triangle implements Shape { private final double base; private final double height;
@Override public double area() { return 0.5 * base * height; }}
// 计算器永远不需要修改!public class AreaCalculator { public double calculate(Shape shape) { return shape.area(); // 对扩展开放,对修改关闭 }}OCP 的实现手段对比:
| 实现方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 多态/继承 | 行为扩展 | 简单直观 | 继承层次过深 |
| 策略模式 | 算法切换 | 灵活可配置 | 客户端需知道策略 |
| 装饰器模式 | 功能叠加 | 动态组合 | 层次过多时复杂 |
| 观察者模式 | 事件驱动 | 解耦彻底 | 调试困难 |
Spring 中的 OCP 体现:
┌────────────────────────────────────────────────┐│ Spring 的扩展点设计 │├────────────────────────────────────────────────┤│ ││ BeanPostProcessor ││ ├── 扩展点:Bean 初始化前后 ││ ├── 示例:@Autowired 注解处理 ││ └── 不修改 Spring 源码,通过接口扩展 ││ ││ BeanFactoryPostProcessor ││ ├── 扩展点:Bean 定义加载后 ││ ├── 示例:PropertyPlaceholderConfigurer ││ └── 不修改 Spring 源码,通过接口扩展 ││ ││ HandlerInterceptor ││ ├── 扩展点:请求处理前后 ││ ├── 示例:权限校验、日志记录 ││ └── 不修改 DispatcherServlet,通过接口扩展 ││ │└────────────────────────────────────────────────┘链式追问三:里氏替换原则(LSP)
Section titled “链式追问三:里氏替换原则(LSP)”Q4:里氏替换原则是什么?正方形继承长方形为什么违反 LSP?高频
Section titled “Q4:里氏替换原则是什么?正方形继承长方形为什么违反 LSP?”定义:子类必须能够替换父类而不破坏程序的正确性。即:父类出现的地方,子类一定能用。
经典违反案例:正方形继承长方形:
// 父类:长方形public class Rectangle { protected int width; protected int height;
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int getArea() { return width * height; }}
// ❌ 违反 LSP:正方形继承长方形public class Square extends Rectangle { @Override public void setWidth(int width) { this.width = width; this.height = width; // 强制宽高相等 }
@Override public void setHeight(int height) { this.width = height; // 强制宽高相等 this.height = height; }}
// 客户端代码(为 Rectangle 设计)public void resize(Rectangle rectangle) { rectangle.setWidth(5); rectangle.setHeight(3); // 期望:面积 = 15 assert rectangle.getArea() == 15; // ❌ 对 Square 失败!面积 = 9}问题根源:
Rectangle 契约: setWidth(w) → 只改变宽度,高度不变 setHeight(h) → 只改变高度,宽度不变
Square 违反契约: setWidth(w) → 改变宽度,同时改变高度(违反契约!) setHeight(h) → 改变高度,同时改变宽度(违反契约!)
LSP 核心要求: 子类不能违反父类的行为契约正确的设计:
// ✅ 方案1:不继承,独立定义public class Square { private int side;
public void setSide(int side) { this.side = side; }
public int getArea() { return side * side; }}
// ✅ 方案2:提取公共接口public interface Shape { int getArea();}
public class Rectangle implements Shape { // ...}
public class Square implements Shape { // ...}LSP 的核心约束:
| 约束类型 | 说明 | 违反后果 |
|---|---|---|
| 前置条件 | 子类方法不能要求更严格的输入 | 客户端传入父类接受的参数,子类却拒绝 |
| 后置条件 | 子类方法不能保证更弱的输出 | 客户端期望父类的返回结果,子类却返回不符合要求的结果 |
| 不变式 | 子类不能破坏父类的不变式 | 父类保证的状态约束被子类打破 |
链式追问四:接口隔离原则(ISP)
Section titled “链式追问四:接口隔离原则(ISP)”Q5:接口隔离原则是什么?与单一职责原则有什么区别?常考
Section titled “Q5:接口隔离原则是什么?与单一职责原则有什么区别?”定义:客户端不应被迫依赖它不使用的方法。将大接口拆分为小而专一的接口。
违反 ISP 的案例:
// ❌ 违反 ISP:一个"胖接口",强迫实现类实现不需要的方法public interface Animal { void eat(); void fly(); // 狗不会飞 void swim(); // 鸟不会游泳 void run(); // 鱼不会跑}
// Dog 被迫实现不需要的方法public class Dog implements Animal { @Override public void eat() { /* 正常实现 */ }
@Override public void fly() { throw new UnsupportedOperationException("狗不会飞"); // ❌ 被迫抛异常 }
@Override public void swim() { /* 正常实现 */ }
@Override public void run() { /* 正常实现 */ }}符合 ISP 的设计:
// ✅ 符合 ISP:小接口,按能力拆分public interface Eatable { void eat();}
public interface Flyable { void fly();}
public interface Swimmable { void swim();}
public interface Runnable { void run();}
// 类按需组合接口public class Dog implements Eatable, Swimmable, Runnable { @Override public void eat() { /* 吃东西 */ }
@Override public void swim() { /* 游泳 */ }
@Override public void run() { /* 奔跑 */ }}
public class Bird implements Eatable, Flyable { @Override public void eat() { /* 吃东西 */ }
@Override public void fly() { /* 飞翔 */ }}
public class Duck implements Eatable, Flyable, Swimmable { // 鸭子:能吃、能飞、能游泳}ISP vs SRP 对比:
| 对比维度 | 单一职责原则(SRP) | 接口隔离原则(ISP) |
|---|---|---|
| 关注点 | 类的职责 | 接口的方法 |
| 目的 | 降低类的复杂度 | 降低接口的耦合度 |
| 判断标准 | 一个类只有一个变化原因 | 客户端不应依赖不需要的方法 |
| 重构方式 | 拆分类 | 拆分接口 |
| 应用层面 | 类的设计 | 接口的设计 |
Spring 中的 ISP 实践:
// Spring 的事件监听器接口:高度隔离@FunctionalInterfacepublic interface ApplicationListener<E extends ApplicationEvent> { void onApplicationEvent(E event); // 只有一个方法!}
// Spring 的 BeanPostProcessor:接口方法也是高度内聚public interface BeanPostProcessor { default Object postProcessBeforeInitialization(Object bean, String beanName) { return bean; }
default Object postProcessAfterInitialization(Object bean, String beanName) { return bean; }}链式追问五:依赖倒置原则(DIP)
Section titled “链式追问五:依赖倒置原则(DIP)”Q6:依赖倒置原则是什么?Spring IoC 如何体现 DIP?必考
Section titled “Q6:依赖倒置原则是什么?Spring IoC 如何体现 DIP?”定义:
- 高层模块不应依赖低层模块,两者都应依赖抽象
- 抽象不应依赖细节,细节应依赖抽象
违反 DIP 的案例:
// ❌ 违反 DIP:高层模块直接依赖低层模块public class UserService { // 直接依赖具体实现:MySQL private MySQLUserRepository repository = new MySQLUserRepository();
public User findById(Long id) { return repository.findById(id); }
// 换数据库?必须修改 UserService!}符合 DIP 的设计:
// ✅ 符合 DIP:依赖抽象接口public interface UserRepository { User findById(Long id); void save(User user);}
// 高层模块依赖抽象public class UserService { private final UserRepository repository; // 依赖接口,不依赖具体
// 构造器注入 public UserService(UserRepository repository) { this.repository = repository; }
public User findById(Long id) { return repository.findById(id); }}
// 低层模块实现抽象public class MySQLUserRepository implements UserRepository { @Override public User findById(Long id) { /* MySQL 实现 */ }}
public class MongoUserRepository implements UserRepository { @Override public User findById(Long id) { /* MongoDB 实现 */ }}
// 切换数据库:只需修改注入的实现类,UserService 无需改动UserService service = new UserService(new MongoUserRepository());Spring IoC 完美体现 DIP:
┌────────────────────────────────────────────────┐│ Spring IoC 的 DIP 实践 │├────────────────────────────────────────────────┤│ ││ 传统方式(违反 DIP): ││ UserService ──创建──> MySQLRepository ││ (高层直接依赖低层) ││ ││ Spring IoC(符合 DIP): ││ UserService ──依赖──> UserRepository ││ ▲ ││ │ 实现 ││ ┌─────────┴─────────┐ ││ │ │ ││ MySQLRepository MongoRepository││ ││ Spring 容器负责: ││ 1. 创建所有 Bean(低层模块) ││ 2. 注入依赖到高层模块 ││ 3. 高层模块不知道具体实现类 ││ │└────────────────────────────────────────────────┘// Spring 中的 DIP 实践@Servicepublic class UserService { @Autowired private UserRepository repository; // Spring 注入具体实现 // UserService 完全不知道具体是 MySQL 还是 MongoDB}
// 切换数据库:只需修改配置或实现类@Repository@Primary // 优先注入public class MySQLUserRepository implements UserRepository { }
@Repository@ConditionalOnProperty(name = "db.type", havingValue = "mongo")public class MongoUserRepository implements UserRepository { }依赖注入的三种方式对比:
| 注入方式 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|
| 构造器注入 | 不可变、强制依赖、易测试 | 参数过多时构造器臃肿 | ⭐⭐⭐⭐⭐ |
| Setter 注入 | 可选依赖、灵活 | 可变、可能为 null | ⭐⭐⭐ |
| 字段注入 | 简洁 | 无法测试、依赖隐藏 | ⭐(不推荐) |
// ✅ 推荐方式:构造器注入(Spring 4.3+ 单构造器可省略 @Autowired)@Servicepublic class UserService { private final UserRepository repository; private final EmailService emailService;
public UserService(UserRepository repository, EmailService emailService) { this.repository = repository; this.emailService = emailService; }}链式追问六:迪米特法则(LoD)
Section titled “链式追问六:迪米特法则(LoD)”Q7:迪米特法则(最少知识原则)是什么?如何应用?中频
Section titled “Q7:迪米特法则(最少知识原则)是什么?如何应用?”定义:一个对象应该对其他对象有尽可能少的了解。只与”直接朋友”交流,不与”陌生人”说话。
直接朋友的定义:
- 当前对象本身
- 以参数形式传入的对象
- 当前对象的成员变量
- 当前对象创建的对象
违反 LoD 的案例:
// ❌ 违反 LoD:链式调用,暴露了内部结构public class OrderService { public void process(Order order) { // 访问了 Customer,又访问了 Wallet,又访问了 Money // Order 需要"知道" Customer 有 Wallet,Wallet 有 Money double money = order.getCustomer().getWallet().getMoney();
if (money >= order.getTotal()) { // 处理订单 } }}// 问题:如果 Customer 没有 Wallet 了,这段代码就要改符合 LoD 的设计:
// ✅ 符合 LoD:只与直接朋友交流public class OrderService { public void process(Order order) { // Order 提供方法,隐藏内部细节 if (order.canAfford()) { order.process(); } }}
public class Order { public boolean canAfford() { return customer.canPay(total); // 让 Customer 判断 }}
public class Customer { public boolean canPay(double amount) { return wallet.hasEnoughMoney(amount); // 让 Wallet 判断 }}
public class Wallet { public boolean hasEnoughMoney(double amount) { return money >= amount; }}迪米特法则的核心思想:
┌────────────────────────────────────────────────┐│ LoD 的核心:封装和隐藏 │├────────────────────────────────────────────────┤│ ││ ❌ 错误示范(暴露内部结构): ││ ││ A.getB().getC().doSomething() ││ └──┘ └──┘ └──┘ ││ 需要知道 B、C 的存在 ││ ││ ✅ 正确示范(隐藏内部结构): ││ ││ A.doSomething() ││ └──┘ ││ 只需要知道 A 的存在 ││ A 内部如何实现,A 自己知道 ││ │└────────────────────────────────────────────────┘实战应用:门面模式
// 门面模式是 LoD 的典型应用// 客户端只需要与门面交互,不需要了解子系统
public class OrderFacade { private InventoryService inventoryService; private PaymentService paymentService; private ShippingService shippingService;
// 门面方法:隐藏复杂的子系统交互 public void placeOrder(Order order) { // 1. 检查库存 inventoryService.checkStock(order);
// 2. 扣款 paymentService.charge(order);
// 3. 发货 shippingService.ship(order);
// 客户端不需要知道这三个服务的存在! }}
// 客户端代码OrderFacade facade = new OrderFacade();facade.placeOrder(order); // 只与门面交互,符合 LoD六大原则对比总结
Section titled “六大原则对比总结”| 原则 | 核心思想 | 解决的问题 | 典型应用 |
|---|---|---|---|
| 单一职责(SRP) | 一个类只有一个变化原因 | 类太庞大、职责混乱 | Spring 的分层架构 |
| 开闭原则(OCP) | 对扩展开放,对修改关闭 | 新增功能破坏已有代码 | Spring 的扩展点设计 |
| 里氏替换(LSP) | 子类可替换父类 | 继承关系不合理 | Java 集合框架 |
| 接口隔离(ISP) | 接口要小而专一 | 接口臃肿、强迫实现 | Spring 的函数式接口 |
| 依赖倒置(DIP) | 依赖抽象,不依赖具体 | 高层依赖低层、耦合严重 | Spring IoC 容器 |
| 迪米特(LoD) | 最少知识原则 | 类之间耦合过深 | 门面模式 |
六原则的统一目标:
┌────────────────────────────────────────────────┐│ ││ 提高系统的可维护性 ││ ▲ ││ │ ││ ┌─────────┴─────────┐ ││ │ │ ││ 降低耦合度 提高内聚度 ││ │ │ ││ ┌────┴────┐ ┌────┴────┐ ││ │ │ │ │ ││ DIP LoD SRP ISP ││ │ │ │ │ ││ └────┬────┘ └────┬────┘ ││ │ │ ││ └─────────┬─────────┘ ││ │ ││ OCP + LSP ││ (继承和多态的正确使用) ││ │└────────────────────────────────────────────────┘高频面试题总结
Section titled “高频面试题总结”常见追问:
-
Q:这么多原则,实际开发中怎么权衡? A:原则是指导思想,不是教条。要根据实际业务复杂度和变化频率灵活运用。
-
Q:SRP 和 ISP 有什么区别? A:SRP 关注类的职责,ISP 关注接口的方法。一个是类层面的内聚,一个是接口层面的隔离。
-
Q:Spring 框架体现了哪些设计原则? A:DIP(IoC)、OCP(扩展点)、ISP(函数式接口)、SRP(分层架构)。
-
Q:如何避免过度设计? A:遵循 YAGNI 原则(You Aren’t Gonna Need It),只在真正需要扩展时才应用设计原则。