面向物件正規化
面向物件是一種程式設計方式,與電子裝置的演變方式有著驚人的相似之處。本頁將解釋過去的難題,以及這些難題是如何透過元件(在電子裝置中)和程式中的物件解決的。
最早的電子裝置是一個元件網路。如果你看一下老式收音機的內部,你會發現所有東西都是連線在一起的。只有少數東西可以斷開:電源插頭,可能還有外部揚聲器。有時這些裝置還會附帶一個電氣圖,其中詳細地顯示了所有內容。你可以花幾個小時看這樣的圖,發現圖中各種各樣的功能,比如無線電接收、放大、音調控制等等。
最早的計算機程式也具有相同的結構。用匯編語言或 Basic 編寫的程式很容易跨越數百甚至數千行程式碼。它必須這樣做,因為所有細節都必須在一個程式碼檔案中提供。
當然,在那些日子裡,如果開發人員設法用一套不錯的功能編寫了一個能工作的程式,他真的會感到很自豪,但維護起來卻很困難。如果你想要另一個程式,你必須從頭開始編寫。重用之前程式的程式碼並非完全不可能(你可以複製一些子程式),但並不容易。
因此,在電子裝置和程式中,都必須對龐大的結構進行組織。如果你開啟一臺現代的收音機,你會發現它的內部與第一代裝置截然不同。如今你會發現的是元件。這些元件通常看起來像是帶有一個基本功能的小型電路板。因此,你可能有一個前置放大器元件(在立體聲裝置中可能有兩個,甚至在環繞聲系統中更多),音調控制組件,電源元件等等。這些元件很容易更換,因為它們是透過插頭連線的。
通常,一家工廠不會多年生產一種複雜的產品,而是生產一整系列產品,這些產品都是由相同或類似的元件組成的。事實上,工廠通常不自己生產所有元件,而是由其他工廠生產的。所有這些都是因為這些元件具有一個定義的功能,並遵循標準。個人電腦甚至更進一步:它們的設計方式允許使用者隨時對其進行升級。
為了使元件能夠互換,需要一些必要條件。所有與元件的電氣通訊都必須透過插頭進行。這樣的插頭定義了一個廣義功能,例如“聲音輸出”,使連線耳機、揚聲器、錄音系統或任何其他“聲音處理器”成為可能,而不論其內部結構如何。事實上,內部結構只對元件本身很重要,不應該打擾使用這些元件來構建裝置的設計人員。
計算機程式的演變方式與電子裝置相同。可重用的元件被稱為物件,所有通訊都應該透過的插頭被稱為介面。
“介面”這個詞可以有多種含義。許多計算機語言允許你定義一個介面。這樣的定義就是一個最小介面,或者說是連線到它的任何東西都應該支援的最低限度。任何物件都可以有多個這樣的介面,就像任何電子元件都可以有多個插頭一樣。很多插頭都是眾所周知的,並且有文件記錄(聲音插頭、USB 插頭、牆壁插座電源插頭等等),同樣,介面也經常有文件記錄,尤其是在它們與外部庫一起提供的情況下。程式設計中的一件好事是,介面是由命名函式和你可以傳遞給它們的引數定義的。僅僅給函式一個描述性的名稱就可以解釋一個介面的可能性,即使它沒有附帶任何其他文件。
“介面”這個詞有時也用來指代所有公開的函式集,或者所有插頭的完整集合。
執行一個基本功能也會讓人聯想到責任的概念。例如,一個牆壁插座負責提供一定的電壓。如果電壓過高,你連線到它的任何東西都可能損壞。但插座的型別表明了應該提供的功率。一個 220 伏插座與一個 380 伏插座看起來完全不同。有些裝置會檢查電壓,例如用保險絲,但它們不應該這樣做。
同樣,你的物件也可以被認為與外部世界有一個契約:如果你提供給它有效的輸入,它將正確地執行其功能並提供正確的輸出。一些程式語言允許你指定特定函式的需求以及在滿足需求的情況下保證的內容。這些保證的理論被稱為契約式設計。
這種契約在面嚮物件語言中尤其有用,在面嚮物件語言中,任何複雜結構都是由物件處理的。例如,當你將一個路徑傳遞給一個檔案時,你可以宣告檔案必須存在的需求。或者,當傳遞資料庫連線時,一個方法可以要求連線是開啟的,並且事務已啟動。同樣,一個方法可以確保一個檔案存在(在它向其中寫入資料之後)或者資料庫事務已關閉。因此,這種契約有助於將呼叫物件和被呼叫物件之間的責任分開。
但一個物件仍然可能遇到它無法自行解決的問題(例如,網路連線突然斷開)。在這種情況下,一個物件可以將問題升級到周圍的應用程式。這通常是透過異常完成的,這將在後面解釋。
由於一個物件執行一個功能,它也可以單獨測試。你可以構建一個小的測試臺,將各種有效和無效的輸入饋送到物件,並測試它是否產生正確的輸出,以及它是否在應該升級的時候升級。這些測試臺被稱為單元測試。
如果任何物件都可以信賴於做好自己的工作,那麼這樣的物件也可以信賴其他物件做好更細粒度的任務。這意味著,任何有責任的物件都可以將部分責任委託給另一個物件。例如,一個 SettingsStorage 物件可以將設定寫入檔案,但將所有檔案處理委託給一個 TextFile 物件。或者,在另一個例子中,一個 Backend 物件可以指示一個 Database 物件執行一個查詢,並將結果傳送到 ObjectRelationalMapper 物件,以便它可以構建一個很好的物件結構。這可以使程式碼非常清晰,因為 ObjectRelationalMapper 物件不必自己“挖掘”資料。
在一個電子裝置中,所有元件通常都必須在裝置通電時存在。但計算機程式在執行時生成物件。為了能夠做到這一點,許多程式語言允許你定義要建立的物件的型別及其功能。這種物件的“設計”被稱為類。簡單地說:類是物件的藍圖。程式設計師編寫一個類,程式使用它來構建物件。
如果你能夠精確地定義一個物件的責任,你也可以測試它。單元測試是專門測試其他物件責任的物件。單元測試通常在程式的正常執行期間不處於活動狀態,但可以從開發環境中啟用,或者使用某種“自檢”選項啟用。
使用已完成的應用程式測試類的所有細節極其困難。一些測試難以組織(例如斷開的連線、不匹配的許可權、查詢中的語法錯誤等),但您希望瞭解程式的內部機制在所有這些條件下都能可靠地工作。這就是單元測試的用武之地。單元測試測試被檢查物件是否能夠執行其任務,同時也測試它在無法執行任務時是否會升級。單元測試允許程式設計師測試系統中最基本、最底層的物件。但單元測試也讓程式設計師對這些物件充滿信心。當然,完美的程式碼是不存在的,因此總是存在遺漏測試的可能性。因此,單元測試會隨著被測程式碼的演變而演變。
單元測試極其重要的一個領域是程式設計師必須修復一些程式碼時。如果這段程式碼被單元測試覆蓋,程式設計師只需執行測試以檢視修復是否成功,並且沒有產生任何意想不到的副作用(或“錯誤”)。
異常
[edit | edit source]異常通常也是物件,但它們用於傳達問題。如果一個物件無法履行其職責,它會透過構建一個包含問題描述的新物件並將其“丟擲”到周圍程式碼中來升級。周圍程式碼可以“捕獲”這些異常並決定如何處理它。
多型
[edit | edit source]多型為您提供了以不同的抽象級別定義物件的機會。例如,您可以定義一個 WebWidget 類,該類可以輸出網頁的一部分並處理一些提交的輸入。從這個相當抽象的類,您可以派生一個 TextBoxWebWidget,它專門在網頁上繪製一個文字框並檢索一段文字。更抽象的類被稱為 *超類*,更具體的類被稱為 *子類*:在本例中,WebWidget 類是 TextBoxWebWidget 的超類,而 TextBoxWebWidget 類是 WebWidget 類的子類。也可以說 TextBoxWebWidget 類 *繼承* 自 WebWidget 類。
這種多型有一個特殊之處:您可以將變數定義為更抽象的形式或更具體的形式。如果您將變數或引數定義為更抽象的形式,您始終可以傳遞更具體的形式。因此,您可以將各種小部件傳遞給生成完整網頁的程式碼,每個小部件都會執行該特定小部件所需的動作,而不會讓網頁構建程式碼困擾所有差異。
一個超類可以有多個子類,但在大多數面向物件的語言中,一個子類只能從一個超類派生。注意,面向物件理論中沒有任何東西禁止這種“多重繼承”,它只是因為語言設計者發現它對自己或語言使用者來說更方便。
回到組織
[edit | edit source]當您閱讀有關職責和繼承的資訊時,您可能會感覺到面向物件的程式碼就像人一樣組織。的確如此。更準確地說,程式碼就像公司或其他結構化組織一樣組織。
繼承和專業化
[edit | edit source]許多組織都有通用功能和專門功能。例如,任何警察都可以逮捕,但有專門的警察負責解決謀殺案、解決毒品案件、在人群中阻止醉酒的人成為問題,或確保沒有人溺水。類似的結構存在於醫生和其他職業中。在警察和醫生方面,這些人也很容易被識別。因此,您可以將介面或類比作一種制服:每種制服都會傳達佩戴者能做什麼,而制服也帶來了責任。
職責、技能和升級
[edit | edit source]當公司裡的人給倉庫打電話說“請寄一個新的筆記型電腦”時,人們相信倉庫員工有足夠的技能來完成這項工作。但這並不意味著倉庫員工必須親自完成任務。他可以將其分配給同事,或者自己去取筆記型電腦,但讓其他人把它送到請求者那裡。這並不重要:工作完成了,請求者不必知道怎麼做。只有在出現問題時,情況才會升級(例如,當筆記型電腦缺貨時),可能會有從公司外部獲取筆記型電腦或稍後送貨的選項。無論如何,沒有人會指望倉庫員工從砍樹開始,做紙張,然後做筆記型電腦。這絕對 *不是* 他的職責。同樣,顯示資料庫中值的物體通常不負責開啟連線、構建查詢等等。它將這些任務外包給其他物件,或者其他物件將顯示任務外包給我們的物件。
面向物件原則
[span>edit | edit source]在面向物件理論中,出現了一些原則,試圖使維護成為可能,而不會引入錯誤。在實踐中,完美地遵循這些原則非常困難,但它們有助於避免在現有軟體中引入新的錯誤。
開閉原則
[edit | edit source]“軟體應該對擴充套件開放,對修改關閉。”
如果您只能擴充套件軟體,則無法觸及現有部分,您只能在擴充套件中引入錯誤。在實踐中,這是透過多型完成的:您可以編寫一個擴充套件現有類的新的類,儘可能使用所有現有函式,並且只覆蓋新的函式。
重構
[edit | edit source]不是真正的“原則”,而是一種實踐。重構聽起來像是完全無用的東西:重構是在不改變軟體行為的情況下改變軟體。換句話說,重構程式後,您將得到一個與原始程式完全相同的程式。這聽起來可能完全沒用,但事實並非如此。
如果您應用了開閉原則,您就可以放心,您不會在現有軟體中引入新的錯誤,但您仍然在軟體中添加了一些東西。許多這些新增的修復可能看起來很醜陋,並且在維護中是一個負擔。因此,在某個時刻,您將不得不重新組織結構以使其再次變得邏輯和合理。或者相反:您甚至可能無法在不開啟結構進行此類修復的情況下向軟體新增修復。
但是您如何知道行為是否已保留?這就是單元測試的用武之地。單元測試檢查程式碼的行為,因此任何行為變化都應該在單元測試中可見。
請注意,開閉原則和重構實踐相互補充。首先保持結構和現有程式碼完好無損,只新增功能,然後保持功能完好無損,只修改結構,您將有最大機會防止程式碼中出現錯誤。
| 一位維基書作者認為此頁面應該拆分為具有更窄子主題的較小頁面。 您可以透過將此大頁面拆分為更小的頁面來提供幫助。請務必遵循 命名策略。將書籍劃分為更小的部分可以提供更多的重點,並允許每個部分做好一件事,這有利於所有人。 |