開/閉,第二固原理
軟體實體(類別、模組、函數等)應該對擴充開放,但對修改關閉。
第二個堅實的開閉原則為我們提供了軟體在維護過程中良好表現的精確指示。
怎麼做呢?
預計閱讀時間: 7 minuti
第一個 紮實的原則 它告訴我們如何設計軟體:模組、類別和函數,這樣當需要新功能時,我們不應該更改現有程式碼,而是編寫將由現有程式碼使用的新程式碼。這可能看起來很奇怪,特別是對於 Java、C、C++ 或 C# 等語言,它不僅適用於原始程式碼本身,也適用於二進位檔案。我們希望以不需要重新分發現有二進位檔案、可執行檔或 DLL 的方式建構新功能。 OCP 背景 堅硬的。
SRP和OCP互補
我們已經看到 SRP單一職責原則 其中規定模組應該只有一個更改理由。 OCP 和SRP 原則是互補的。程式碼設計如下 SRP原則,也會尊重 OCP 原則。當我們的程式碼只有一個需要更改的原因時,引入一項新功能將為該更改建立一個次要原因。因此 SRP 和 OCP 都會被違反。同樣,如果我們的程式碼只應在其核心功能發生變化時進行更改,並且在添加新功能時應保持不變,從而尊重 OCP,那麼它通常也會尊重 SRP。
這並不意味著SRP總是導致OCP,反之亦然,但是在大多數情況下,如果堅持其中之一,則實現第二個目標非常簡單。
違反實體開閉原則的例子
從純粹的技術角度來看, 堅實的開/閉原則 這很簡單。兩個類別之間的簡單關係(如下所示)違反了 OCP 原則。
User類直接使用Logic類。 如果我們需要以允許我們同時使用當前和新邏輯類的方式來實現第二個Logic類,則需要更改現有的Logic類。 用戶直接依賴於邏輯的實現,我們無法在不影響當前邏輯的情況下提供新邏輯。 當我們談論靜態類型的語言時,User類也很可能需要更改。 如果我們談論的是編譯語言,那麼肯定要對User可執行文件和Logic可執行文件或動態庫都進行重新編譯和交付,最好盡可能避免。
參考上圖,我們可以推斷任何直接使用另一個類別的類別都可能導致違反 堅實的開/關原則.
例子
假設我們要編寫一個類,該類可以通過我們的應用程序提供下載文件進度的“百分比”。 我們將有兩個主要的類,一個Progress和一個File,我想我們想按如下方式使用它們:
function testItCanGetTheProgressOfAFileAsAPercent() {
$file = new File();
$file->length = 200;
$file->sent = 100;
$progress = new Progress($file);
$this->assertEquals(50, $progress->getAsPercent());
}
在此代碼中,我們是Progress用戶。 無論實際文件大小如何,我們都希望獲得一個百分比值。 我們使用File作為信息源。 一個文件的長度以字節為單位,一個名為send的字段表示發送給下載程序的數據量。 我們不在乎如何在應用程序中更新這些值。 我們可以假設有一些神奇的邏輯可以為我們做這件事,因此在測試中我們可以顯式設置它們。
class File {
public $length;
public $sent;
}
File類只是一個包含兩個字段的簡單數據對象。 當然,它還應該包含其他信息和行為,例如文件名,路徑,相對路徑,當前目錄,類型,權限等。
class Progress {
private $file;
function __construct(File $file) {
$this->file = $file;
}
function getAsPercent() {
return $this->file->sent * 100 / $this->file->length;
}
}
Progress 只是一個在其建構函式中接受檔案的類別。為了清楚起見,我們在建構函數參數中指定了變數類型。 Progress 有一個有用的方法,getAsPercent(),它將從 File 中取得提交的值和長度並將它們轉換為百分比。簡單又有效。
這段程式碼看似正確,但它違反了 堅實的開/閉原理.
但為什麼?
如何?
讓我們嘗試更改需求
隨著時間的發展,每個應用程序都將需要新功能。 我們的應用程序的一項新功能可能是允許音樂流式傳輸,而不僅僅是下載文件。 文件的長度以字節為單位,音樂的持續時間以秒為單位。 我們想為監聽器提供進度條,但是我們可以重用上面編寫的類嗎?
不,我們不能。我們的進程與 File 綁定在一起。它只能管理文件訊息,儘管它也可以應用於音樂內容。但要做到這一點,我們需要改變它,我們需要讓 Progress 了解音樂和檔案。如果我們的設計尊重 堅固的開閉原則,我們不需要點擊“文件”或“進度”。我們可以簡單地重新利用現有的進展並將其應用於音樂。
可能的解決方案
動態類型語言具有在運行時管理對像類型的優勢。 這使我們可以從Progress構造函數中刪除typehint,並且代碼將繼續工作。
class Progress {
private $file;
function __construct($file) {
$this->file = $file;
}
function getAsPercent() {
return $this->file->sent * 100 / $this->file->length;
}
}
現在,我們可以在Progress中啟動任何東西。 無論如何,我的意思是任何東西:
class Music {
public $length;
public $sent;
public $artist;
public $album;
public $releaseDate;
function getAlbumCoverFile() {
return 'Images/Covers/' . $this->artist . '/' . $this->album . '.png';
}
}
而且像上面的音樂課將完美地工作。 我們可以使用非常類似於File的測試來輕鬆對其進行測試。
function testItCanGetTheProgressOfAMusicStreamAsAPercent() {
$music = new Music();
$music->length = 200;
$music->sent = 100;
$progress = new Progress($music);
$this->assertEquals(50, $progress->getAsPercent());
}
因此,基本上所有可衡量的內容都可以與Progress類一起使用。 也許我們應該通過更改變量名來用代碼表達它:
class Progress {
private $measurableContent;
function __construct($measurableContent) {
$this->measurableContent = $measurableContent;
}
function getAsPercent() {
return $this->measurableContent->sent * 100 / $this->measurableContent->length;
}
}
當我們將File指定為類型提示時,我們對類可以處理的內容感到樂觀。 這很明顯,如果還有其他事情發生,他告訴了我們一個大錯誤。
U覆蓋基類方法的類,以使派生類不遵守基類合同。
我們不想最終嘗試在不符合我們合同的對像上調用方法或訪問字段。 當我們有類型提示時,合同就是由它指定的。 File類的字段和方法。 現在我們什麼都沒有了,我們可以發送任何東西,即使是字符串也可能導致錯誤。
固體開閉原理應用的最終結果
雖然兩種情況的最終結果是相同的,這意味著程式碼被破壞,但前者產生了一個很好的消息。然而,這是非常模糊的。無法知道變數是什麼(在我們的例子中是字串)以及搜尋了哪些屬性但未找到。調試和修復問題很困難。程式設計師必須打開 Progress 類,閱讀並理解它。在這種情況下,當未明確指定類型提示時,合約由 Progress 的行為定義。這是一份默示合同,只有 Progress 知道。在我們的範例中,它是透過存取 getAsPercent() 方法中的傳送和長度這兩個欄位來定義的。在現實生活中,隱含的契約可能非常複雜,很難透過在課堂上花幾秒鐘的時間來發現。
僅當以下任何其他技巧都無法輕鬆實現,或者如果它們造成嚴重的體系結構更改而不值得付出努力時,才建議使用此解決方案。