Java编程中必知必会的五条SOLID原则

2023年 10月 10日 53.2k 0

在面向对象编程(OOP)领域,SOLID原则是类设计的指导准则。这五个原则形成了一套规则和最佳实践,开发人员在设计类结构时应遵循这些原则。通过理解和应用这些原则,我们可以发挥出设计模式的潜力,创建强大的软件架构。

在本文中,我们介绍SOLID原则的核心内容,并帮助您理解这些原则如何应用到项目中。

核心目标:编写可理解、无回归和可测试的代码

SOLID原则的核心目标是构建可理解、无回归、可读和可测试的代码。这些代码作为一个画布,多个开发人员可以轻松地进行协作,促进生产力和创新的环境。

现在,我们逐个深入讨论SOLID原则,揭示它们在塑造类设计中的重要性。

  • S — 单一责任原则
  • O — 开放封闭原则
  • L — 里氏替换原则
  • I — 接口隔离原则
  • D — 依赖反转原则

1. 单一责任原则

SOLID的第一个支柱,单一责任原则(SRP),强调一个类应该只有一个变化的原因。遵循这个原则,我们确保每个类只负责一个任务,可使将来的维护和修改更容易。

/*手机类,具有属性和构造函数*/
Class MobilePhone{
    String brandName;
    Float price;
    Date manufactureDate;
  public MobilePhone(String brandName,Float price,Date manufactureDate){
      this.brandName=brandName;
      this.price=price;
      this.manufactureDate=manufactureDate;
  }
};

/*Invoice类拥有一个手机(mobile phone)和该手机的数量(quantity)*/
Class Invoice{
    private MobilePhone mPhone;
    int quantity;
  public Invoice(MobilePhone mPhone,quantity){
    this.mPhone=mPhone;
    this.quantity=quantity;
  }
  public float calculateTotalPrice(){
    return mPhone.price*this.quantity;//返回发票的总金额
  }
  public void printInvoice(){
    //打印发票的逻辑
  }
  public void sendNotification(){
    //发送通知的逻辑
  }

}

以上代码,与计算、打印或通知逻辑相关的任何更改都需要修改Invoice类。因此,发票类缺乏明确的关注点分离,对一个方面的修改会影响到其他功能。为了遵循SRP,关键是将Invoice类重构为更小、更专注的类, 每个类都独立处理特定的职责,比如计算、打印或通知逻辑。

根据职责将代码分离成独立的类是遵循单一责任原则(SRP)并促进可维护和灵活的代码库的正确方法。

在这种设计中,我们应该有:

  • Invoice类只包含计算逻辑。
  • InvoicePrint类只包含打印逻辑。
  • InvoiceNotify类只包含发送通知的逻辑。
/*手机类,具有属性和构造函数*/
Class MobilePhone{
    String brandName;
    Float price;
    Date manufactureDate;
  public MobilePhone(String brandName,Float price,Date manufactureDate){
      this.brandName=brandName;
      this.price=price;
      this.manufactureDate=manufactureDate;
  }
};

/*发票类,拥有手机和数量属性*/
Class Invoice{
    private MobilePhone mPhone;
    int quantity;
  public Invoice(MobilePhone mPhone,quantity){
    this.mPhone=mPhone;
    this.quantity=quantity;
  }
  public float calculateTotalPrice(){
    return mPhone.price*this.quantity;//返回发票的总金额
  }
}

Class InvoicePrint{
  private Invoice invoice;
  
  public InvoicePrint(Invoice invoice){
  this.invoice=invoice
  }
  public void printInvoice(){
    //打印发票的逻辑
  }
}/*如果打印逻辑发生变化,只有InvoicePrint类会发生变化。*/
Class InvoiceNotify{
  private Invoice invoice;
  
  public InvoiceNotify(Invoice invoice){
  this.invoice=invoice
  public void sendNotification(){
    //发送通知给用户的逻辑
  }
}/*如果通知逻辑发生变化,只有InvoiceNotify类会发生变化。*/

2. 开放-封闭原则

第二个原则是开放-封闭原则(OCP),它鼓励软件实体对扩展开放,但对修改封闭。换句话说,一旦一个类被建立,它应该能够轻松地进行扩展,而不需要修改其现有的代码。这促进了代码的重用和稳定性。

让我们以上面使用的InvoiceNotify类为例,InvoiceNotify类经过测试,并且当前在客户端中实际使用,通过电子邮件发送发票通知。

现在有一个客户需求,他们需要通过推送通知发送通知。

Class InvoiceNotify{
  private Invoice invoice;
  
  public InvoiceNotify(Invoice invoice){
  this.invoice=invoice
  public void sendNotification(){
    //发送通知给用户的逻辑
  }
  public void sendPushNotification(){
    //发送推送通知给用户的逻辑
  }
}

以上代码,通过在现有类中添加一个新方法,我们违反了开放/封闭原则

与其在现有类中添加一个新方法,我们应该设计一个接口并在各个类中实现。

Interface InvoiceNotification{
 public void sendNotification();
}
Class EmailNotification implements InvoiceNotification{
 private Invoice invoice;
  public EmailNotification(Invoice invoice){
     this.invoice=invoice;
  }
  @Override
  public void sendNotification(){
  //通过电子邮件发送通知的逻辑
  }
}
Class PushNotification implements InvoiceNotification{
 private Invoice invoice;
  public PushNotification(Invoice invoice){
     this.invoice=invoice;
  }
  @Override
  public void sendNotification(){
  //发送推送通知的逻辑
  }
}

如果进一步增强需求,需要通过短信发送通知,无需修改现有类。相反,我们可以创建一个名为TextNotification的新类,它实现了InvoiceNotification接口并重写了sendNotification()方法。这样,我们就能够顺利地集成新功能,而不会破坏现有的代码库。

3. 里氏替换原则

里氏替换原则(LSP)定义了基类和派生类之间的契约。它规定派生类应该能够替代其基类,而不会影响程序的正确性。实质上,遵循这个原则可以确保继承被谨慎地使用,并保持类层次结构的完整性。

例如:在数学中,正方形可以被归类为矩形的一种特殊形式。它们之间的“是一个”关系可能会导致我们考虑在代码中使用继承来建模这种关系。然而,将正方形实现为矩形的派生类可能会导致意外的行为。

在数学中,正方形确实是矩形的一种特殊形式,正如“是一个”关系所暗示的那样。这往往会引诱我们在代码中使用继承来建模这种关系。然而,将正方形实现为矩形的派生类可能会导致意想不到和违反直觉的行为。

我们用一个简单的Java代码示例来说明这个问题:

class Rectangle {
    protected int width;
    protected int height;

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int calculateArea() {
        return width * 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; // 正方形的边长始终相等,所以两个维度都设置为相同的值。
    }
}

public class Main {
    public static void main(String[] args) {
        Rectangle rectangle = new Square();
        rectangle.setWidth(5);
        rectangle.setHeight(3);

        System.out.println("Area: " + rectangle.calculateArea());
    }
}

在这个例子中,我们有一个基类Rectangle,其中包含setWidth和setHeight方法,分别用于设置矩形的宽度和高度。Square类继承Rectangle类,并重写这些方法,以确保两个维度保持相等,以保持正方形的特性。

在主方法中,我们创建一个Rectangle引用,指向一个Square对象。当我们尝试为宽度和高度设置不同的值(分别为5和3)时,我们得到了一个边长为3的正方形,而不是实际宽度为5、高度为3的矩形。因此,计算得到的面积(9)与我们期望从宽度为5、高度为3的矩形得到的面积不符。

这个场景展示了里氏替换原则被违反的情况,通过Rectangle引用使用Square对象导致了意外的行为。

为了解决Square继承Rectangle的问题,我们需要重新评估继承关系和类设计。一种方法是在这种情况下避免使用继承,而是专注于公共接口或组合。我们用Java代码来说明解决方案:

interface Shape {
    int calculateArea();
}

class Rectangle implements Shape {
    protected int width;
    protected int height;

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    @Override
    public int calculateArea() {
        return width * height;
    }
}

class Square implements Shape {
    protected int side;

    public void setSide(int side) {
        this.side = side;
    }

    @Override
    public int calculateArea() {
        return side * side;
    }
}

public class Main {
    public static void main(String[] args) {
        Shape rectangle = new Rectangle();
        rectangle.setWidth(5);
        rectangle.setHeight(3);
        System.out.println("矩形面积: " + rectangle.calculateArea());

        Shape square = new Square();
        square.setSide(5);
        System.out.println("正方形面积: " + square.calculateArea());
    }
}

在这个解决方案中,我们引入了一个名为Shape的公共接口,定义了calculateArea()方法。现在,Rectangle和Square都实现了这个接口。Rectangle类保留了setWidth和setHeight方法,而Square类有一个setSide方法。每个类根据自己特定的属性计算面积。

现在,在main方法中,我们为Rectangle和Square对象分别创建了不同的Shape引用。我们可以适当设置尺寸而不会遇到任何问题。

通过使用组合和共同接口,我们确保每个形状都能独立运作,并且按预期运行,而不违反里氏替换原则。这种设计使我们能够优雅地处理不同的形状,促进了更清晰和可维护的代码库。

4. 接口隔离原则

接口隔离原则(ISP)建议客户端不应被强迫依赖于它们不使用的接口。与其拥有庞大而笨重的接口,更好的做法是创建小而专注的接口,以满足客户端的特定需求。

让我们通过一个简单的Java代码示例来说明ISP:

假设我们有一个名为Printer的接口,提供打印功能:

interface DocumentProcessor {
    void print();
    void fax();
}

class LaserPrinter implements DocumentProcessor {
    @Override
    public void print() {
        System.out.println("Printing with a laser printer.");
    }

    @Override
    public void fax() {
        System.out.println("Sending a fax with a laser printer.");
    }
}

class FaxMachine implements DocumentProcessor {
    @Override
    public void print() {
        // 传真机无法打印,所以将这个方法保持为空。
    }

    @Override
    public void fax() {
        System.out.println("Sending a fax with a fax machine.");
    }
}

这个设计的问题在于FaxMachine类对于print()方法没有有意义的实现,因为传真机无法打印文档。尽管如此,FaxMachine类仍然被强制实现print()方法,这是因为DocumentProcessor接口的设计。

这种对接口隔离原则的违反显而易见,因为FaxMachine类现在需要实现它不需要或使用的方法。

5. 依赖反转原则

SOLID原则的最后一块拼图是依赖反转原则(Dependency Inversion Principle,DIP)。该原则主张高层模块不应依赖于低层模块,而应依赖于抽象。通过遵循这一原则,我们实现了解耦,从而增强了灵活性、可维护性和测试的便捷性。

让我们通过一个小的Java代码示例来说明违反依赖反转原则的情况:

假设我们有一个ReportGenerator类,它直接依赖于一个DatabaseConnection类:

class DatabaseConnection {
    public void connect() {
        System.out.println("Connected to the database.");
    }

    public void executeQuery(String query) {
        System.out.println("Executing query: " + query);
    }

    public void close() {
        System.out.println("Connection closed.");
    }
}

class ReportGenerator {
    private DatabaseConnection databaseConnection;

    public ReportGenerator() {
        this.databaseConnection = new DatabaseConnection();
    }

    public void generateReport() {
        databaseConnection.connect();
        databaseConnection.executeQuery("SELECT * FROM data_table");
        databaseConnection.close();
        System.out.println("Report generated successfully.");
    }
}

在这段代码中,ReportGenerator类在其构造函数中直接创建了一个DatabaseConnection实例。结果,ReportGenerator与DatabaseConnection紧密耦合。对DatabaseConnection类的任何更改都可能会影响到ReportGenerator。

为了解决这个问题,我们需要应用依赖反转原则,引入一个两个类都依赖的接口:

interface Connection {
    void connect();
    void executeQuery(String query);
    void close();
}

class DatabaseConnection implements Connection {
    @Override
    public void connect() {
        System.out.println("Connected to the database.");
    }

    @Override
    public void executeQuery(String query) {
        System.out.println("Executing query: " + query);
    }

    @Override
    public void close() {
        System.out.println("Connection closed.");
    }
}

class ReportGenerator {
    private Connection connection;

    public ReportGenerator(Connection connection) {
        this.connection = connection;
    }

    public void generateReport() {
        connection.connect();
        connection.executeQuery("SELECT * FROM data_table");
        connection.close();
        System.out.println("Report generated successfully.");
    }
}

public class Main {
    public static void main(String[] args) {
        Connection databaseConnection = new DatabaseConnection();
        ReportGenerator reportGenerator = new ReportGenerator(databaseConnection);

        reportGenerator.generateReport();
    }
}

通过遵循依赖反转原则,我们通过Connection接口解耦了ReportGenerator和DatabaseConnection类。这种方法允许我们在不修改ReportGenerator的情况下轻松切换和扩展Connection接口的实现。现在的代码符合原则,更易于维护和灵活。

结论

SOLID原则是面向对象类设计的基石,对于每个寻求创建高效、可维护和协作的软件的开发人员来说至关重要。当你踏上编码之旅时,请记住SOLID运用原则!

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论