SOLID 原则详解:面向对象设计的六大原则

🎯 面试题:SOLID 原则分别指的是什么?

SOLID 是面向对象设计的五个基本原则,由 Robert C. Martin(Uncle Bob)提出。它们是写出高可维护性代码的理论基础,面试中常用来考察候选人的设计能力。


一、单一职责原则(SRP — Single Responsibility Principle)

核心定义

一个类应该只有一个引起它变化的原因。

反面案例

// ❌ 违反 SRP:一个类承担了多个职责
public class User {
    public void saveUser() { /* 持久化 */ }
    public void sendEmail() { /* 发送邮件 */ }
    public void validateUser() { /* 数据验证 */ }
    public void generateReport() { /* 生成报表 */ }
}
// 这个类会因为任何职责的变化而被修改

正面案例

// ✅ 符合 SRP:职责分离
public class UserValidator {
    public boolean validate(User user) { /* 数据验证 */ }
}

public class UserRepository {
    public void save(User user) { /* 持久化 */ }
}

public class EmailService {
    public void sendWelcomeEmail(User user) { /* 发邮件 */ }
}

public class ReportGenerator {
    public Report generateUserReport(User user) { /* 生成报表 */ }
}

Spring 中的体现

// 每个类只做一件事
@Component
public class PaymentService { /* 只负责支付逻辑 */ }

@Component
public class NotificationService { /* 只负责通知 */ }

@Component
public class AuditLogService { /* 只负责审计日志 */ }

判断标准

变化原因检查:
  问:改变这个类的理由是什么?
  如果有多个不同的理由 → 违反 SRP
  如果只有一个理由 → 符合 SRP

二、开闭原则(OCP — Open-Closed Principle)

核心定义

软件实体应该对扩展开放,对修改关闭。

反面案例

// ❌ 违反 OCP:新增支付方式要修改原有代码
public class PaymentService {
    public void pay(String type) {
        if ("alipay".equals(type)) {
            // 支付宝支付
        } else if ("wechat".equals(type)) {
            // 微信支付
        } else if ("card".equals(type)) {
            // 银行卡支付
        }
        // 每加一个支付方式都要改这里
    }
}

正面案例:策略模式

// ✅ 符合 OCP:新增支付方式只需新增类,不改原有代码
public interface PaymentStrategy {
    void pay(Order order);
}

@Component
public class AlipayStrategy implements PaymentStrategy {
    @Override
    public void pay(Order order) { /* 支付宝支付逻辑 */ }
}

@Component
public class WechatPayStrategy implements PaymentStrategy {
    @Override
    public void pay(Order order) { /* 微信支付逻辑 */ }
}

@Service
public class PaymentService {
    @Autowired
    private Map<String, PaymentStrategy> strategies; // Spring 自动注入所有实现

    public void pay(String type, Order order) {
        PaymentStrategy strategy = strategies.get(type);
        if (strategy == null) throw new BizException("不支持的支付方式");
        strategy.pay(order);
    }
    // 新增支付方式只需新增一个实现类,PaymentService 不用改
}

Spring 中的体现

// Spring 的扩展点机制就是 OCP 的经典应用
// 定义扩展点(对修改关闭)
@FunctionalInterface
public interface BeanFactoryPostProcessor {
    void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException;
}

// 用户实现扩展点(对扩展开放)
@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
        // 在 Bean 创建前修改 Bean 定义
    }
}

三、里氏替换原则(LSP — Liskov Substitution Principle)

核心定义

子类对象可以替换父类对象,而程序行为不变。

反面案例

// ❌ 违反 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; }
}

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 void resize(Rectangle rect, int width, int height) {
    rect.setWidth(width);
    rect.setHeight(height);
    // 期望宽高分别设成了 width 和 height
    // 但如果是 Square,结果是 width * width
}

// 调用
Rectangle rect = new Square();
resize(rect, 5, 3); // 期望面积 15,实际面积 25 ❌

正面案例

// ✅ 符合 LSP:正方形不应该继承矩形
public interface Shape {
    int getArea();
}

public class Rectangle implements Shape {
    private int width;
    private int height;

    @Override
    public int getArea() { return width * height; }
}

public class Square implements Shape {
    private int side;

    @Override
    public int getArea() { return side * side; }
}

Spring 中的体现

// List 是 ArrayList 和 LinkedList 的父类
// 它们可以互换使用而不影响程序行为(符合 LSP)
List<String> list = new ArrayList<>();  // ✅
list = new LinkedList<>();              // ✅ 可以替换

四、接口隔离原则(ISP — Interface Segregation Principle)

核心定义

客户端不应该依赖它不需要的接口。

反面案例

// ❌ 违反 ISP:一个臃肿接口
public interface Animal {
    void fly();      // 鸟需要,鱼不需要
    void swim();     // 鱼需要,鸟不需要
    void run();      // 猫需要,鱼不需要
}

public class Dog implements Animal {
    @Override
    public void fly() { /* 狗不会飞,但必须实现 */ }
    @Override
    public void swim() { /* 狗会游泳 */ }
    @Override
    public void run() { /* 狗会跑 */ }
}

正面案例

// ✅ 符合 ISP:拆分为多个小接口
public interface Flyable {
    void fly();
}

public interface Swimmable {
    void swim();
}

public interface Runnable {
    void run();
}

public class Dog implements Swimmable, Runnable {
    @Override
    public void swim() { /* 狗游泳 */ }
    @Override
    public void run() { /* 狗跑 */ }
    // 不需要实现 fly()
}

public class Bird implements Flyable, Runnable {
    @Override
    public void fly() { /* 鸟飞 */ }
    @Override
    public void run() { /* 鸟跑 */ }
    // 不需要实现 swim()
}

Spring 中的体现

// Spring 的接口都尽量小而专
// JdbcTemplate 实现了 JdbcOperations
// RestTemplate 实现了 RestOperations
// 用户按需注入

public interface InitializingBean {
    void afterPropertiesSet() throws Exception;
}

public interface DisposableBean {
    void destroy() throws Exception;
}
// 类按需实现,不需要实现所有接口

五、依赖倒置原则(DIP — Dependency Inversion Principle)

核心定义

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

反面案例

// ❌ 违反 DIP:高层依赖低层
@Service
public class OrderService {
    private MySQLOrderRepository repository = new MySQLOrderRepository(); // 直接依赖实现

    public void createOrder(Order order) {
        repository.save(order); // 耦合具体实现
    }
}

public class MySQLOrderRepository {
    public void save(Order order) { /* MySQL 实现 */ }
}

正面案例

// ✅ 符合 DIP:依赖抽象
// 1. 定义抽象
public interface OrderRepository {
    void save(Order order);
    Order findById(Long id);
}

// 2. 低层实现
@Repository
public class MySQLOrderRepository implements OrderRepository {
    @Override
    public void save(Order order) { /* MySQL 实现 */ }
    @Override
    public Order findById(Long id) { /* MySQL 实现 */ }
}

@Repository
public class RedisOrderRepository implements OrderRepository {
    @Override
    public void save(Order order) { /* Redis 实现 */ }
    @Override
    public Order findById(Long id) { /* Redis 实现 */ }
}

// 3. 高层依赖抽象
@Service
public class OrderService {
    private final OrderRepository repository; // 依赖抽象,不依赖实现

    @Autowired
    public OrderService(OrderRepository repository) {
        this.repository = repository; // 由 Spring 注入具体实现
    }

    public void createOrder(Order order) {
        repository.save(order); // 不知道具体是 MySQL 还是 Redis
    }
}

Spring 中的体现

// Spring 的依赖注入就是 DIP 的最佳实践
@Service
public class UserService {
    // 依赖接口,不依赖实现类
    private final UserRepository userRepository;

    // 由 Spring 注入具体实现(MySQL/Redis/MongoDB...)
    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

六、合成复用原则(CRP — Composite Reuse Principle)

核心定义

尽量使用组合/聚合,而不是继承来实现复用。

继承 vs 组合

// ❌ 继承复用:类耦合高
public class A { void method() { /* ... */ } }

public class B extends A {  // B 和 A 强耦合
    // B 继承了 A 的所有方法
}

// 问题:A 改变 → B 可能受影响
// 问题:单继承限制
// ✅ 组合复用:松耦合
public class A { void method() { /* ... */ } }

public class B {
    private A a;  // 组合:has-a 而非 is-a

    public B(A a) { this.a = a; }

    public void doSomething() {
        a.method(); // 委托给 A
    }
}

// 优点:可以运行时替换 A 的实现
// 优点:符合 OCP

Spring 中的体现

// Spring 大量使用组合而非继承
// 为什么 Spring Bean 不是继承某个基类?
// 因为 Spring 用组合 + 依赖注入实现所有功能

@Component
public class OrderService {
    @Autowired
    private PaymentService paymentService;  // 组合

    @Autowired
    private NotificationService notificationService;  // 组合

    @Autowired
    private InventoryService inventoryService;  // 组合
    // 所有功能通过组合实现,不继承任何基类
}

七、迪米特法则(LoD — Law of Demeter)

核心定义

只与直接的朋友通信,不要和陌生人说话。

反面案例

// ❌ 违反 LoD:调用链过长
public class Customer {
    private Wallet wallet;
    public Wallet getWallet() { return wallet; }
}

public class Wallet {
    private double balance;
    public double getBalance() { return balance; }
}

public class OrderService {
    public void printCustomerBalance(Customer customer) {
        // 链条太长:客户 → 钱包 → 余额
        double balance = customer.getWallet().getBalance(); // ❌ 直接访问了陌生类
    }
}

正面案例

// ✅ 符合 LoD:通过直接朋友访问
public class Customer {
    private Wallet wallet;

    public double getBalance() {
        return wallet.getBalance(); // 自己内部处理,不暴露内部结构
    }
}

public class OrderService {
    public void printCustomerBalance(Customer customer) {
        double balance = customer.getBalance(); // ✅ 只和 Customer 通信
    }
}

Spring 中的体现

// Spring MVC 中 Controller 只和 Service 层交互
// 不直接操作 Repository/DAO
@RestController
public class UserController {
    @Autowired
    private UserService userService; // 直接朋友

    // ✅ 只调用 Service,不直接操作 Repository
    public UserVO getUser(Long id) {
        return userService.getUserById(id); // ✅ 只和直接朋友通信
    }
}

八、六大原则对比总结

原则 缩写 核心 关键词
单一职责 SRP 一个类只做一件事 职责分离
开闭原则 OCP 对扩展开放,对修改关闭 扩展优于修改
里氏替换 LSP 子类可替换父类 is-a 关系正确
接口隔离 ISP 接口要小而专 拒绝臃肿接口
依赖倒置 DIP 依赖抽象而非实现 面向接口编程
合成复用 CRP 组合优于继承 has-a 优于 is-a
迪米特法则 LoD 只和直接朋友通信 不要和陌生人说话

六大原则在 Spring 源码中的体现

Spring 源码               │ 体现的原则
─────────────────────────┼───────────────────
BeanFactoryPostProcessor  │ OCP(扩展点机制)
@Inject/@Autowired        │ DIP(依赖抽象)
JdbcTemplate/RabbitTemplate│ ISP(专用接口)
BeanDefinitionBuilder     │ CRP(组合优于继承)
JdbcTemplate 只调 Connection│ LoD(直接朋友)
@Scope("prototype") 新实例│ LSP(子类可替换父类)

九、高频面试题

Q1: SOLID 原则分别指什么?

S(Single)单一职责:一个类只负责一件事。O(Open-Closed)开闭原则:对扩展开放,对修改关闭,用策略模式、模板方法实现。L(Liskov)里氏替换:子类可以替换父类,不改变程序正确性。I(Interface Segregation)接口隔离:接口要小而专,不强迫实现不需要的方法。D(Dependency Inversion)依赖倒置:依赖抽象不依赖实现,高层模块和底层模块都依赖抽象。

Q2: 开闭原则如何落地?

核心手段是「面向接口编程 + 策略模式」。定义抽象接口(对扩展开放),具体实现类实现接口(新增功能只需新增类),业务代码依赖接口而非具体实现(对修改关闭)。典型场景:支付方式、消息发送、日志记录等。Spring 的 BeanFactoryPostProcessor、Servlet 的 Filter 都是开闭原则的经典应用。

Q3: 依赖倒置和依赖注入有什么区别?

DIP 是设计原则(高层依赖抽象),DI 是实现手段(Spring 通过反射注入实现)。DIP 告诉我们应该怎么做,DI 告诉我们怎么落地。依赖注入是 DIP 的一种具体实现方式。

Q4: 什么情况下应该用继承而不是组合?

当子类「是」父类的一种(is-a 关系),且子类完全继承父类的所有行为时用继承。当子类「有」某个功能(has-a 关系)时用组合。在 Java 中,由于没有多重继承,且继承耦合度高,组合通常优于继承。

Q5: Spring 为什么大量使用组合而不是继承?

组合比继承更灵活:① 组合可以运行时替换组件,继承是编译时静态绑定;② 组合没有单继承限制;③ 组合降低了类之间的耦合度;④ Spring 用 Bean 容器管理组件,通过 DI 实现组合,避免了继承带来的强耦合。