Skip to content

六大设计原则深度解析

面试官:你了解 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));
}
}
// ✅ 邮件监听器:只负责发送邮件
@Component
public class EmailListener {
@EventListener
public void onUserRegistered(UserRegisteredEvent event) {
emailService.sendWelcomeEmail(event.getUser());
}
}
// ✅ 短信监听器:只负责发送短信
@Component
public 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 不是追求类的数量最少,而是让每个类只有一个变化的理由。


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 的事件监听器接口:高度隔离
@FunctionalInterface
public 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?”

定义

  1. 高层模块不应依赖低层模块,两者都应依赖抽象
  2. 抽象不应依赖细节,细节应依赖抽象

违反 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 实践
@Service
public 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)
@Service
public 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

原则核心思想解决的问题典型应用
单一职责(SRP)一个类只有一个变化原因类太庞大、职责混乱Spring 的分层架构
开闭原则(OCP)对扩展开放,对修改关闭新增功能破坏已有代码Spring 的扩展点设计
里氏替换(LSP)子类可替换父类继承关系不合理Java 集合框架
接口隔离(ISP)接口要小而专一接口臃肿、强迫实现Spring 的函数式接口
依赖倒置(DIP)依赖抽象,不依赖具体高层依赖低层、耦合严重Spring IoC 容器
迪米特(LoD)最少知识原则类之间耦合过深门面模式

六原则的统一目标

┌────────────────────────────────────────────────┐
│ │
│ 提高系统的可维护性 │
│ ▲ │
│ │ │
│ ┌─────────┴─────────┐ │
│ │ │ │
│ 降低耦合度 提高内聚度 │
│ │ │ │
│ ┌────┴────┐ ┌────┴────┐ │
│ │ │ │ │ │
│ DIP LoD SRP ISP │
│ │ │ │ │ │
│ └────┬────┘ └────┬────┘ │
│ │ │ │
│ └─────────┬─────────┘ │
│ │ │
│ OCP + LSP │
│ (继承和多态的正确使用) │
│ │
└────────────────────────────────────────────────┘

常见追问

  1. Q:这么多原则,实际开发中怎么权衡? A:原则是指导思想,不是教条。要根据实际业务复杂度和变化频率灵活运用。

  2. Q:SRP 和 ISP 有什么区别? A:SRP 关注类的职责,ISP 关注接口的方法。一个是类层面的内聚,一个是接口层面的隔离。

  3. Q:Spring 框架体现了哪些设计原则? A:DIP(IoC)、OCP(扩展点)、ISP(函数式接口)、SRP(分层架构)。

  4. Q:如何避免过度设计? A:遵循 YAGNI 原则(You Aren’t Gonna Need It),只在真正需要扩展时才应用设计原则。