
阿裡妹導讀:好的系統架構離不開好的接口設計,是以,真正懂接口設計的人往往是軟體設計隊伍中的稀缺型人才。
為什麼在接口制定标準中說:一流的企業做标準,二流的企業做品牌,三流的企業做産品?依賴倒置到底是什麼意思?什麼時候使用接口才算合理?今天,阿裡匠人——張建飛将為你詳細解讀。
接口有什麼好處(Why)
在我看來,接口在軟體設計中主要有兩大好處:
1. 制定标準
标準規範的制定離不開接口,制定标準的目的就是為了讓定義和實作分離,而接口作為完全的抽象,是标準制定的不二之選。
這個世界的運轉離不開分工協作,而分工協作的前提就是标準化。試想一下,你家的電腦能允許你把顯示卡從NVIDIA換成七彩虹;你家的燈泡壞了,你可以随便找一個超市買一個新的就可以換上;你把資料從Oracle換成了MySQL,但是你基于JDBC寫的代碼都不用動。等等這些事情的背後都是因為接口,以及基于接口定制的标準化在起作用。
在Java的世界裡,有一個很NB的社群叫JCP( Java Community Process),就是專門通過JSR(Java Specification Request)來制定标準的。正是有了JSR-315(Java Servlet),我們服務端的代碼才能在Tomcat和Jetty之間自由切換。
最後,我想用一句話來總結一下标準的重要性,那就是:“一流的企業做标準,二流的企業做品牌,三流的企業做産品。
2. 提供抽象
除了标準之外,接口還有一個特征就是抽象。正是這樣的抽象,得以讓接口的調用者和實作者可以完全的解耦。
解耦的好處是調用者不需要依賴具體的實作,這樣也就不用關心實作的細節。這樣,不管是實作細節的改動,還是替換新的實作,對于調用者來說都是透明的。
這種擴充性和靈活性,是軟體設計中,最美妙的設計藝術之一。一旦你品嘗過這種“依賴接口”的設計來帶的美好,就不大會再願意回到“依賴實作”的簡單粗暴。平時我們說的“面向接口程式設計原則”和“依賴倒置原則”說的都是這種設計。
另外,一旦你融會貫通的掌握了這個強大的技巧——面向抽象、面向接口,你會發現,雖然面向實作和面向接口在代碼層面的差異不大,但是其背後所隐含的設計思想和設計理念的差異,不亞于我籃球水準和詹姆斯籃球水準之間的差異!
//面向接口
Animal dog = new Dog();
//面向實作
Dog dog = new Dog();
作為一名資深職場老兵,我牆裂建議各位在做系統設計、子產品設計、甚至對象設計的時候。要多考慮考慮更高層次的抽象——也就是接口,而不是一上來就陷入到實作的細節中去。要清楚的意識到接口設計是我們系統設計中的主要工作内容。而這種可以跳出細節内容,站在更高抽象層次上,來看整個系統的子產品設計、子產品劃分、子產品互動的人,正是我們軟體設計隊伍中,非常稀缺的人才。有時候,我們也管這些人叫架構師。
什麼時候要用接口(When)
有擴充性需求的時候
可擴充設計,主要是利用了面向對象的多态特性,是以這裡的接口是一個廣義的概念,如果用程式設計語言的術語來說,它既可以是Interface,也可能是Abstract Class。
這種擴充性的訴求在軟體工作中可以說無處不在,小到一個工具類。例如,我現在系統中需要一個開關的功能,開關的配置目前是用資料庫做配置的,但是後續可能會遷移到Diamond配置中心,或者SwitchCenter上去。
簡單做法就是,我直接用資料庫的配置去實作開關功能,如下圖所示:
但是這樣做的問題很明顯,當需要切換新的配置實作的話,就不得不扒開原來的應用代碼做修改了。更恰當的做法應該是提供一個Switch的接口,讓不同的實作去實作這個接口,進而在切換配置實作的時候,應用代碼不再需要更改了。
如果說,上面的重構隻是使用政策模式對代碼進行了局部優化,做了當然更好,不做的話,影響也還好,可以将就着過。
那麼接下來我要給大家介紹的場景,就不僅僅是“要不要”的問題,而是“不得不”的問題了。
例如,老闆給你布置了一個任務,實作一個類似于eclipse可以可插拔(Pluggable)的産品,此時,使用接口就不僅僅是一個選擇問題了,而是你不得不使用的架構方法了。因為,可插拔的本質就是,你制定一個标準接口(API),然後有不同的實作者去做插件的實作,最後再由PluginManager把這個插件機制串起來而已。
下圖是我當時給ICBU設計的一個企業協同雲的Pluggable架構,其本質上,也就是基于接口的一種标準和擴充的設計。
需要解耦的時候
上面介紹的關于Switch的例子,從表面上來看,是擴充性的訴求。但不可擴充的本質原因正是因為耦合性。當我們通過Switch Interface來解開耦合之後,擴充性的訴求也就迎刃而解了。
發現這種耦合性,對系統的可維護性至關重要。有一些耦合比較明顯(比如Switch的例子)。但更多的耦合是隐式的,并沒有那麼明顯,而且在很長一段時間,它也不是什麼問題,但是,一旦它變成一個問題,将是一個非常頭痛的問題。
一個真實的典型案例,就是java的logger,早些年,大家使用commons-logging、log4j并沒有什麼問題。然而,此處一個隐患正在生長——那就是對logger實作的強耦合。
當logback出來之後,事情開始變得複雜,當我們想替換一個新的logger vendor的時候,為了盡量減少代碼改動,不得不上各種Bridge(橋接),到最後日志代碼變成了誰也看不懂的代碼迷宮。下圖就是我費了九頭二虎之力,才梳理清楚的一個老業務系統的日志架構依賴情況。
試想一下,假如一開始我們就能遇見到這種緊耦合帶來的問題。在應用和日志架構之間加入一層抽象解耦。後續的那麼多橋接,那麼多的向後相容都是可以省掉的麻煩。而我們所要做的事情,實際上也很簡單——就是加一個接口做解耦而已(如下圖所示):
要給外界提供API的時候
上文已經介紹過JCP和JSR了,大家有空可以去閱讀一些JSR的文檔。不管是做的比較成功的JSR-221(JDBC規範)、JSR-315(Servlet規範),還是比較失敗的JSR-94(規則引擎規範)等等。其本質上都是在定義标準、和制定API。其規範的内容都是抽象的,其對外釋出的形式都是接口,它不提供實作,最多會指導實作。
還有就是我們通常使用的各種開放平台的SDK,或者分布式服務中RPC的二方庫,其包含的主要成分也是接口,其實作不在本地,而是在遠端服務提供方。
類似于這種API的情況,都是在倒逼開發者要把接口想清楚。我想,這也算微服務架構一個漂亮的“副作用”吧。當原來單體應用裡的各種耦合的業務子產品,一旦被服務化之後,就自然而然的變成“面向接口”的了。
通過依賴倒置來實作面向接口(How)
關于依賴倒置,我以前寫過不少文章,來闡述它的重要性。實際上,我上面給出的關于擴充需求的Switch案例,關于解耦的logger案例。其背後用來解決問題的方法論都是依賴倒置。
如上圖所示,依賴倒置原則主要規定了兩件事情:
- 高層子產品不應該依賴底層子產品,兩者都應該依賴抽象(如上面的圖2所示)
- 抽象不應該依賴細節,細節應該依賴抽象。
我們回頭看一下,不管是Switch的設計,還是抽象Logger的設計,是不是都在遵循上面的兩條定義内容呢。
實際上,DIP(依賴倒置原則)不光在對象設計,子產品設計的時候有用。在架構設計的時候也非常有用,比如,我在做COLA 1.0的時候,和大多數應用架構分層設計一樣,默許了Domain層可以依賴Infrastructure層。
這種看起來“無傷大雅”的設計,實際上還是存在不小的隐患,也違背了我當初想把業務複雜度和技術複雜度分開的初心,當業務變得更加複雜的時候,這種“偷懶”行為很可能會導緻Domain層堕落成大泥球(Big mud ball)。是以,在COLA 2.0的時候,我決定用DIP來反轉Domain層和Infrastructure層的關系,最終形成如下的結構:
這樣做的好處是Domain層會變得更加純粹,其好處展現在以下三點:
1、解耦: Domain層完全擺脫了對技術細節(以及技術細節帶來的複雜度)的依賴,隻需要安心處理業務邏輯就好了。
2、并行開發: 隻要在Domain和Infrastructure約定好接口,可以有兩個同學并行編寫Domain和Infrastructure的代碼。
3、可測試性: 沒有任何依賴的Domain裡面都是POJO的類,單元測試将會變得非常友善,也非常适合TDD的開發。
什麼時候不需要接口
"勁酒雖好,可不要貪杯哦!"
和許多其它軟體原則一樣,面向接口很好,但也不應該是不分背景、不分場合胡亂使用的殺手锏和尚方寶劍。因為過多的使用接口,過多的引入間接層也會帶來一些不必要的複雜度。
比如,我就看過有些應用的内部子產品設計的過于“靈活”,給什麼DAO、Convertor都加上一層Interface,但實際情況是,應用中對DAO、Convertor的實作進行替換的可能性極低。類似于這樣的,裝模作樣,裝腔作勢的Interface就屬于可有可無的雞骨頭(比雞肋還低一個檔次)。
就像《Effective Java》的作者Joshua Bloch所說:
“同大多數學科一樣,學習程式設計的藝術首先要學會基本的規則,然後才能知道什麼時候可以打破這些規則。”
原文釋出時間為:2019-11-7
作者:從碼農到工匠
本文來自雲栖社群合作夥伴“
阿裡技術”,了解相關資訊可以關注“
”。