01.21 使用接口是好的設計,還是弊大於利?

這可能是一個不受歡迎的觀點,但是使用面向對象的編程語言中的接口可能弊大於利

使用接口是好的設計,還是弊大於利?

Photo by Clint Patterson on Unsplash

讓我解釋。

接口101

首先,消除歧義的時間:當我在這裡談論接口時,我指的是代碼中的接口定義,而不是用戶界面,用戶體驗或任何圖形特性。

簡而言之,接口是一個契約,說一個類將具有某些特性,主要是公共方法和屬性,其他組件可以與之交互。

接口的目的是為編程語言提供某種程度的解耦。

例如,如果您有一個需要讀取一些外部數據的類,則可以讓它接受一個IDomainObjectDataProvider而不是SqlServerDomainObjectDataProvider。

這樣一來,您的課程就不必在乎是與內存中的數據,數據庫中的數據對話還是由某些外部API調用提供的對話。

這是有道理的,這是擁有接口的經典原因。

另一個原因可能是一層中的類沒有引用另一層中定義的類。 從這個意義上講,接口可以提供一定程度的間接性。 我不太喜歡這種推理,但在某些情況下可能是有效的。

接口有什麼問題?

界面本身還不錯,但是它們確實存在一些重大的折衷,而且我不相信開發人員會充分考慮使用它們的折衷。

導航困境

首先,使用IDE很難瀏覽代碼。

如果我處於自己的開發環境中,則大多數人將支持單擊控制鍵或某些鍵盤快捷鍵來導航到類型的定義。

使用具體類型,導航將直接帶您到您最感興趣的方法的實現。使用界面,您將導航到該方法的界面定義。

這聽起來可能並不重要,但是如果您"在流程中"並經過一個過程的思考,這類似於繞過一個拐角並找到堅固的牆,您希望在其中有一扇門。

您必須先了解自己所看到的內容,然後找出您實際使用的具體類型,找到它們,然後找到相關的定義。

這對我們的生產力造成的損失很小,但意義重大,並且這種情況在界面豐富的IDE中經常發生。

通過接口進行混淆

讓我們回到將接口定義為合同的角度,以及前面針對特定類型的數據提供者的接口示例。

沒錯,我們的代碼靈活且與特定的實現脫鉤非常好。 但是,如果我正在查看一個類並看到一個界面,有時可能會失去對運行時細節的跟蹤。

例如,假設我們在應用程序中只有一種類型的IEmalSender。 如果我在瀏覽代碼時看到的只是IEmailSender引用,則可能無法跟蹤生產中實際使用的發件人及其實現的某些細節。

有人可能會認為這是一件好事,我不必理會,而且它們在一定程度上是對的,但問題出在我們從抽象的角度考慮太多,以至於很難看到具體的部署方案 。

架構水泥

我喜歡將接口視為軟件開發中的一種"架構水泥"。

我的意思是,如果我正在做一些重構(在不更改其行為的情況下清理代碼的形式)並且發現我不再需要傳遞某個參數,或者我想使一個異步的方法同步 (反之亦然),或任何數量的細微調整,使此操作變得更加困難。

我不必導航到一個地方,而必須導航到該接口並在那裡進行更改。 如果該接口還有其他實現,我需要找出它們並確保也進行了更改。

這意味著,過去可能微不足道的一項操作現在將我帶離了我的上下文,並且需要額外的努力和思想來執行。 可能不算很多,但足以讓我三思。

此外,如果從未使用過接口的成員,那麼使用代碼分析工具檢測這種情況要比不遵循接口的方法難得多。 這意味著作為接口定義一部分的無效代碼的停留時間更長。

我的意思是,我們在軟件維護期間為接口支付的費用很少。

數量不多,但比您想的要多,而且使用的接口越多,問題就越明顯。

接口隔離原理

我在接口上看到的另一個主要問題是違反了接口隔離原理(ISP)。 ISP是SOLID編程原則的一部分,這是隨著時間的推移生產可維護軟件的五項原則。

具體地說,ISP談論的是在專門任務周圍選擇許多較小的接口,而不是為執行許多常規操作的類設計的較大接口。

當開發人員向現有系統添加接口時,經常會違反該原理。 通常,他們會進入一個類併為所有公共成員提取一個接口,然後用該接口的用法替換該類的用法。

它有點簡單易行,因此阻力最小的路徑導致了諸如IUserRepository之類的大型接口,而不是諸如IUserValidator和IUserCreator之類的較小接口。

這些較大的接口存在許多問題,包括:

· 他們經常演示前面幾節中列出的問題。

· 由於作為接口一部分的成員數量,它們使製作新的實施變得困難。

· 它們往往是該接口的唯一具體實現。

· 它往往會促進不遵守"單一責任原則"(SOLID的另一個承租人)的類。

總而言之,大型接口不是一個好主意,從長期來看,它往往會導致維護方面的麻煩。

繼承與接口

因此,如果我在界面上提出警告,那麼我建議使用哪種方法更好?

通常,當系統在實現上需要一定程度的靈活性時,它們並不需要接口提供的完全靈活性。 通常,他們只需要一個基類,就可以作為依賴項注入或測試的微型合同。

因此,我主張在您考慮添加接口時,應該考慮引入或使用現有的基類是否更合適。

基類可以提供的一些優點:

  • 導航到基類實際上可以導航到相關方法的具體或默認實現。
  • 基類提供一定程度的代碼重用/共享,這是無法通過接口實現的。
  • 基類比接口稍微容易重構。

當然,有一些缺點和折衷考慮:

  • 您的基類中的代碼將出現在任何派生類中,除非被重寫,否則可能會嚴重限制實現。
  • 您不一定總是控制足夠的代碼以使基類成為可行的選擇,否則圖層依賴性使這成為不可能。
  • 如果您的類層次結構中已經在進行繼承,則可能導致過多的"繼承深度"。

因此,就使用基類還是接口而言,這是一個權衡。

總的來說,我喜歡將接口用於非常小的功能,並且傾向於將基類用於配置控制容器的反轉之類的事情。


總結思想

您的喜好將滿足您的需求。 我要問的是,您不會自動假設:"這應該是一個接口"或"這應該是基類",甚至"我不應該將具體的類傳遞給此方法"。

是否針對靈活性,維護,快速開發或其他方面進行優化完全取決於您。

一切都有優點和缺點,軟件工程就是要為您的代碼庫找到合適的組合。


(本文翻譯自Matt Eland的文章《Death by Interfaces》,參考:https://medium.com/better-programming/death-by-interfaces-ec7b35e634c1)


分享到:


相關文章: