前言
关于设计原则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),修改一处可能影响多处功能,测试困难,代码难以复用。
-
反例:
javaclass Student { void saveToDatabase() { /* 数据库操作 */ } void generateReport() { /* 生成PDF */ } // 违反SRP }
2. 开闭原则(OCP)
-
核心:通过扩展(继承/组合)添加新功能,而非修改已有代码。
-
关键点 :
- 多态是手段之一,但OCP更强调抽象(接口/抽象类)的设计。
- 示例:支付系统支持新支付方式时,应实现
Payment
接口,而非修改原有代码。
javainterface 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会导致在使用多态时出现意外行为,子类无法真正替代父类,增加了代码的脆弱性。
- 反例:
父类Bird
有fly()
方法,子类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)
-
核心:客户端不应被迫依赖它不需要的接口方法。
-
关键点 :
-
将庞大接口拆分为更小、更具体的接口(如
Printer
和Scanner
分开,而非合并为MultiFunctionDevice
)。 -
示例:
javainterface 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)实际编程中的选择
- 写业务代码时 :优先用 SRP 和 DIP(拆分职责+依赖接口)。
- 设计架构时 :重点考虑 OCP 和 ISP(方便扩展+接口精简)。
- review代码时 :检查 LSP(子类是否破坏父类行为)。
后记
SOLID不是教条,而是帮助写出更健壮代码的工具。在SpringBoot项目中,很多设计已经遵循了这些原则,我们只需要有意识地应用它们。