面向对象编程的 SOLID 原则

发布时间 2023-07-29 16:36:31作者: 易先讯

SOLID 原则是面向对象 class 设计的五条原则。他们是设计 class 结构时应该遵守的准则和最佳实践。

通常,这五个原则可以帮助我们了解设计模式和软件架构。这是每个开发人员都应该了解的主题。

这篇文章介绍了在项目中使用 SOLID 原则的细节。

首先我们先看一下 SOLID 原则的历史。然后我们会设计一个 class 并且逐步改善它,来亲密接触 SOLID 原则,了解为什么使用以及怎么使用各个原则。

准备一杯咖啡或者茶,让我们马上开始!

背景

SOLID 原则首先由著名的计算机科学家 Robert C·Martin (著名的Bob大叔)由 2000 年在他的论文中提出。但是 SOLID 缩略词是稍晚由 Michael Feathers 先使用的。

Bob大叔也是畅销书《代码整洁之道》和《架构整洁之道》的作者,也是 "Agile Alliance" 的成员。

因此,代码整洁、面向对象架构、设计模式彼此互补并以这种方式连接就不足为奇了。

他们达成的目标是一致的:

“创建可多人协作的、易于理解的、易读的以及可测试的代码。”

现在依次看一下各个原则,SOLID 是以下是原则的缩写:

  • S 单一职责原则
  • O 开闭原则
  • L 里氏替换原则
  • I 接口隔离原则
  • D 依赖倒置原则

单一职责原则

单一职责原则的描述是 ** 一个 class 应该只做一件事,一个 class 应该只有一个变化的原因**。

更技术的描述该原则:应该只有一个软件定义的潜在改变(数据库逻辑、日志逻辑等)能够影响 class 的定义。

这意味着如果 class 是一个数据容器,比如 Book class 或者 Student class,考虑到这个实体有一些字段,应该只有我们更改了数据定义时才能够修改这些字段。

遵守单一职责原则很重要。首先,可能很多不同的团队可能修改同一个项目,可能因为不同的原因修改同一个 class,会导致冲突。

其次,单一职责更容易版本管理,比如,有一个持久化 class 处理数据库操作,我们在 GitHub 看到某个文件上有一处修改。如果遵循 SRP 原则,根据文件就能判断这是关于存储或者数据库相关的提交。

另一个例子是合并冲突,当不同的团队修改同一个文件时,如果遵循 SRP原则,冲突很少会发生,因为文件只有一个变化的原因,即使出现冲突也会很容易解决。

常见错误和反面教材

在本节我们会看一些违背单一职责原则的常见错误。然后会探讨修复他们的方法。

我们会以一个简单的书店发票程序代码作为例子。让我们从定义一个使用发票的图书 class 开始。


class Book {
	String name;
	String authorName;
	int year;
	int price;
	String isbn;

	public Book(String name, String authorName, int year, int price, String isbn) {
		this.name = name;
		this.authorName = authorName;
		this.year = year;
        this.price = price;
		this.isbn = isbn;
	}
}

这是一个有一些字段的 book class。没什么新奇的。之所以没有把字段设置为私有的是因为想专注于逻辑而不是 getter 和 setter。

现在让我们来创建一个 invoice class,包含创建发票和计算总额的业务逻辑。目前为止,假设书店只卖书,不卖别的。


public class Invoice {

	private Book book;
	private int quantity;
	private double discountRate;
	private double taxRate;
	private double total;

	public Invoice(Book book, int quantity, double discountRate, double taxRate) {
		this.book = book;
		this.quantity = quantity;
		this.discountRate = discountRate;
		this.taxRate = taxRate;
		this.total = this.calculateTotal();
	}

	public double calculateTotal() {
	        double price = ((book.price - book.price * discountRate) * this.quantity);

		double priceWithTaxes = price * (1 + taxRate);

		return priceWithTaxes;
	}

	public void printInvoice() {
            System.out.println(quantity + "x " + book.name + " " +          book.price + "$");
            System.out.println("Discount Rate: " + discountRate);
            System.out.println("Tax Rate: " + taxRate);
            System.out.println("Total: " + total);
	}

        public void saveToFile(String filename) {
	// Creates a file with given name and writes the invoice
	}

}

这是 invoice class。它包含一些发票相关的字段以及三个方法。

  • calculateTotal 方法,计算总价格
  • printInvoice 方法,打印发票信息到控制台
  • saveToFile 方法,负责将发票写到一个文件里

在读下一段之前停下来想一想,这样的 class 设计有什么问题。

那么问题出在哪呢? 我们的 class 在多个地方都违背了单一职责原则。

第一处是 printInvoice 方法,因为里面包含了打印逻辑。SRP 描述 class 应该只有一个变化的原因,这个变化原因应该是 class 里的发票计算。

在这个架构里,如果我们想要改变打印格式,我们需要修改这个 class。我们不能把打印逻辑和业务逻辑混合在一个class 里。

在 class 里面还有一个方法违背了 SRP: saveToFile 方法。这也是一个很常见的错误,把持久化逻辑和业务逻辑混合在了一起。

这不单单是写入文件 - 也可能是存库,发起 API 调用或者其他与持久化相关的操作。

你可能会问,怎样修复这个打印函数呢?

可以为打印和持久化逻辑创造一个新 class,因此就无需因为这些原因修改 invoice class 了。

创建两个 class, InvoicePrinter 和 InvoicePersistence ,并移入相应方法。


public class InvoicePrinter {
    private Invoice invoice;

    public InvoicePrinter(Invoice invoice) {
        this.invoice = invoice;
    }

    public void print() {
        System.out.println(invoice.quantity + "x " + invoice.book.name + " " + invoice.book.price + " $");
        System.out.println("Discount Rate: " + invoice.discountRate);
        System.out.println("Tax Rate: " + invoice.taxRate);
        System.out.println("Total: " + invoice.total + " $");
    }
}


public class InvoicePersistence {
    Invoice invoice;

    public InvoicePersistence(Invoice invoice) {
        this.invoice = invoice;
    }

    public void saveToFile(String filename) {
        // Creates a file with given name and writes the invoice
    }
}

现在 class 结构遵从了单一职责原则,每个 class 为我们应用的一个部分负责。棒!