Java SOLID原则详解
单一职责原则(SRP - Single Responsibility Principle)
- 原则定义 单一职责原则指出,一个类应该只有一个引起它变化的原因,即一个类应该只负责一项职责。在软件设计中,如果一个类承担了过多的职责,那么当其中一个职责发生变化时,可能会影响到其他职责的功能,这增加了软件的维护难度和出现 bug 的风险。
- 违背 SRP 的示例
假设我们正在开发一个简单的图书管理系统,其中有一个
BookService
类,代码如下:
public class BookService {
private BookDatabase bookDatabase;
public BookService(BookDatabase bookDatabase) {
this.bookDatabase = bookDatabase;
}
// 添加图书到数据库
public void addBook(Book book) {
bookDatabase.save(book);
}
// 从数据库删除图书
public void deleteBook(int bookId) {
bookDatabase.delete(bookId);
}
// 生成图书借阅报表
public String generateBorrowReport() {
List<Book> borrowedBooks = bookDatabase.getBorrowedBooks();
StringBuilder report = new StringBuilder();
for (Book book : borrowedBooks) {
report.append("Book Title: ").append(book.getTitle())
.append(", Borrower: ").append(book.getBorrower()).append("\n");
}
return report.toString();
}
}
在这个 BookService
类中,它既负责图书的数据库操作(添加和删除图书),又负责生成图书借阅报表。如果数据库的存储方式发生变化,不仅 addBook
和 deleteBook
方法可能需要修改,generateBorrowReport
方法中获取借阅图书的逻辑也可能受到影响。同时,如果报表的格式要求发生变化,整个 BookService
类都需要修改,这违背了单一职责原则。
3. 遵循 SRP 的重构
我们可以将 BookService
类拆分成两个类:BookDatabaseService
负责图书的数据库操作,BookReportService
负责生成报表。
public class BookDatabaseService {
private BookDatabase bookDatabase;
public BookDatabaseService(BookDatabase bookDatabase) {
this.bookDatabase = bookDatabase;
}
public void addBook(Book book) {
bookDatabase.save(book);
}
public void deleteBook(int bookId) {
bookDatabase.delete(bookId);
}
}
public class BookReportService {
private BookDatabase bookDatabase;
public BookReportService(BookDatabase bookDatabase) {
this.bookDatabase = bookDatabase;
}
public String generateBorrowReport() {
List<Book> borrowedBooks = bookDatabase.getBorrowedBooks();
StringBuilder report = new StringBuilder();
for (Book book : borrowedBooks) {
report.append("Book Title: ").append(book.getTitle())
.append(", Borrower: ").append(book.getBorrower()).append("\n");
}
return report.toString();
}
}
这样,当数据库操作或报表格式发生变化时,只需要修改对应的类,而不会影响到其他功能,代码的维护性和可扩展性得到了提高。
开闭原则(OCP - Open - Closed Principle)
- 原则定义 开闭原则表明,软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这意味着当我们需要增加新功能时,应该通过扩展现有代码来实现,而不是直接修改已有的代码。这样可以避免因为修改现有代码而引入新的 bug,同时提高软件的稳定性和可维护性。
- 违背 OCP 的示例
假设我们有一个图形绘制程序,其中有一个
Shape
类和一个ShapeDrawer
类,代码如下:
class Shape {
private String type;
public Shape(String type) {
this.type = type;
}
public String getType() {
return type;
}
}
class ShapeDrawer {
public void drawShape(Shape shape) {
if ("circle".equals(shape.getType())) {
System.out.println("Drawing a circle");
} else if ("rectangle".equals(shape.getType())) {
System.out.println("Drawing a rectangle");
}
}
}
如果现在要添加一种新的图形,比如三角形,就需要修改 ShapeDrawer
类的 drawShape
方法,在 if - else
语句中添加新的条件判断。这违背了开闭原则,因为每次添加新的图形类型都要修改 ShapeDrawer
类的代码,增加了引入 bug 的风险。
3. 遵循 OCP 的重构
我们可以通过使用接口和多态来重构代码,使其遵循开闭原则。首先定义一个 Shape
接口,然后为每种图形创建实现该接口的类。
interface Shape {
void draw();
}
class Circle implements Shape {
@Override
public void draw() {
System.out.println("Drawing a circle");
}
}
class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("Drawing a rectangle");
}
}
class ShapeDrawer {
public void drawShape(Shape shape) {
shape.draw();
}
}
现在,如果要添加新的图形,比如三角形,只需要创建一个实现 Shape
接口的 Triangle
类,而不需要修改 ShapeDrawer
类的代码。
class Triangle implements Shape {
@Override
public void draw() {
System.out.println("Drawing a triangle");
}
}
这样,系统对扩展开放(可以轻松添加新的图形类),对修改关闭(不需要修改 ShapeDrawer
类)。
里氏替换原则(LSP - Liskov Substitution Principle)
- 原则定义 里氏替换原则指出,所有引用基类(父类)的地方必须能透明地使用其子类的对象。这意味着子类对象应该能够替换掉它们的父类对象,而不会引起程序的错误或异常。子类必须遵循父类定义的契约,包括方法的签名和行为。
- 违背 LSP 的示例
假设我们有一个
Rectangle
类和一个Square
类,Square
类继承自Rectangle
类。
class Rectangle {
private int width;
private int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
class Square extends Rectangle {
public Square(int side) {
super(side, side);
}
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width);
}
@Override
public void setHeight(int height) {
super.setWidth(height);
super.setHeight(height);
}
}
现在有一个计算矩形面积总和的方法:
class AreaCalculator {
public static int calculateTotalArea(Rectangle[] rectangles) {
int totalArea = 0;
for (Rectangle rectangle : rectangles) {
rectangle.setWidth(5);
rectangle.setHeight(10);
totalArea += rectangle.getArea();
}
return totalArea;
}
}
如果我们传入一个包含 Square
对象的数组到 calculateTotalArea
方法中,由于 Square
类对 setWidth
和 setHeight
方法的重写,会导致 width
和 height
总是相等,这与方法中预期的 width = 5
和 height = 10
不符,从而得到错误的结果。这违背了里氏替换原则。
3. 遵循 LSP 的重构
一种重构方式是不使用继承关系,而是让 Square
和 Rectangle
实现一个共同的接口 Shape
。
interface Shape {
int getArea();
}
class Rectangle implements Shape {
private int width;
private int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
@Override
public int getArea() {
return width * height;
}
}
class Square implements Shape {
private int side;
public Square(int side) {
this.side = side;
}
@Override
public int getArea() {
return side * side;
}
}
class AreaCalculator {
public static int calculateTotalArea(Shape[] shapes) {
int totalArea = 0;
for (Shape shape : shapes) {
totalArea += shape.getArea();
}
return totalArea;
}
}
这样,Square
和 Rectangle
都可以在需要 Shape
的地方被使用,而不会出现违背里氏替换原则的问题。
接口隔离原则(ISP - Interface Segregation Principle)
- 原则定义 接口隔离原则提倡客户端不应该依赖它不需要的接口。也就是说,一个类对另一个类的依赖应该建立在最小的接口上。将臃肿庞大的接口拆分成更小、更具体的接口,让客户端只依赖它们实际需要的接口,这样可以提高代码的灵活性和可维护性。
- 违背 ISP 的示例
假设我们有一个
Animal
接口,包含eat
、fly
和swim
方法,然后有Bird
和Fish
类实现这个接口。
interface Animal {
void eat();
void fly();
void swim();
}
class Bird implements Animal {
@Override
public void eat() {
System.out.println("Bird is eating");
}
@Override
public void fly() {
System.out.println("Bird is flying");
}
@Override
public void swim() {
// 大多数鸟不会游泳,这里实现为空或者抛出异常
System.out.println("This bird can't swim");
}
}
class Fish implements Animal {
@Override
public void eat() {
System.out.println("Fish is eating");
}
@Override
public void fly() {
// 鱼不会飞,这里实现为空或者抛出异常
System.out.println("Fish can't fly");
}
@Override
public void swim() {
System.out.println("Fish is swimming");
}
}
在这个例子中,Bird
和 Fish
类都实现了它们并不需要的接口方法(Bird
的 swim
方法和 Fish
的 fly
方法),这违背了接口隔离原则。
3. 遵循 ISP 的重构
我们可以将 Animal
接口拆分成更小的接口:Eatable
、Flyable
和 Swimmable
。
interface Eatable {
void eat();
}
interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}
class Bird implements Eatable, Flyable {
@Override
public void eat() {
System.out.println("Bird is eating");
}
@Override
public void fly() {
System.out.println("Bird is flying");
}
}
class Fish implements Eatable, Swimmable {
@Override
public void eat() {
System.out.println("Fish is eating");
}
@Override
public void swim() {
System.out.println("Fish is swimming");
}
}
这样,Bird
和 Fish
类只实现它们实际需要的接口,符合接口隔离原则。
依赖倒置原则(DIP - Dependency Inversion Principle)
- 原则定义 依赖倒置原则主要包含两个方面:高层模块不应该依赖低层模块,两者都应该依赖抽象;抽象不应该依赖细节,细节应该依赖抽象。在实际编程中,这意味着我们应该尽量依赖接口或抽象类,而不是具体的实现类,从而降低模块之间的耦合度,提高系统的可维护性和可扩展性。
- 违背 DIP 的示例
假设我们有一个
EmailSender
类用于发送邮件,一个UserService
类用于处理用户相关业务,并且UserService
类依赖EmailSender
类来给用户发送注册邮件。
class EmailSender {
public void sendEmail(String to, String subject, String content) {
System.out.println("Sending email to " + to + " with subject: " + subject + " and content: " + content);
}
}
class UserService {
private EmailSender emailSender;
public UserService(EmailSender emailSender) {
this.emailSender = emailSender;
}
public void registerUser(String username, String email) {
// 处理用户注册逻辑
System.out.println("User " + username + " registered successfully");
emailSender.sendEmail(email, "Registration Confirmation", "Dear " + username + ", you have registered successfully.");
}
}
在这个例子中,UserService
类直接依赖于具体的 EmailSender
类。如果我们想要更换邮件发送方式(比如使用其他邮件服务提供商),就需要修改 UserService
类的代码,这增加了代码的耦合度。
3. 遵循 DIP 的重构
我们可以定义一个 EmailSender
接口,然后让具体的邮件发送类实现这个接口。
interface EmailSender {
void sendEmail(String to, String subject, String content);
}
class DefaultEmailSender implements EmailSender {
@Override
public void sendEmail(String to, String subject, String content) {
System.out.println("Sending email to " + to + " with subject: " + subject + " and content: " + content);
}
}
class UserService {
private EmailSender emailSender;
public UserService(EmailSender emailSender) {
this.emailSender = emailSender;
}
public void registerUser(String username, String email) {
// 处理用户注册逻辑
System.out.println("User " + username + " registered successfully");
emailSender.sendEmail(email, "Registration Confirmation", "Dear " + username + ", you have registered successfully.");
}
}
现在,如果要更换邮件发送方式,只需要创建一个新的实现 EmailSender
接口的类,而不需要修改 UserService
类的代码。例如:
class NewEmailSender implements EmailSender {
@Override
public void sendEmail(String to, String subject, String content) {
// 使用新的邮件发送逻辑
System.out.println("Using new email service to send to " + to + " with subject: " + subject + " and content: " + content);
}
}
然后在使用 UserService
时,可以传入 NewEmailSender
的实例:
public class Main {
public static void main(String[] args) {
EmailSender newEmailSender = new NewEmailSender();
UserService userService = new UserService(newEmailSender);
userService.registerUser("John", "john@example.com");
}
}
通过这种方式,UserService
类依赖的是抽象接口 EmailSender
,而不是具体的实现类,符合依赖倒置原则。