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,面向对象设计五大基本原则

相关推荐
鬼布5 分钟前
post请求在gateway打印日志内存遗漏
java·gateway
编程就是如此7 分钟前
手撕Tomcat
java·tomcat
未定义.22119 分钟前
UML-饮料自助销售系统(无法找零)序列图
设计模式·流程图·状态模式·软件工程·需求分析·uml
奔驰的小野码22 分钟前
本地实现Rtsp视频流推送
java·linux·后端·ffmpeg
PHASELESS41125 分钟前
Java二叉树深度解析:结构、算法与应用实践指南
java·开发语言·数据结构·算法
谦行1 小时前
前端视角 Java Web 入门手册 5.4:真实世界 Web 开发——Java Web 代码组织与分层
java·后端·架构
User_芊芊君子1 小时前
【Java】面向对象程序三板斧——如何优雅设计包、封装数据与优化代码块?
java·开发语言
Java_SuSheng1 小时前
关于SQLite轻量数据库的研究
java·数据库·spring boot·sqlite·mybatis
qq_447663052 小时前
Spring-注解编程
java·后端·spring
士兵木木2 小时前
类头文件相互包含的问题
java·开发语言