MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java SOLID原则详解

2024-06-037.5k 阅读

单一职责原则(SRP - Single Responsibility Principle)

  1. 原则定义 单一职责原则指出,一个类应该只有一个引起它变化的原因,即一个类应该只负责一项职责。在软件设计中,如果一个类承担了过多的职责,那么当其中一个职责发生变化时,可能会影响到其他职责的功能,这增加了软件的维护难度和出现 bug 的风险。
  2. 违背 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 类中,它既负责图书的数据库操作(添加和删除图书),又负责生成图书借阅报表。如果数据库的存储方式发生变化,不仅 addBookdeleteBook 方法可能需要修改,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)

  1. 原则定义 开闭原则表明,软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这意味着当我们需要增加新功能时,应该通过扩展现有代码来实现,而不是直接修改已有的代码。这样可以避免因为修改现有代码而引入新的 bug,同时提高软件的稳定性和可维护性。
  2. 违背 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)

  1. 原则定义 里氏替换原则指出,所有引用基类(父类)的地方必须能透明地使用其子类的对象。这意味着子类对象应该能够替换掉它们的父类对象,而不会引起程序的错误或异常。子类必须遵循父类定义的契约,包括方法的签名和行为。
  2. 违背 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 类对 setWidthsetHeight 方法的重写,会导致 widthheight 总是相等,这与方法中预期的 width = 5height = 10 不符,从而得到错误的结果。这违背了里氏替换原则。 3. 遵循 LSP 的重构 一种重构方式是不使用继承关系,而是让 SquareRectangle 实现一个共同的接口 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;
    }
}

这样,SquareRectangle 都可以在需要 Shape 的地方被使用,而不会出现违背里氏替换原则的问题。

接口隔离原则(ISP - Interface Segregation Principle)

  1. 原则定义 接口隔离原则提倡客户端不应该依赖它不需要的接口。也就是说,一个类对另一个类的依赖应该建立在最小的接口上。将臃肿庞大的接口拆分成更小、更具体的接口,让客户端只依赖它们实际需要的接口,这样可以提高代码的灵活性和可维护性。
  2. 违背 ISP 的示例 假设我们有一个 Animal 接口,包含 eatflyswim 方法,然后有 BirdFish 类实现这个接口。
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");
    }
}

在这个例子中,BirdFish 类都实现了它们并不需要的接口方法(Birdswim 方法和 Fishfly 方法),这违背了接口隔离原则。 3. 遵循 ISP 的重构 我们可以将 Animal 接口拆分成更小的接口:EatableFlyableSwimmable

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");
    }
}

这样,BirdFish 类只实现它们实际需要的接口,符合接口隔离原则。

依赖倒置原则(DIP - Dependency Inversion Principle)

  1. 原则定义 依赖倒置原则主要包含两个方面:高层模块不应该依赖低层模块,两者都应该依赖抽象;抽象不应该依赖细节,细节应该依赖抽象。在实际编程中,这意味着我们应该尽量依赖接口或抽象类,而不是具体的实现类,从而降低模块之间的耦合度,提高系统的可维护性和可扩展性。
  2. 违背 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,而不是具体的实现类,符合依赖倒置原则。