面向对象设计的五大原则(SOLID 原则)
面向对象设计的五大原则(SOLID 原则)是指导我们设计可维护、灵活且易扩展的面向对象系统的核心准则。这些原则帮助开发者避免常见的设计陷阱,使代码更具可读性和可维护性。
0.设计原则和设计模式的关系
设计原则(Design Principles)指的是抽象性比较高、编程都应该遵守的原则,对应的设计模式(Design Pattens)是解决具体场景下特定问题的套路,这里要对两个概念进行区分。换句话说,设计模式要遵循设计原则。
1. 单一职责原则(SRP - Single Responsibility Principle)
定义:一个类应该只有一个引起它变化的原因,即一个类只负责一个功能或职责。
示例:
假设我们有一个类负责处理用户信息,同时负责生成用户报告。这样当用户信息或报告格式发生变化时,都会影响到同一个类,违背了 SRP。
class User {
String name;
String email;
public void saveUser() {
// 保存用户信息到数据库
}
public void generateUserReport() {
// 生成用户报告
}
}
改进:将用户管理和报告生成拆分为两个类。
class User {
String name;
String email;
public void saveUser() {
// 保存用户信息到数据库
}
}
class UserReportGenerator {
public void generateUserReport(User user) {
// 生成用户报告
}
}
这样,如果用户管理和报告生成的逻辑变更,它们只会影响各自相关的类。
2. 开放封闭原则(OCP - Open/Closed Principle)
定义:软件实体(类、模块、函数等)应该对扩展开放,对修改封闭。即当系统需求变化时,应该通过扩展类的行为,而不是修改已有的类来实现。
示例:
假设我们有一个用于处理图形的类,包含绘制不同图形的逻辑。
class GraphicEditor {
public void drawShape(Shape s) {
if (s instanceof Circle) {
drawCircle((Circle) s);
} else if (s instanceof Square) {
drawSquare((Square) s);
}
}
private void drawCircle(Circle c) {
// 绘制圆形
}
private void drawSquare(Square s) {
// 绘制方形
}
}
如果需要支持绘制新形状,例如三角形,就需要修改 drawShape
方法,违反了 OCP。
改进:通过抽象类或接口实现扩展性。
abstract class Shape {
public abstract void draw();
}
class Circle extends Shape {
@Override
public void draw() {
// 绘制圆形
}
}
class Square extends Shape {
@Override
public void draw() {
// 绘制方形
}
}
class GraphicEditor {
public void drawShape(Shape s) {
s.draw();
}
}
现在如果要支持新形状,只需要添加新类,而不需要修改已有代码。
3. 里氏替换原则(LSP - Liskov Substitution Principle)
定义:子类对象必须能够替换其父类对象,程序的行为应该保持不变。即子类应当完整实现父类的行为,而不应违背父类的契约。
示例:
假设我们有一个 Rectangle
(矩形)类,后来引入了 Square
(正方形)作为它的子类。
class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
}
class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // 正方形的宽和高必须相等
}
@Override
public void setHeight(int height) {
this.height = height;
this.width = height; // 正方形的宽和高必须相等
}
}
虽然 Square
是 Rectangle
的子类,但由于 Square
违反了矩形的宽高独立性,它无法正确替换 Rectangle
。
改进:避免继承错误的层次关系。
class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
}
class Square {
private int sideLength;
public void setSideLength(int sideLength) {
this.sideLength = sideLength;
}
}
正方形和矩形应该是独立的类,不应通过继承关系来实现。
4. 接口隔离原则(ISP - Interface Segregation Principle)
定义:一个类不应该依赖于它不需要的接口。即接口应该尽量小而精简,不要强迫实现类去实现无关的方法。
示例:
假设有一个大型接口 Worker
,它定义了很多不同类型工人的职责。
interface Worker {
void work();
void eat();
}
class Developer implements Worker {
@Override
public void work() {
// 开发代码
}
@Override
public void eat() {
// 吃午餐
}
}
class Robot implements Worker {
@Override
public void work() {
// 执行任务
}
@Override
public void eat() {
// 机器人不需要吃饭
}
}
Robot
需要实现 eat
方法,但实际上并不需要这个功能,违反了 ISP。
改进:将接口分割成多个小接口。
interface Workable {
void work();
}
interface Eatable {
void eat();
}
class Developer implements Workable, Eatable {
@Override
public void work() {
// 开发代码
}
@Override
public void eat() {
// 吃午餐
}
}
class Robot implements Workable {
@Override
public void work() {
// 执行任务
}
}
现在 Robot
只需实现 Workable
接口,不再被迫实现与自己无关的功能。
5. 依赖倒置原则(DIP - Dependency Inversion Principle)
定义:高层模块不应该依赖于低层模块,二者都应该依赖于抽象。换句话说,依赖于抽象,而不是具体实现。
示例:
假设我们有一个 Developer
类,它依赖于具体的 BackendDeveloper
和 FrontendDeveloper
类。
class BackendDeveloper {
public void writeJava() {
// 编写 Java 代码
}
}
class FrontendDeveloper {
public void writeJavaScript() {
// 编写 JavaScript 代码
}
}
class Project {
private BackendDeveloper backendDeveloper = new BackendDeveloper();
private FrontendDeveloper frontendDeveloper = new FrontendDeveloper();
public void implement() {
backendDeveloper.writeJava();
frontendDeveloper.writeJavaScript();
}
}
Project
类直接依赖于具体的开发者实现类,违反了 DIP。
改进:通过抽象接口进行依赖倒置。
interface Developer {
void writeCode();
}
class BackendDeveloper implements Developer {
@Override
public void writeCode() {
// 编写 Java 代码
}
}
class FrontendDeveloper implements Developer {
@Override
public void writeCode() {
// 编写 JavaScript 代码
}
}
class Project {
private List<Developer> developers;
public Project(List<Developer> developers) {
this.developers = developers;
}
public void implement() {
for (Developer developer : developers) {
developer.writeCode();
}
}
}
现在 Project
类依赖于 Developer
接口,而不是具体的开发者实现类,符合 DIP 原则。
总结:
- SRP:一个类只负责一件事。
- OCP:类应该通过扩展而非修改来应对需求变化。
- LSP:子类可以替代父类,不改变程序行为。
- ISP:接口应该小而精,避免无关功能。
- DIP:高层模块依赖于抽象而非具体实现。