SOLID 面向对象设计原则
为什么需要 SOLID 原则?
代码虽然由机器来执行,但是代码的读者始终是开发人员。写出可以运行的代码并不难,但是一个软件服务通常由大量的人力,经过长时间开发才能够完成,一段代码可能由不同的开发人员接手。因此,软件行业的开发者都需要面对一个严重的问题:
经过长时间开发后,代码如何保持良好的可读性和可维护性?
为了解决这个问题,软件开发人员们总结出了一系列的开发设计原则,其中有代表性的就是 SOLID 原则与设计模式。
SOLID 简介
在程序设计领域, SOLID(单一功能、开闭原则、里氏替换、接口隔离以及依赖反转)是由罗伯特·C·马丁(代表作《Clean Code》)在 21 世纪早期引入的记忆术首字母缩略字,指代了面向对象编程和面向对象设计的五个基本原则。
SOLID 分别指代:
- Single Responsibility Principle(单一功能原则)
- Open/Closed Principle(开闭原则)
- Liskov Substitution Principle(里氏替换原则)
- Interface Segregation Principle(接口隔离原则)
- Dependency Inversion Principle(依赖反转原则)
SOLID 原则旨在帮助软件开发人员设计出更易于维护、扩展和重用的代码。通过设计、重构等方式,清除代码异味。SOLID 被典型的应用在测试驱动开发上,并且是敏捷开发以及自适应软件开发的基本原则的重要组成部分。
SOLID 原则
1. 单一职责原则(Single Responsibility Principle, SRP)
每个类应只有一个职责或原因导致其变更。这样可以使类更易于理解、维护和扩展,避免因为一个职责的修改而影响到其他职责的实现。
设想一个用户注册的场景,我们通常会设计一个 User
类来维护用户的信息。如果需要对 User
进行校验,我们应该将校验逻辑放在一个独立的类 UserValidator
中,而不是将校验逻辑直接放在 User
类中。
User
类中应该只包含用户的基本信息:
public class User {
private String username;
private String password;
private String email;
// getter and setter
}
`UserValidator` 类中存放用户校验的逻辑:
```java
public class UserValidator {
public boolean validate(User user) {
// validate user
}
}
2. 开闭原则(Open/Closed Principle, OCP)
软件实体(类、模块、函数等)应对扩展开放,对修改关闭。这意味着在不修改现有代码的情况下,可以通过添加新功能来扩展系统,从而降低引入错误的风险。
假设我们又一些类用来描述几何形状,比如 TriAngle
、Rectangle
等,它们有共同的父类 Shape
。如果我们需要增加一个新的形状 Circle
,我们应该通过继承或接口的方式来实现,而不是直接修改现有的类。
public abstract class Shape {
public abstract void draw();
}
public class Triangle extends Shape {
@Override
public void draw() {
System.out.println("Triangle is drawing");
}
}
// 增加一个新的形状 Circle
public class Circle extends Shape {
@Override
public void draw() {
System.out.println("Circle is drawing");
}
}
3. 里氏替换原则(Liskov Substitution Principle, LSP)
子类必须能够替换其基类而不影响系统的正确性。这确保了继承关系的正确性,子类能完美适应父类的使用场景。
继续上一个几何形状的示例。如果我们有一个函数接收 Shape
类型的参数,那么我们可以传入任何继承自 Shape
的类,而不用担心会影响函数的正确性。
public void draw(Shape shape) {
shape.draw();
}
下面的代码同样都是支持的:
draw(new Triangle());
draw(new Rectangle());
draw(new Circle());
4. 接口隔离原则(Interface Segregation Principle, ISP)
客户端不应该被迫依赖它们不使用的方法。应将庞大的接口拆分成多个更小、更具体的接口,从而使实现类只需关注与其相关的接口。
对于功能的添加,我们应该通过增加接口的方式来实现。
现在让我们来描述一下神奇的动物世界。所有的动物都继承自 Animal
类,但是不同的动物有不同的行为,比如有些动物会游泳,有些动物会飞。我们可以通过接口的方式来实现:
public interface Flyable {
void fly();
}
public interface Swimmable {
void swim();
}
一只鸭子即会飞又会游泳,我们可以这样实现:
public class Duck extends Animal implements Flyable, Swimmable {
@Override
public void fly() {
System.out.println("Duck is flying");
}
@Override
public void swim() {
System.out.println("Duck is swimming");
}
}
但是鱼只会游泳,我们可以这样实现:
public class Fish extends Animal implements Swimmable {
@Override
public void swim() {
System.out.println("Fish is swimming");
}
}
通过不同的接口隔离了不同的功能。需要实现不同的功能时,只需要实现对应的接口。增加新的功能时,也只需要增加新的接口即可。
5. 依赖倒置原则(Dependency Inversion Principle, DIP)
高层模块不应依赖于低层模块,二者都应该依赖于抽象。抽象不应依赖于细节,细节应依赖于抽象。这样可以减少模块之间的耦合,提高系统的灵活性和可维护性。
在使用 Spring 框架中,我们大量应用了 DIP 原则。
考虑一个用户管理的场景,我们通常会设计一个 UserService
类来处理用户的信息。如果我们需要保存用户信息,我们应该将保存用户信息的逻辑放在一个独立的类 UserRepository
中,而不是直接在 UserService
类中 实现。
UserRepository
中应该定义以下功能:
public interface UserRepository {
void save(User user);
}
对于 `UserService` 类,它不需要关注用户信息存储在 `MySQL` 还是 `MongoDB` 中,它只需要关注 `UserRepository` 提供了 `save` 方法即可,实际存储的逻辑由 `UserRepository` 实现。
```java
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void save(User user) {
userRepository.save(user);
}
}