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