前言
关于设计原则SOLID具体指的是什么,怎么理解这些设计原则,我觉得有必要记录一笔,毕竟这个设计原则确实经常在关键技术文档中提及,在编程思想中提及,在日常的开发中使用,但是对我来说,似乎知道但又不那么明确,我希望自己对设计原则的思想有一个更加准确和全面的理解,也想明确如果没有这个设计原则会如何?此设计原则的亮点和优势是什么?我在日常开发中怎么使用到这些设计原则的?
本文就是基于以上问题的总结归纳,方便自己日后复盘。
说明:汇总风格和内容借助AI工具
一、什么是SOLID?
SOLID是面向对象编程和软件设计的五个基本原则的首字母缩写,这些原则帮助我们编写更易于维护、扩展和理解的代码。
- S - 单一职责原则 (Single Responsibility Principle)
- O - 开闭原则 (Open/Closed Principle)
- L - 里氏替换原则 (Liskov Substitution Principle)
- I - 接口隔离原则 (Interface Segregation Principle)
- D - 依赖倒置原则 (Dependency Inversion Principle)
1. 单一职责原则(SRP)
- 核心:一个类应该只有一个引起它变化的原因(即只有一个职责)。
- 关键点:
- 方法层面:一个方法只做一件事(如
saveStudent()
不应同时包含验证和存储逻辑)。 - 类层面:
Student
类管理学生属性,若需日志记录,应拆分出StudentLogger
类。
- 方法层面:一个方法只做一件事(如
- 优势:降低复杂度、提高可维护性,修改一个功能时不会意外影响其他功能。
- 现实类比:就像餐厅里厨师负责烹饪,服务员负责上菜,收银员负责结账,各司其职,而不是一个人做所有事情。
日常开发中的问题:忽视SRP会导致"上帝类"(God Class),修改一处可能影响多处功能,测试困难,代码难以复用。
- 反例:
class Student {void saveToDatabase() { /* 数据库操作 */ }void generateReport() { /* 生成PDF */ } // 违反SRP }
2. 开闭原则(OCP)
- 核心:通过扩展(继承/组合)添加新功能,而非修改已有代码。
- 关键点:
- 多态是手段之一,但OCP更强调抽象(接口/抽象类)的设计。
- 示例:支付系统支持新支付方式时,应实现
Payment
接口,而非修改原有代码。
interface Payment { void pay(); } class CreditCard implements Payment { /* 无需修改现有类 */ }
- 优势:减少回归测试风险,提高系统可扩展性。
- 现实类比:USB接口设计 - 你可以插入各种设备(扩展开放),而不需要修改电脑的USB接口本身(修改关闭)。
日常开发中的问题:忽视OCP会导致每次需求变更都要修改核心类,增加回归测试负担,引入新bug的风险高。
- 反例:
class Shape {private String type;public double calculateArea() {if (type.equals("circle")) {// 计算圆形面积} else if (type.equals("rectangle")) {// 计算矩形面积}// 每添加一个新形状都要修改这个方法}
}
3. 里氏替换原则(LSP)
- 核心:子类必须能够替换父类而不破坏程序逻辑(行为一致性)。
- 关键点:
- 子类可扩展父类功能,但不能改变父类的契约(如输入/输出约束)。
- 优势:保证继承体系的健壮性,避免运行时意外错误。
- 现实类比:正方形是长方形的特例,但如果长方形有设置不同长宽的方法,正方形继承长方形就会有问题,因为正方形长宽必须相同。
日常开发中的问题:忽视LSP会导致在使用多态时出现意外行为,子类无法真正替代父类,增加了代码的脆弱性。
- 反例:
父类Bird
有fly()
方法,子类Penguin
重写为空方法——违反LSP。
class Bird {public void fly() {System.out.println("Flying");}
}class Ostrich extends Bird {@Overridepublic void fly() {throw new UnsupportedOperationException("鸵鸟不会飞!");}
}public class Main {public static void makeBirdFly(Bird bird) {bird.fly(); // 对于鸵鸟,这会抛出异常}
}
4. 接口隔离原则(ISP)
- 核心:客户端不应被迫依赖它不需要的接口方法。
- 关键点:
- 将庞大接口拆分为更小、更具体的接口(如
Printer
和Scanner
分开,而非合并为MultiFunctionDevice
)。 - 示例:
interface Printable { void print(); } interface Scannable { void scan(); } class SimplePrinter implements Printable { ... } // 无需实现scan()
- 将庞大接口拆分为更小、更具体的接口(如
- 优势:减少接口污染,降低依赖耦合。
- 现实类比:多功能工具 vs 专用工具 ,你不会用瑞士军刀上的剪刀功能来剪头发(虽然可以,但不合适)。
日常开发中的问题:忽视ISP会导致"胖接口",实现类被迫提供空实现或抛出异常,接口变得难以理解和维护。
- 反例:
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依赖)。
- 反例:
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):
@Service
public class UserService {@Autowiredprivate UserMapper userMapper;// 用户CRUDpublic 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):
// 用户CRUD服务
@Service
public class UserService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate PasswordEncoder passwordEncoder;@Autowiredprivate PermissionChecker permissionChecker;@Autowiredprivate 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):
@Mapper
public interface UserMapper {@Select("SELECT * FROM user WHERE ${condition}") List<User> findByCondition(String condition); // 危险!SQL注入风险// 每新增一种查询方式都要添加新方法
}
正面案例(遵循OCP):
使用MyBatis-Plus,它的Wrapper设计就符合OCP:
@Service
public class UserService {@Autowiredprivate 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):
class User {public void deletePost(Post post) {// 基础权限检查}
}class Admin extends User {@Overridepublic void deletePost(Post post) {throw new UnsupportedOperationException("管理员应该用adminDeletePost方法");}public void adminDeletePost(Post post) {// 跳过权限检查}
}
问题:Admin无法替换User,因为重写的方法抛出了异常。
正面案例(遵循LSP):
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):
public interface UserService {User getById(Long id);List<User> findAll();void save(User user);void delete(Long id);void resetPassword(Long id);// 很多方法...
}// 报表系统只需要读功能,但被迫实现所有方法
正面案例(遵循ISP):
// 拆分接口
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):
@Service
public class UserService {// 直接依赖具体实现private UserMySQLRepository userRepository = new UserMySQLRepository();// 如果改用Redis需要修改代码
}
正面案例(遵循DIP):
// 定义抽象接口
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 {@Autowiredprivate UserRepository userRepository; // 依赖抽象// 可以通过@Qualifier或Profile决定注入哪个实现
}
SpringBoot天生支持DIP:
- 通过@Autowired注入接口
- 具体实现由Spring容器管理
- 轻松替换实现而不修改业务代码
三、实际应用建议
(1)实际应用
- Spring框架:依赖注入(DI)是DIP的典型实现。
- Java集合框架:
List
接口(抽象)与ArrayList
/LinkedList
(实现)遵循DIP和OCP。 - 日志库:SLF4J是抽象,Logback/Log4j是具体实现,符合DIP。
(2)实际编程中的选择
- 写业务代码时:优先用 SRP 和 DIP(拆分职责+依赖接口)。
- 设计架构时:重点考虑 OCP 和 ISP(方便扩展+接口精简)。
- review代码时:检查 LSP(子类是否破坏父类行为)。
后记
SOLID不是教条,而是帮助写出更健壮代码的工具。在SpringBoot项目中,很多设计已经遵循了这些原则,我们只需要有意识地应用它们。
参考链接
SOLID,面向对象设计五大基本原则