SOLID原则详解:提升软件设计质量的关键

前言

关于设计原则SOLID具体指的是什么,怎么理解这些设计原则,我觉得有必要记录一笔,毕竟这个设计原则确实经常在关键技术文档中提及,在编程思想中提及,在日常的开发中使用,但是对我来说,似乎知道但又不那么明确,我希望自己对设计原则的思想有一个更加准确和全面的理解,也想明确如果没有这个设计原则会如何?此设计原则的亮点和优势是什么?我在日常开发中怎么使用到这些设计原则的?

本文就是基于以上问题的总结归纳,方便自己日后复盘。

说明:汇总风格和内容借助AI工具

一、什么是SOLID?

SOLID是面向对象编程和软件设计的五个基本原则的首字母缩写,这些原则帮助我们编写更易于维护、扩展和理解的代码。

  1. S - 单一职责原则 (Single Responsibility Principle)
  2. O - 开闭原则 (Open/Closed Principle)
  3. L - 里氏替换原则 (Liskov Substitution Principle)
  4. I - 接口隔离原则 (Interface Segregation Principle)
  5. D - 依赖倒置原则 (Dependency Inversion Principle)

1. 单一职责原则(SRP)

  • 核心:一个类应该只有一个引起它变化的原因(即只有一个职责)。
  • 关键点
    • 方法层面:一个方法只做一件事(如saveStudent()不应同时包含验证和存储逻辑)。
    • 类层面:Student类管理学生属性,若需日志记录,应拆分出StudentLogger类。
  • 优势:降低复杂度、提高可维护性,修改一个功能时不会意外影响其他功能。
  • 现实类比:就像餐厅里厨师负责烹饪,服务员负责上菜,收银员负责结账,各司其职,而不是一个人做所有事情。

日常开发中的问题:忽视SRP会导致"上帝类"(God Class),修改一处可能影响多处功能,测试困难,代码难以复用。

  • 反例:

    java 复制代码
    class Student {
        void saveToDatabase() { /* 数据库操作 */ }
        void generateReport() { /* 生成PDF */ } // 违反SRP
    }

2. 开闭原则(OCP)

  • 核心:通过扩展(继承/组合)添加新功能,而非修改已有代码。

  • 关键点

    • 多态是手段之一,但OCP更强调抽象(接口/抽象类)的设计。
    • 示例:支付系统支持新支付方式时,应实现Payment接口,而非修改原有代码。
    java 复制代码
    interface Payment { void pay(); }
    class CreditCard implements Payment { /* 无需修改现有类 */ }
  • 优势:减少回归测试风险,提高系统可扩展性。

  • 现实类比:USB接口设计 - 你可以插入各种设备(扩展开放),而不需要修改电脑的USB接口本身(修改关闭)。

日常开发中的问题:忽视OCP会导致每次需求变更都要修改核心类,增加回归测试负担,引入新bug的风险高。

  • 反例:
java 复制代码
class Shape {
    private String type;
    
    public double calculateArea() {
        if (type.equals("circle")) {
            // 计算圆形面积
        } else if (type.equals("rectangle")) {
            // 计算矩形面积
        }
        // 每添加一个新形状都要修改这个方法
    }
}

3. 里氏替换原则(LSP)

  • 核心:子类必须能够替换父类而不破坏程序逻辑(行为一致性)。
  • 关键点:
    • 子类可扩展父类功能,但不能改变父类的契约(如输入/输出约束)。
  • 优势:保证继承体系的健壮性,避免运行时意外错误。
  • 现实类比:正方形是长方形的特例,但如果长方形有设置不同长宽的方法,正方形继承长方形就会有问题,因为正方形长宽必须相同。

日常开发中的问题:忽视LSP会导致在使用多态时出现意外行为,子类无法真正替代父类,增加了代码的脆弱性。

  • 反例:
    父类Birdfly()方法,子类Penguin重写为空方法------违反LSP。
java 复制代码
class Bird {
    public void fly() {
        System.out.println("Flying");
    }
}

class Ostrich extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("鸵鸟不会飞!");
    }
}

public class Main {
    public static void makeBirdFly(Bird bird) {
        bird.fly();  // 对于鸵鸟,这会抛出异常
    }
}

4. 接口隔离原则(ISP)

  • 核心:客户端不应被迫依赖它不需要的接口方法。

  • 关键点

    • 将庞大接口拆分为更小、更具体的接口(如PrinterScanner分开,而非合并为MultiFunctionDevice)。

    • 示例:

      java 复制代码
      interface Printable { void print(); }
      interface Scannable { void scan(); }
      class SimplePrinter implements Printable { ... } // 无需实现scan()
  • 优势:减少接口污染,降低依赖耦合。

  • 现实类比:多功能工具 vs 专用工具 ,你不会用瑞士军刀上的剪刀功能来剪头发(虽然可以,但不合适)。

日常开发中的问题:忽视ISP会导致"胖接口",实现类被迫提供空实现或抛出异常,接口变得难以理解和维护。

  • 反例:
java 复制代码
interface Worker {
    void work();
    void eat();
    void sleep();
}

class HumanWorker implements Worker {
    // 实现所有方法
}

class RobotWorker implements Worker {
    public void work() {
        // 机器人可以工作
    }
    
    public void eat() {
        throw new UnsupportedOperationException("机器人不需要吃饭");
    }
    
    public void sleep() {
        throw new UnsupportedOperationException("机器人不需要睡觉");
    }
}

5. 依赖倒置原则(DIP)

  • 核心
    高层模块不应直接依赖低层模块,二者都应依赖抽象(接口或抽象类)。
    抽象不应依赖细节(具体实现),细节应依赖抽象。
  • 关键点
    "反转"传统的依赖关系方向,使得软件的设计更加灵活、可复用,并且更容易应对变化。
  • 现实类比:电源插座提供标准接口(抽象),各种电器(具体实现)只要符合接口标准就能使用,插座不需要知道具体是什么电器。

日常开发中的问题:忽视DIP会导致高层模块与低层模块紧耦合,难以替换实现,单元测试困难(因为难以mock依赖)。

  • 反例:
java 复制代码
class LightBulb {
    public void turnOn() {
        // 开灯
    }
    
    public void turnOff() {
        // 关灯
    }
}

class Switch {
    private LightBulb bulb;
    
    public Switch(LightBulb bulb) {
        this.bulb = bulb;
    }
    
    public void operate() {
        // 直接依赖具体实现
        bulb.turnOn();
    }
}

二、SpringBoot+MyBatis后台系统中的SOLID原则实践

1. 单一职责原则(SRP)在SpringBoot中的体现

反面案例(违反SRP)

java 复制代码
@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;
    
    // 用户CRUD
    public User getUserById(Long id) { /*...*/ }
    public void saveUser(User user) { /*...*/ }
    
    // 密码加密
    public String encryptPassword(String raw) { /*...*/ }
    
    // 权限检查
    public boolean checkPermission(User user) { /*...*/ }
    
    // 日志记录
    public void writeLog(User user, String action) { /*...*/ }
}

问题:这个Service类做了太多事情,违反了SRP。如果密码加密算法或日志记录方式需要修改,都要改这个类。

正面案例(遵循SRP)

java 复制代码
// 用户CRUD服务
@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private PermissionChecker permissionChecker;
    @Autowired
    private UserActionLogger actionLogger;
    
    public User getUserById(Long id) { /*...*/ }
    public void saveUser(User user) { 
        user.setPassword(passwordEncoder.encode(user.getPassword()));
        userMapper.insert(user);
        actionLogger.log(user, "CREATE");
    }
}

// 密码加密组件
@Component
public class BCryptPasswordEncoder implements PasswordEncoder {
    public String encode(String raw) { /* 使用BCrypt加密 */ }
}

// 权限检查组件
@Component
public class PermissionChecker {
    public boolean check(User user) { /*...*/ }
}

// 日志记录组件
@Component
public class UserActionLogger {
    public void log(User user, String action) { /*...*/ }
}

SpringBoot中的体现

  • Controller只处理HTTP请求和响应
  • Service只处理业务逻辑
  • Mapper只负责数据库操作
  • 各种Util/Helper类各司其职

2. 开闭原则(OCP)在MyBatis中的体现

场景:我们需要支持多种数据库查询方式(ID查询、姓名查询、条件组合查询)

反面案例(违反OCP)

java 复制代码
@Mapper
public interface UserMapper {
    @Select("SELECT * FROM user WHERE ${condition}") 
    List<User> findByCondition(String condition); // 危险!SQL注入风险
    
    // 每新增一种查询方式都要添加新方法
}

正面案例(遵循OCP)

使用MyBatis-Plus,它的Wrapper设计就符合OCP:

java 复制代码
@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;
    
    // 使用条件构造器,不需要修改原有代码就能扩展新查询方式
    public List<User> findUsers(String name, Integer age) {
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        if (name != null) {
            wrapper.like("name", name);
        }
        if (age != null) {
            wrapper.eq("age", age);
        }
        return userMapper.selectList(wrapper);
    }
}

MP的设计

  • 通过Wrapper可以灵活组合查询条件
  • 新增查询条件不需要修改Mapper接口
  • 符合"对扩展开放,对修改关闭"

3. 里氏替换原则(LSP)在权限系统中的应用

场景:我们有普通用户和管理员用户

反面案例(违反LSP)

java 复制代码
class User {
    public void deletePost(Post post) {
        // 基础权限检查
    }
}

class Admin extends User {
    @Override
    public void deletePost(Post post) {
        throw new UnsupportedOperationException("管理员应该用adminDeletePost方法");
    }
    
    public void adminDeletePost(Post post) {
        // 跳过权限检查
    }
}

问题:Admin无法替换User,因为重写的方法抛出了异常。

正面案例(遵循LSP)

java 复制代码
interface PostDeleter {
    void deletePost(Post post);
}

class UserPostDeleter implements PostDeleter {
    public void deletePost(Post post) {
        // 基础权限检查
    }
}

class AdminPostDeleter implements PostDeleter {
    public void deletePost(Post post) {
        // 管理员有特殊处理,但不抛出异常
    }
}

// 使用时
@Autowired
private Map<String, PostDeleter> deleterMap; // Spring会自动注入所有实现

public void deletePost(Post post, String userType) {
    PostDeleter deleter = deleterMap.get(userType + "PostDeleter");
    deleter.deletePost(post); // 无论什么用户类型都能安全调用
}

4. 接口隔离原则(ISP)在Service层设计中的应用

场景:用户操作有读操作和写操作,有些客户端只需要读功能

反面案例(违反ISP)

java 复制代码
public interface UserService {
    User getById(Long id);
    List<User> findAll();
    void save(User user);
    void delete(Long id);
    void resetPassword(Long id);
    // 很多方法...
}

// 报表系统只需要读功能,但被迫实现所有方法

正面案例(遵循ISP)

java 复制代码
// 拆分接口
public interface UserReadService {
    User getById(Long id);
    List<User> findAll();
}

public interface UserWriteService {
    void save(User user);
    void delete(Long id);
    void resetPassword(Long id);
}

@Service
public class UserServiceImpl implements UserReadService, UserWriteService {
    // 实现所有方法
}

// 报表系统只需要注入UserReadService
@Autowired
private UserReadService userReadService;

5. 依赖倒置原则(DIP)在SpringBoot中的体现

场景:用户数据存储可能使用MySQL或Redis

反面案例(违反DIP)

java 复制代码
@Service
public class UserService {
    // 直接依赖具体实现
    private UserMySQLRepository userRepository = new UserMySQLRepository();
    
    // 如果改用Redis需要修改代码
}

正面案例(遵循DIP)

java 复制代码
// 定义抽象接口
public interface UserRepository {
    User findById(Long id);
    void save(User user);
}

// MySQL实现
@Repository
public class UserMySQLRepository implements UserRepository {
    // 实现方法
}

// Redis实现
@Repository
public class UserRedisRepository implements UserRepository {
    // 实现方法
}

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository; // 依赖抽象
    
    // 可以通过@Qualifier或Profile决定注入哪个实现
}

SpringBoot天生支持DIP

  • 通过@Autowired注入接口
  • 具体实现由Spring容器管理
  • 轻松替换实现而不修改业务代码

三、实际应用建议

(1)实际应用

  • Spring框架:依赖注入(DI)是DIP的典型实现。
  • Java集合框架List接口(抽象)与ArrayList/LinkedList(实现)遵循DIP和OCP。
  • 日志库:SLF4J是抽象,Logback/Log4j是具体实现,符合DIP。

(2)实际编程中的选择

  • 写业务代码时 :优先用 SRPDIP(拆分职责+依赖接口)。
  • 设计架构时 :重点考虑 OCPISP(方便扩展+接口精简)。
  • review代码时 :检查 LSP(子类是否破坏父类行为)。

后记

SOLID不是教条,而是帮助写出更健壮代码的工具。在SpringBoot项目中,很多设计已经遵循了这些原则,我们只需要有意识地应用它们。

参考链接

SOLID,面向对象设计五大基本原则

相关推荐
小鸡脚来咯3 分钟前
spring IOC控制反转
java·后端·spring
怡人蝶梦2 小时前
Java后端技术栈问题排查实战:Spring Boot启动慢、Redis缓存击穿与Kafka消费堆积
java·jvm·redis·kafka·springboot·prometheus
瓯雅爱分享2 小时前
MES管理系统:Java+Vue,含源码与文档,实现生产过程实时监控、调度与优化,提升制造企业效能
java·mysql·vue·软件工程·源代码管理
蓝色天空的银码星2 小时前
Springcloud Alibaba自定义负载均衡详解
spring·spring cloud·负载均衡
鬼多不菜3 小时前
一篇学习CSS的笔记
java·前端·css
深色風信子3 小时前
Eclipse 插件开发 5.3 编辑器 监听输入
java·eclipse·编辑器·编辑器 监听输入·插件 监听输入
Blossom.1183 小时前
人工智能在智能健康监测中的创新应用与未来趋势
java·人工智能·深度学习·机器学习·语音识别
shangjg33 小时前
Kafka 如何保证不重复消费
java·分布式·后端·kafka
无处不在的海贼3 小时前
小明的Java面试奇遇之互联网保险系统架构与性能优化
java·面试·架构