天天看點

《面向對象的思考過程(原書第4版)》一2.1 清楚接口和實作之間的差別

正如第1章所示,建構健壯的面向對象設計的關鍵之一是了解接口和實作之間的不同。是以,當設計類時,應該向使用者暴露什麼、隐藏什麼是非常重要的。而封裝與生俱來的資料隐藏機制可以對使用者隐藏不必要的資料。

小心不要混淆接口與圖形化使用者接口(graphical user interface,gui)這兩個概念。雖然gui名稱本身包含了接口這個單詞,但是我們所說的接口是一種更通用的術語,它并不局限于圖形化接口。

還記得第1章中的烤面包機例子嗎?烤面包機(或相似功能的裝置)需要插入一個接口,即電源插座,如圖2-1所示。所有需要電的裝置都需要符合正确的接口,即電源插座。烤面包機不需要知道插座的任何實作,或者電是如何産生的。對所有烤面包機而言,它不關心電是燃煤電廠還是核工廠生産的,隻關心具體接口可以正确、安全的工作就行。

《面向對象的思考過程(原書第4版)》一2.1 清楚接口和實作之間的差別

還有一個汽車的例子。司機和汽車之間具有很多接口,比如方向盤、油門踏闆、刹車和點火開關。先抛開美觀問題,大多數人開車時的主要關注點是啟動、加速、停止、轉向等。大部分司機中極少關心那些眼睛看不到的元件(實作)。事實上,大多數人根本就無法識别出某些元件,比如催化器和墊圈。然而,任何司機必須清楚如何使用油門踏闆,因為這是一個通用接口。制造廠商為車安裝一個标準的油門踏闆,確定市場上的目标客戶能夠使用這個系統。

然而,如果制造廠商決定安裝一個操縱杆來代替油門踏闆,大多數司機會不習慣這點,那麼這個車型銷量不會很廣(隻能博得一些喜歡打破正常的人的喜愛)。而如果制造廠商替換了汽車的引擎(改變了部分實作),隻要沒有改變性能和外觀,大多數司機都不會注意到這點。

隻要接口不變,可替換的引擎必須嚴格遵守接口。把四缸發動機替換為八缸發動機可能會改變接口規則,導緻需要使用該引擎接口的其他元件不能正常工作。而在發電廠例子中從交流電(ac)改成直流電(dc)也會影響接口規則。

引擎屬于實作,方向盤屬于接口。改變實作不會對司機造成影響,而改變接口則會。司機會注意到方向盤的外觀變化,即使改變可能很微小。必須強調的是,對引擎的改變不應讓司機注意到,否則就會破壞接口。例如,改變引擎可能會喪失動力,這會引起駕駛者的注意,事實上是改變了接口。

使用者能看到什麼

接口與類直接相關。終端使用者通常看不到任何類,隻會看到gui或者指令行。然而,程式員會接觸類接口。重用類的前提是有人已經寫了一個類。是以,程式員必須知道如何正确使用這個類。程式員需要将很多類組合成一個系統,是以需要了解類的接口。是以,本章中讨論使用者時,我們主要指設計人員和開發人員,沒必要引入終端使用者。是以,當我們在此上下文中讨論接口時,我們在讨論類接口,而不是gui。

正确地設計類時要注意兩部分,即接口和實作。

呈現給終端使用者的服務暴露了接口。最佳實踐中,隻呈現給使用者需要的服務。當然,不同的人對使用者需要什麼服務可能持有不同看法。如果你把10個人放到一個屋子讓他們每個人進行獨立設計,你可能會得到10份完全不同的設計,而且這些設計都沒什麼錯。然而,作為一個通用的規則,一個類的接口應該隻包含需要使用者知道的東西。在烤面包機例子中,使用者隻需要知道烤面包機必須插到接口上(這個例子中接口就是電源插座)以及如何操作烤面包機本身。

識别使用者

當設計類時最重要的考慮就是識别類的讀者(或使用者)。

實作細節對于使用者是隐藏的。我們必須時刻牢記關于實作的一個目标,那就是修改實作不需要變動使用者代碼。看起來可能有些困惑,但該目标是設計問題的核心所在。如果對接口的設計是恰當的,那麼即使調整了實作,使用者調用代碼也無需任何改變。請記住,接口包含了調用方法及傳回值的文法。如果沒有改變接口,使用者無需關心是否修改了實作。程式員隻關心使用相同的文法能夠獲得相同的值即可。

我們可以拿手機來舉例。打電話的接口很簡單,我們隻需撥一個号碼或者從位址簿中選取一個聯系人。如果供應商更新了軟體,它不會改變你打電話的方式。無論如何修改實作,打電話的接口始終保持不變。然而,如果我的電話區号變了,供應商也有可能會修改接口。基礎接口變了(比如電話區号變了),需要使用者改變行為。商家希望保持這樣的修改最小化,因為有些使用者不喜歡這種改變,或者不想這樣的麻煩。

再說烤面包機的例子。隻要接口一直是電源插座,具體實作就可能會從一個燃煤電廠切換為核電站,但這不會影響烤面包機。這裡有一個非常重要的規則,即煤電廠和核電廠都必須要遵循接口規格。如果煤電廠提供交流電(ac),而核電廠提供直流電(dc),就會出問題。使用者和實作都必須要遵循接口規格。

我們來建立一個簡單讀取資料庫的類。我們将編寫一些java代碼,這些代碼會從資料庫中擷取記錄。正如之前讨論的一樣,進行任何設計時,識别終端使用者一直是最重要的問題。你可能需要做一些場景分析,一起對終端使用者做一些引導性訪談,然後會列出這個項目的需求。接下來是我們對這個資料庫閱讀器的需求:

必須能打開資料庫的連接配接。

必須能關閉資料庫的連接配接。

必須能将遊标指向資料庫中的第一條記錄。

必須能把遊标指向資料庫中的最後一條記錄。

必須能得到資料庫中的記錄條數。

必須能知道目前資料庫是否仍有記錄(即我們目前是否指向的是最後一條記錄)。

必須能夠根據鍵值把遊标指向特定的記錄。

必須能夠擷取指定鍵值的記錄。

必須能基于目前遊标的位置擷取下一條記錄。

根據以上需求,可以開始嘗試設計一個讀取資料庫的類,為終端使用者設計可能的接口。

在本例中,讀取資料庫的類僅提供給想使用資料庫的程式員。是以接口本質上是程式員想要使用的應用程式程式設計接口(application-programming interface,api)。這些方法其實包裝了資料庫系統暴露的功能。為什麼要這麼做?本章後面會詳細讨論該問題。簡短的回答是我們需要定制資料庫功能。例如,我們必須處理對象進而可以将它們寫入關系型資料庫中。編寫這樣的中間件對于設計和編碼而言可能過于簡單,但這是封裝特性的真實示例。最重要的是,如果我們想替換資料庫引擎,則無需修改大量代碼。

圖2-2展示了一個類圖,表示了databasereader類的潛在接口。

請注意,該類中的方法都是公共方法(請記住,靠近方法名的加号表示該方法是一個公共接口)。而且這裡隻展示了接口,沒有展示任何實作。請花一分鐘來确定這個類圖是否能大體滿足上面列出的項目需求。如果你之後發現該類圖沒有滿足所有需求也沒關系。因為面向對象設計是一個疊代的過程,是以你無須一開始就保證它絕對正确。

公共接口

《面向對象的思考過程(原書第4版)》一2.1 清楚接口和實作之間的差別

請記住,如果一個方法是公共方法,那麼程式員就可以通路它,是以可以認為它是類的接口。請不要混淆術語“接口”與java和.net中的關鍵字interface。稍後的章節會讨論關鍵字interface。

對于我們列出的每個需求,需要有對應的方法來提供對應的功能。現在需要考慮一些問題:

實際使用此類時,作為程式員的你需要了解與其有關的其他事情嗎?

需要知道内部資料庫代碼是如何打開資料庫的嗎??

需要知道内部資料庫代碼如何對應一條具體記錄的實體位置嗎?

需要知道内部資料庫代碼如何确定是否還有剩餘記錄嗎?

回答是都不需要!你不需要知道任何資訊。隻需要關心能擷取到正确的值并且操作沒有出錯。事實上,程式員更喜歡對具體實作再做一層抽象。應用程式将使用你自定義的類來操作資料庫,而這些自定義的類則負責調用相應的資料庫api。

最小接口

在極限情況下,保證最小接口是剛開始不給使用者提供任何公共接口。當然,這樣的類是無用的。然而,這強制使用者主動找你說:“我需要這個功能。”然後你們可以協商。這樣保證你隻在需要的情況下增加接口,絕不要假設使用者需要什麼東西。

建立包裝對象看起來有些小題大做,但編寫這樣的類有很多好處。比如,當今市場上有很多中間件産品使用了包裝對象的技術。考慮把對象映射到關系型資料庫的問題。一些面向對象的資料庫可能非常适合面向對象的應用程式。然而,一個現存的問題是大多數公司有數年的遺留資料存放在關系型資料庫中。如果公司既需要保留關系型資料庫中的資料,又要擁抱面向對象技術,那麼如何處理這個斷層?

首先,可以把所有遺留的關系型資料轉換到一個全新的面向對象的資料庫中。然而,任何遭受過嚴重的(也是長期的)資料轉換之痛的人都知道不能這樣做。這種轉換往往會耗費大量的時間和精力,到頭來系統還是不能正常工作。

其次,可以使用中間件産品把應用程式代碼中的對象平滑地映射到關系型模型中。隻要關系型資料庫依舊盛行,這種方案相比之前就要更好些。有些人可能會認為面向對象的資料庫比關系型資料庫更友善持久化對象。事實上,很多開發系統都能提供這樣平滑轉換的服務。

對象持久化

對象持久化,意思即儲存對象的狀态以便稍後可以恢複和使用,因為沒有持久化的對象在其生命周期之外就會被銷毀掉。例如,對象的狀态可以儲存在資料庫中。

在目前的業務環境下,關系型資料庫和對象建立映射關系是一個非常好的方案。很多公司內建了這樣的中間件技術。比如一個公司擁有一個網站作為前端接口,而資料存放在大型機中,這很常見。

如果建立一個完全面向對象的系統,使用面向對象的資料庫是個可行的選項(也擁有更好的性能)。不過面向對象的資料庫的發展曆程與面向對象語言的發展曆程比起來差遠了。

獨立應用程式

甚至從頭建立一個全新的面向對象的應用程式,也很難完全避免遺留資料。新建立的面向對象的應用程式也不會是獨立的應用程式,因為它很可能需要擷取存儲在關系型資料庫(或者其他的資料儲存設備)中的資料。

讓我們回到資料庫例子中。圖2-2隻展示了該類的公共接口,除此之外别無其他。當完成該類後,它可能會包含更多的方法,當然也會包含一些屬性。不過作為使用該類的程式員無須知道任何私有方法和屬性的相關資訊。你肯定無需了解公共方法中的具體代碼,隻需簡單知道如何與這些接口互動即可。

公共接口的實作代碼會是什麼樣子呢(假設我們使用的是oracle資料庫)?我們來看看open()方法:

《面向對象的思考過程(原書第4版)》一2.1 清楚接口和實作之間的差別

在這個例子中,如果你是程式員,會發現open方法需要string類型作為參數。name代表了需要傳入的資料庫檔案,但我們并不關心它如何映射到一個具體的資料庫。使用接口隻需知道如何使用這個方法,不用關心方法細節,這正是接口的美好之處!

為了迷惑使用者,我們修改資料庫的實作。昨晚我們把oracle資料庫中的全部資料遷移到了一個sqlanywhere資料庫中(我們忍耐了這巨大而漫長的痛苦)。雖然總共花費了好幾個小時,但最終我們做到了。

現在代碼如下所示:

《面向對象的思考過程(原書第4版)》一2.1 清楚接口和實作之間的差別

今天早上竟然沒有聽到任何使用者的抱怨。這是因為雖然改變了實作,但并未改變接口!使用者關心的調用接口仍然是相同的。修改實作代碼可能需要大量的工作(即使隻改了一行代碼,整個子產品都需要重新編譯),但使用了da-tabasereader類的應用程式代碼則無需任何修改。

代碼重新編譯

動态加載的類是在運作時加載的,并不是靜态連結到一個可執行檔案。當使用動态加載的類(比如java和.net)時,無需重新編譯使用者的類。在靜态連結語言(比如c++)中,引入新類需要一個連結。

分離使用者接口與實作,能省卻大量頭痛的事。在圖2-3中,資料庫的具體實作對終端使用者來說是透明的,終端使用者隻能看到接口。

《面向對象的思考過程(原書第4版)》一2.1 清楚接口和實作之間的差別