五项 SOLID 原则以及为什么应该在代码库中使用它们
SOLID 概述了一组可供开发人员用来简化和阐明其代码的准则。虽然它们并非法律,但理解这些概念将使您成为更优秀的开发人员。概括而言,SOLID 的五项原则如下:
- 单一职责原则;一个类应该只具有单一职责,也就是说,只有对软件规范的一部分的更改才能够影响该类的规范。
- 开放封闭原则;您的课程应该对扩展开放,但对修改关闭。
- 里氏替换原则;程序中的对象应该可以用其子类型的实例替换,而不会改变该程序的正确性。
- 接口隔离原则;多个客户端特定接口比一个通用接口更好。
- 依赖倒置原则;应该依赖抽象,而不是具体。
如果你第一次阅读时没有完全理解这些原则的含义,也不用担心——我就是这样。现在我将逐一讲解每个原则,并尝试解释它们不仅如何运作,还会解释它们将如何从长远来看使你受益。
单一职责原则
SOLID 设计原则中最常见的是单一职责原则,它指出一个类应该只有一个更改的原因。当一个类处理多个职责时,对功能所做的任何更改都可能以意想不到的方式在整个应用程序中传播。如果您的应用程序规模较小,这种意外行为可能会造成严重后果,但当您处理大型企业级软件时,情况可能会更加糟糕。通过确保每个函数仅封装一个职责,您可以节省大量测试时间并创建更易于维护的架构。
让我给你举个例子。我将使用 PHP,但你也可以将 SOLID 设计原则应用于任何其他 OOP 语言。
假设我们有一个表示文本文档的类,该文档包含标题和内容。该文档必须能够导出为 HTML 和 PDF。
<?php
class Document
{
protected $title;
protected $content;
public function __construct(string $title, string $content)
{
$this->title = $title;
$this->content= $content;
}
public function getTitle(): string
{
return $this->title;
}
public function getContent(): string
{
return $this->content;
}
public function exportHtml() {
echo "DOCUMENT EXPORTED TO HTML".PHP_EOL;echo "Title: ".$this->getTitle().PHP_EOL;
echo "Content: ".$this->getContent().PHP_EOL.PHP_EOL;
}
public function exportPdf() {
echo "DOCUMENT EXPORTED TO PDF".PHP_EOL;
echo "Title: ".$this->getTitle().PHP_EOL;
echo "Content: ".$this->getContent().PHP_EOL.PHP_EOL;
}
}
在这种情况下,将自身导出为特定格式不是文档的责任,文档应该只是其自身的一种表现形式。
解决这个问题的关键是将每个导出方法移到它们自己的类中,这些类将实现“可导出”接口。
<?php
interface ExportableDocumentInterface
{
public function export(Document $document);
}
接下来我们要做的是提取不适用于该类的逻辑。
<?php
class PdfExportableDocument implements ExportableDocumentInterface
{
public function export(Document $document)
{
echo "DOCUMENT EXPORTED TO PDF".PHP_EOL;
echo "Title: ".$document->getTitle().PHP_EOL;
echo "Content: ".$document->getContent().PHP_EOL.PHP_EOL;
}
}
class HtmlExportableDocument implements ExportableDocumentInterface
{
public function export(Document $document)
{
echo "DOCUMENT EXPORTED TO HTML".PHP_EOL;
echo "Title: ".$document->getTitle().PHP_EOL;
echo "Content: ".$document->getContent().PHP_EOL.PHP_EOL;
}
}
留下这样的文档类
<?php
class Document
{
protected $title;
protected $content;
public function __construct(string $title, string $content)
{
$this->title = $title;
$this->content= $content;
}
public function getTitle(): string
{
return $this->title;
}
public function getContent(): string
{
return $this->content;
}
}
开放封闭原则
开放封闭原则指出,对象或实体应该对扩展开放,但对修改关闭。这是开发人员经常忽略的原则之一,但尽量避免这样做。这些技巧对于成熟的设计至关重要。
因此,您应该能够使用诸如通过子类和接口继承之类的功能来扩展现有代码。但是,切勿修改已存在的类、接口和其他代码单元,因为这可能会导致意外行为。如果您通过扩展代码而不是修改代码来添加新功能,则可以尽可能降低失败的风险。
假设我们需要实现一个登录系统。为了验证用户身份,我们需要用户名和密码,目前为止一切顺利。那么,如果一年后我们想要让用户通过 Twitter 或 Facebook 进行身份验证,该怎么办呢?重要的是要明白,我们被要求的不是对现有功能的修改,而是构建一个新功能。
假设我们的身份验证类看起来像这样,您可以在其中为您的用户调用身份验证方法。
<?php
class LoginService
{
public function login($user)
{
$this->authenticateUser($user);
}
}
当涉及到实现我们的第三方用户时,我们可能想尝试这样的事情,我们使用 if 语句检查我们有什么类型的用户,并相应地执行代码。
<?php
class LoginService
{
public function login($user)
{
if ($user instanceof User) {
$this->authenticateUser($user);
} else if ($user instanceOf ThirdPartyUser) {
$this->authenticateThirdPartyUser($user);
}
}
}
这不太好,因为我们正在修改已有的代码。现在看起来可能不错,但当你支持五六种身份验证类型时会发生什么?相反,你应该抽象并实现一个接口。我们应该做的第一件事就是构建一个符合我们特定用例需求的接口。
<?php
interface LoginInterface
{
public function authenticateUser($user);
}
现在我们可以解耦我们已经为用例创建的逻辑,然后使用我们的新接口实现一个类。
<?php
class UserAuthentication implements LoginInterface
{
public function authenticateUser($user)
{
// TODO: Implement authenticateUser() method.
}
}
Class ThirdPartyUserAuthentication implements LoginInterface
{
public function authenticateUser($user)
{
// TODO: Implement authenticateUser() method.
}
}
现在我们的 LoginService 类不关心我们有什么类型的用户,它只与“LoginInterface”交互。
<?php
class LoginService
{
public function login(LoginInterface $user)
{
$user->authenticateUser($user);
}
}
里氏替换
该原则由 Barbara Liskov 提出,它指出,任何抽象(接口)的实现都应该可以在任何接受抽象的地方进行替换。
通俗地说,它规定父类的对象应该可以被其子类的对象替换,而不会在应用程序中引发问题。因此,子类永远不应该改变其父类的特征(例如参数列表和返回类型)。您可以通过注意正确的继承层次结构来实现里氏替换原则。
假设我们有一个运输类,它将根据产品的重量和目的地计算产品的运输成本。
<?php
class Shipping
{
public function calculateShippingCost($weightOfPackageKg, $destiny)
{
// Pre-condition:
if ($weightOfPackageKg <= 0) {
throw new \Exception('Package weight cannot be less than or equal to zero');
}
// We calculate the shipping cost by
$shippingCost = rand(5, 15);
// Post-condition
if ($shippingCost <= 0) {
throw new \Exception('Shipping price cannot be less than or equal to zero');
}
return $shippingCost;
}
}
但对于全球运输,我们希望这些规则略有不同,因此我们创建了一个扩展 Shipping 类的子类。
<?php
class WorldWideShipping extends Shipping
{
public function calculateShippingCost($weightOfPackageKg, $destiny)
{
// Pre-condition
if ($weightOfPackageKg <= 0) {
throw new \Exception('Package weight cannot be less than or equal to zero');
}
// We strengthen the pre-conditions
if (empty($destiny)) {
throw new \Exception('Destiny cannot be empty');
}
// We calculate the shipping cost by
$shippingCost = rand(5, 15);
// By changing the post-conditions we allow there to be cases
// in which the shipping is 0
if ('Spain' === $destiny) {
$shippingCost = 0;
}
return $shippingCost;
}
}
这里的问题是,全球运输方式没有提供相同的实现,正如$destiny
现在所见,如果为空则会引发异常。
避免破坏 LSP 的最佳方法是使用接口。而不是从父类扩展我们的子类。
<?php
interface CalculabeShippingCost
{
public function calculateShippingCost($weightOfPackageKg, $destiny);
}
<?php
class WorldWideShipping implements CalculabeShippingCost
{
public function calculateShippingCost($weightOfPackageKg, $destiny)
{
// Implementation of logic
}
}
通过使用接口,您可以实现不同类所共有的方法,但每种方法都有自己的实现、自己的前置条件和后置条件等。我们并不依赖于父类。
接口隔离原则
接口隔离原则指出,永远不应强迫客户端实现它不使用的接口。你会发现,这一切都取决于知识。
违反接口隔离原则会损害代码的可读性,并迫使程序员编写空的存根方法,而这些方法什么也不做。在设计良好的应用程序中,应该避免接口污染(也称为胖接口)。解决方案是创建更小、更灵活的接口。
假设我们有一个类代表一本精装书,另一个类代表一本有声读物。我们想创建一个接口来表示用户可以对这本书执行的操作。
<?php
interface BookAction {
public function seeReviews();
public function searchSecondhand();
public function listenSample();
}
现在,如果我们要将此实现添加到我们的类中,那么这两个类现在都必须包含与它们无关的方法。例如,HardcoverBook 类不能包含可供收听的样本。同样,有声读物没有二手副本,因此 AudioBook 类也不需要它。
<?php
class HardcoverBook implements BookAction {
public function seeReviews() {...}
public function searchSecondhand() {...}
public function listenSample() {...}
}
class AudioBook implements BookAction {
public function seeReviews() {...}
public function searchSecondhand() {...}
public function listenSample() {...}
}
然而,由于 BookAction 接口包含这些方法,它的所有依赖类都必须实现它们。换句话说,BookAction 是一个污染接口,我们需要将其隔离。让我们扩展它,添加两个更具体的接口:HardcoverAction 和 AudioAction。
<?php
interface BookAction {
public function seeReviews();
}
interface HardcoverAction extends BookAction {
public function searchSecondhand();
}
interface AudioAction extends BookAction {
public function listenSample();
}
现在,HardcoverBook 类可以实现 HardcoverAction 接口,AudioBook 类也可以实现 AudioAction 接口。这样,两个类都可以实现 BookAction 父接口的 seeReviews() 方法。不过,HardcoverBook 不必实现不相关的 listenSample() 方法,AudioBook 也不必实现 searchSecondhand() 方法。
依赖倒置原则
依赖倒置原则指出,高级模块永远不应该依赖于低级模块,相反,高级模块可以依赖于一个抽象,而低级模块又依赖于同一个抽象。这可不是我们见过的最简单的说法。简而言之……不,这么复杂的说法根本无法简化。
我们来看一个例子,以这个 PasswordReminder 类为例。我们将 MySQLConnection 传递给构造函数。这看起来可能合理,但这违反了依赖倒置原则。高级类(PasswordReminder)现在依赖于低级类(MySQLConnection)。
<?php
class PasswordReminder {
protected $dbConnection;
public function __construct(MySQLConnection $dbConnection)
{
$this->dbConnection = $dbConnection;
}
}
那么我们该怎么做才能解决这个问题呢?好吧,我们编写一个接口代码。你可能已经注意到,接口是遵循 SOLID 原则的非常有用的工具。所以,如果我们设置一个 ConnectionInterface,它可以有一个 collect 方法。
<?php
interface ConnectionInterface()
{
public function connect();
}
现在,如果我们遵循原则,我们应该将 PasswordReminder 类更改为使用此接口而不是接口的实现。
<?php
class PasswordReminder {
protected $dbConnection;
public function __construct(ConnectionInterface $dbConnection)
{
$this->dbConnection = $dbConnection;
}
}
在软件项目中运用这些原则的目的是充分利用面向对象范式的优势,避免诸如代码标准化不足、代码重复等问题。如果我们能够遵循所有这些技巧,就能轻松维护、测试、重用和扩展代码。下一步,开始在个人、小型且简单的项目上进行训练。你可以从修改特定的类开始。很快,你就能开始训练你的大脑,使其在面对更复杂的开发情况时能够更加成熟地思考。
链接链接:https://dev.to/tomgreen/the-five-solid-priciples-and-why-you-should-use-them-in-your-codebase-2f3n