核心要點
如果要對每個 SOLID 原則的相關性做出武斷的“所羅門式”決定,我會說依賴倒置原則 (DIP) 是最被低估的原則。雖然面向對象設計領域的一些核心概念一開始很難理解,例如關注點分離和實現切換,但另一方面,更直觀、更清晰的範例則更簡單,例如面向接口編程。不幸的是,DIP 的正式定義籠罩著雙刃劍般的詛咒/祝福,這往往使程序員忽略它,因為在許多情況下,人們默認認為該原則只不過是前面提到的“面向接口編程”戒律的另一種說法:
乍一看,上述陳述似乎不言自明。考慮到目前沒有人會不同意建立在對具體實現的強烈依賴之上的系統是不良設計的凶兆,因此切換一些抽像是完全合理的。因此,這將使我們回到起點,認為 DIP 的主要關注點是關於面向接口編程。實際上,在滿足該原則的要求時,將接口與實現解耦只是一個半成品的方法。缺少的部分是實現真正的反轉過程。當然,由此產生的問題是:什麼東西的反轉?傳統意義上,系統總是設計為讓高層組件(無論是類還是過程例程)依賴於低層組件(細節)。例如,日誌記錄模塊可能對一系列具體的日誌記錄器(實際上將信息記錄到系統中)具有很強的依賴性。因此,每當日誌記錄器的協議被修改時,這個方案都會將副作用嘈雜地向上層傳遞,即使該協議已經被抽像出來。然而,DIP 的實現有助於在一定程度上減輕這些漣漪,方法是讓日誌記錄模塊擁有協議,從而反轉整體依賴關係的流程。反轉後,日誌記錄器應該忠實地遵守協議,因此如果將來發生變化,它們應該相應地進行更改並適應協議的波動。簡而言之,這表明 DIP 在幕後比僅僅依賴於標準接口-實現解耦帶來的大量好處要復雜一些。是的,它討論了讓高層和低層模塊都依賴於抽象,但同時高層模塊必須擁有這些抽象——這是一個微妙但相關的細節,不能輕易忽略。正如您可能預期的那樣,一種可能幫助您更容易理解 DIP 實際涵蓋內容的方法是通過一些實踐代碼示例。因此,在本文中,我將設置一些示例,以便您可以學習如何在開發 PHP 應用程序時充分利用此 SOLID 原則。
開發一個簡單的存儲模塊(DIP 中缺失的“I”)
許多開發人員,特別是那些厭惡面向對象 PHP 的冰冷水域的開發人員,傾向於將 DIP 和其他 SOLID 原則視為僵化的教條,這與該語言固有的實用主義背道而馳。我可以理解這種想法,因為在野外很難找到展示該原則真正好處的實用 PHP 示例。我不是想把自己吹捧成一位開明的程序員(那套西裝不太合身),但為了一個好的目標而努力並從實踐的角度演示如何在現實用例中實現 DIP 還是很有用的。首先,考慮一下簡單文件存儲模塊的實現。該模塊負責讀取和寫入指定目標文件的數據。在非常簡化的層面,問題中的模塊可以這樣編寫:
<code class="language-php"><?php namespace LibraryEncoderStrategy; class Serializer implements Serializable { protected $unserializeCallback; public function __construct($unserializeCallback = false) { $this->unserializeCallback = (boolean) $unserializeCallback; } public function getUnserializeCallback() { return $this->unserializeCallback; } public function serialize($data) { if (is_resource($data)) { throw new InvalidArgumentException( "PHP resources are not serializable."); } if (($data = serialize($data)) === false) { throw new RuntimeException( "Unable to serialize the supplied data."); } return $data; } public function unserialize($data) { if (!is_string($data) || empty($data)) { throw new InvalidArgumentException( "The data to be decoded must be a non-empty string."); } if ($this->unserializeCallback) { $callback = ini_get("unserialize_callback_func"); if (!function_exists($callback)) { throw new BadFunctionCallException( "The php.ini unserialize callback function is invalid."); } } if (($data = @unserialize($data)) === false) { throw new RuntimeException( "Unable to unserialize the supplied data."); } return $data; } }</code>
<code class="language-php"><?php namespace LibraryFile; class FileStorage { const DEFAULT_STORAGE_FILE = "default.dat"; protected $serializer; protected $file; public function __construct(Serializable $serializer, $file = self::DEFAULT_STORAGE_FILE) { $this->serializer = $serializer; $this->setFile($file); } public function getSerializer() { return $this->serializer; } public function setFile($file) { if (!is_file($file) || !is_readable($file)) { throw new InvalidArgumentException( "The supplied file is not readable or writable."); } $this->file = $file; return $this; } public function getFile() { return $this->file; } public function resetFile() { $this->file = self::DEFAULT_STORAGE_FILE; return $this; } public function write($data) { try { return file_put_contents($this->file, $this->serializer->serialize($data)); } catch (Exception $e) { throw new Exception($e->getMessage()); } } public function read() { try { return $this->serializer->unserialize( @file_get_contents($this->file)); } catch (Exception $e) { throw new Exception($e->getMessage()); } } }</code>
該模塊是一個相當簡單的結構,僅由幾個基本組件組成。第一個類讀取和寫入文件系統中的數據,第二個類是一個簡單的 PHP 序列化器,用於在內部生成數據的可存儲表示。這些示例組件很好地在隔離狀態下執行其業務,並且可以像這樣連接在一起以同步工作:
<code class="language-php"><?php use LibraryLoaderAutoloader, LibraryEncoderStrategySerializer, LibraryFileFileStorage; require_once __DIR__ . "/Library/Loader/Autoloader.php"; $autoloader = new Autoloader; $autoloader->register(); $fileStorage = new FileStorage(new Serializer); $fileStorage->write(new stdClass()); print_r($fileStorage->read()); $fileStorage->write(array("This", "is", "a", "sample", "array")); print_r($fileStorage->read()); $fileStorage->write("This is a sample string."); echo $fileStorage->read();</code>
乍一看,考慮到該模塊的功能允許從文件系統中輕鬆保存和獲取各種數據,因此該模塊表現出相當不錯的行為。此外,FileStorage 類在構造函數中註入了一個 Serializable 接口,因此依賴於抽象提供的靈活性,而不是僵化的具體實現。憑藉這些優勢,該模塊有什麼問題呢?通常情況下,膚淺的第一印象可能會很棘手且模糊。仔細觀察一下,不僅 FileStorage 實際上依賴於序列化器,而且由於這種緊密依賴,從目標文件存儲和提取數據僅限於使用 PHP 的原生序列化機制。如果數據必須作為 XML 或 JSON 傳遞給外部服務會發生什麼情況?精心設計的模塊不再可重用。令人悲傷但真實!這種情況提出了一些有趣的問題。首先也是最重要的一點是,即使使它們相互操作的協議已經與實現隔離,FileStorage 仍然表現出對低層 Serializer 的強烈依賴。其次,問題中協議公開的通用性級別非常有限,僅限於將一個序列化器換成另一個序列化器。在這種情況下,依賴於抽像是一種虛幻的感知,而且 DIP 鼓勵的真正反轉過程從未實現。可以重構文件模塊的某些部分,使其忠實地遵守 DIP 的要求。這樣做,FileStorage 類將獲得用於存儲和提取文件數據的協議的所有權,從而擺脫對低層序列化器的依賴,並使您能夠在運行時在多個存儲策略之間切換。這樣做,您實際上將免費獲得很大的靈活性。因此,讓我們繼續前進,看看如何將文件存儲模塊轉換為真正符合 DIP 的結構。
反轉協議所有權和解耦接口與實現(充分利用 DIP)
雖然沒有很多選擇,但仍有一些方法可以有效地反轉 FileStorage 類及其低層協作者之間的協議所有權,同時保持協議的抽象性。但是,有一種方法非常直觀,因為它依賴於 PHP 命名空間開箱即用的自然封裝。為了將這個有點難以捉摸的概念轉化為具體的代碼,應該對模塊進行的第一個更改是定義一個更寬鬆的協議來保存和檢索文件數據,這樣就可以輕鬆地通過除 PHP 序列化之外的其他格式來操作它。如下所示的一個精簡的、隔離的接口可以優雅而簡單地完成這項工作:
<code class="language-php"><?php namespace LibraryEncoderStrategy; class Serializer implements Serializable { protected $unserializeCallback; public function __construct($unserializeCallback = false) { $this->unserializeCallback = (boolean) $unserializeCallback; } public function getUnserializeCallback() { return $this->unserializeCallback; } public function serialize($data) { if (is_resource($data)) { throw new InvalidArgumentException( "PHP resources are not serializable."); } if (($data = serialize($data)) === false) { throw new RuntimeException( "Unable to serialize the supplied data."); } return $data; } public function unserialize($data) { if (!is_string($data) || empty($data)) { throw new InvalidArgumentException( "The data to be decoded must be a non-empty string."); } if ($this->unserializeCallback) { $callback = ini_get("unserialize_callback_func"); if (!function_exists($callback)) { throw new BadFunctionCallException( "The php.ini unserialize callback function is invalid."); } } if (($data = @unserialize($data)) === false) { throw new RuntimeException( "Unable to unserialize the supplied data."); } return $data; } }</code>
EncoderInterface 的存在似乎對文件模塊的整體設計沒有產生深遠的影響,但它的作用遠不止表面承諾的那些。第一個改進是定義了一個高度通用的協議來編碼和解碼數據。第二個改進與第一個同樣重要,那就是現在協議的所有權屬於 FileStorage 類,因為該接口存在於該類的命名空間中。簡而言之,我們僅僅通過編寫一個正確命名空間的接口,就設法使仍然未定義的低層編碼器/解碼器依賴於高層 FileStorage。簡而言之,這就是 DIP 在其學術面紗背後所推崇的實際反轉過程。當然,如果 FileStorage 類沒有被修改為註入前面接口的實現者,那麼反轉將是一個笨拙的半途而廢的嘗試,因此以下是重構後的版本:
<code class="language-php"><?php namespace LibraryFile; class FileStorage { const DEFAULT_STORAGE_FILE = "default.dat"; protected $serializer; protected $file; public function __construct(Serializable $serializer, $file = self::DEFAULT_STORAGE_FILE) { $this->serializer = $serializer; $this->setFile($file); } public function getSerializer() { return $this->serializer; } public function setFile($file) { if (!is_file($file) || !is_readable($file)) { throw new InvalidArgumentException( "The supplied file is not readable or writable."); } $this->file = $file; return $this; } public function getFile() { return $this->file; } public function resetFile() { $this->file = self::DEFAULT_STORAGE_FILE; return $this; } public function write($data) { try { return file_put_contents($this->file, $this->serializer->serialize($data)); } catch (Exception $e) { throw new Exception($e->getMessage()); } } public function read() { try { return $this->serializer->unserialize( @file_get_contents($this->file)); } catch (Exception $e) { throw new Exception($e->getMessage()); } } }</code>
現在 FileStorage 在構造函數中明確聲明了編碼/解碼協議的所有權,唯一剩下的事情就是創建一組具體的低層編碼器/解碼器,從而允許您處理多種格式的文件數據。這些組件中的第一個只是之前編寫的 PHP 序列化器的重構實現:
<code class="language-php"><?php use LibraryLoaderAutoloader, LibraryEncoderStrategySerializer, LibraryFileFileStorage; require_once __DIR__ . "/Library/Loader/Autoloader.php"; $autoloader = new Autoloader; $autoloader->register(); $fileStorage = new FileStorage(new Serializer); $fileStorage->write(new stdClass()); print_r($fileStorage->read()); $fileStorage->write(array("This", "is", "a", "sample", "array")); print_r($fileStorage->read()); $fileStorage->write("This is a sample string."); echo $fileStorage->read();</code>
剖析 Serializer 背後的邏輯肯定是多餘的。儘管如此,值得指出的是,它現在不僅依賴於更寬鬆的編碼/解碼抽象,而且抽象的所有權在命名空間級別明確公開。同樣,我們可以更進一步,著手編寫更多編碼器,以便突出 DIP 帶來的好處。話雖如此,以下是另一個額外的低層組件的編寫方式:
<code class="language-php"><?php namespace LibraryFile; interface EncoderInterface { public function encode($data); public function decode($data); }</code>
正如預期的那樣,額外編碼器背後的底層邏輯通常類似於第一個 PHP 序列化器,除了任何明顯的改進和變體。此外,這些組件符合 DIP 強加的要求,因此遵守 FileStorage 命名空間中定義的編碼/解碼協議。由於文件模塊中的高層和低層組件都依賴於抽象,並且編碼器對文件存儲類具有明確的依賴性,因此我們可以安全地聲稱該模塊的行為符合 DIP 規範。此外,以下示例展示瞭如何將這些組件組合在一起:
<code class="language-php"><?php namespace LibraryFile; class FileStorage { const DEFAULT_STORAGE_FILE = "default.dat"; protected $encoder; protected $file; public function __construct(EncoderInterface $encoder, $file = self::DEFAULT_STORAGE_FILE) { $this->encoder = $encoder; $this->setFile($file); } public function getEncoder() { return $this->encoder; } public function setFile($file) { if (!is_file($file) || !is_readable($file)) { throw new InvalidArgumentException( "The supplied file is not readable or writable."); } $this->file = $file; return $this; } public function getFile() { return $this->file; } public function resetFile() { $this->file = self::DEFAULT_STORAGE_FILE; return $this; } public function write($data) { try { return file_put_contents($this->file, $this->encoder->encode($data)); } catch (Exception $e) { throw new Exception($e->getMessage()); } } public function read() { try { return $this->encoder->decode( @file_get_contents($this->file)); } catch (Exception $e) { throw new Exception($e->getMessage()); } } }</code>
除了模塊向客戶端代碼公開的一些簡單的細微之處外,它對於說明要點以及以相當有指導意義的方式演示為什麼 DIP 的謂詞實際上比舊的“面向接口編程”範例更廣泛非常有用。它描述並明確規定了依賴項的反轉,因此應該通過不同的機制來實現。 PHP 的命名空間是一種在沒有過多負擔的情況下實現此目的的好方法,儘管像定義結構良好、表達性強的應用程序佈局這樣的傳統方法也可以產生相同的結果。
結束語
通常情況下,基於主觀專業知識的觀點往往帶有偏見,當然,我在本文開頭表達的觀點也不例外。然而,確實存在一種輕微的傾向,即為了其更複雜的 SOLID 對應物而忽略依賴倒置原則,因為它很容易誤解為依賴抽象的同義詞。此外,一些程序員傾向於直覺地反應,並將術語“反轉”視為控制反轉的縮寫表達式,雖然兩者彼此相關,但這最終是一個錯誤的概念。既然您知道了 DIP 的真正內涵,請務必利用它帶來的所有好處,這肯定會使您的應用程序不易受到隨著時間的推移而可能出現的脆弱性和僵硬性問題的影響。 圖片來自 kentoh/Shutterstock
關於依賴倒置原則的常見問題
依賴倒置原則 (DIP) 是面向對象編程中 SOLID 原則的一個關鍵方面。其主要目的是解耦軟件模塊。這意味著提供複雜邏輯的高層模塊與提供基本操作的低層模塊分離。通過這樣做,對低層模塊的更改將對高層模塊的影響最小,從而使整個系統更易於管理和維護。
傳統的程序化編程通常涉及高層模塊依賴於低層模塊。這可能導致一個僵化的系統,其中一個模塊的更改會對其他模塊產生重大影響。另一方面,DIP 反轉了這種依賴關係。高層和低層模塊都依賴於抽象,這促進了靈活性,並使系統更能適應變化。
當然,讓我們考慮一個從文件讀取數據並處理數據的簡單程序示例。在傳統方法中,處理模塊可能直接依賴於文件讀取模塊。但是,使用 DIP,這兩個模塊都將依賴於一個抽象,例如“DataReader”接口。這意味著處理模塊沒有直接綁定到文件讀取模塊,我們可以輕鬆切換到不同的數據源(例如數據庫或 Web 服務),而無需更改處理模塊。
DIP 可以為您的代碼帶來多項好處。它促進了解耦,這使得您的系統更靈活,更容易修改。它還提高了代碼的可測試性,因為依賴關係可以輕鬆地被模擬或存根。此外,它鼓勵良好的設計實踐,例如面向接口編程而不是面向實現編程。
雖然 DIP 具有許多優點,但它也可能引入複雜性,尤其是在大型系統中,抽象的數量可能變得難以管理。它也可能導致編寫更多代碼,因為您需要定義接口並可能創建其他類來實現這些接口。但是,這些挑戰可以通過良好的設計和架構實踐來減輕。
DIP 是 SOLID 首字母縮寫中的最後一個原則,但它與其他原則密切相關。例如,單一職責原則 (SRP) 和開閉原則 (OCP) 都促進了解耦,這是 DIP 的一個關鍵方面。里氏替換原則 (LSP) 和接口隔離原則 (ISP) 都處理抽象,而抽像是 DIP 的核心。
絕對可以。雖然 DIP 通常是在 Java 和其他面向對象語言的上下文中討論的,但該原則本身是與語言無關的。您可以在任何支持抽象的語言中應用 DIP,例如接口或抽像類。
一個好的起點是在您的代碼中查找高層模塊直接依賴於低層模塊的區域。考慮一下您是否可以在這些模塊之間引入抽象來解耦它們。請記住,目標不是消除所有直接依賴關係,而是確保依賴關係是針對抽象的,而不是針對具體的實現。
DIP 主要用於改進代碼的結構和可維護性,而不是其性能。但是,通過使您的代碼更模塊化且更容易理解,它可以幫助您更有效地識別和解決性能瓶頸。
雖然 DIP 的好處在大型複雜系統中往往更明顯,但它在較小的項目中也可能有用。即使在小型代碼庫中,解耦模塊也可以使您的代碼更容易理解、測試和修改。
以上是依賴性反轉原理的詳細內容。更多資訊請關注PHP中文網其他相關文章!