白話設(shè)計(jì)——淺談DIP和IOC


追本溯源,不斷的回顧基礎(chǔ)對(duì)我而言是種不錯(cuò)的方式,每次重新回顧這些點(diǎn)往往收獲很大.以前,受個(gè)人所限,覺(jué)得這些理論毫指導(dǎo)價(jià)值價(jià)值,過(guò)于相信實(shí)踐的的力量,導(dǎo)致自己進(jìn)步緩慢.其實(shí)有些時(shí)候,實(shí)踐更需要站在理論巨人的肩膀,這會(huì)讓我們少走很多的彎路.

當(dāng)然具體因人而異.


開(kāi)發(fā)之困

實(shí)際開(kāi)發(fā)中最常遇到的問(wèn)題是類A直接依賴類B.當(dāng)我們希望將類A修改為依賴類C時(shí),就必須要通過(guò)修改類A來(lái)實(shí)現(xiàn).這種 情況下類A作為高層的業(yè)務(wù)模塊,負(fù)責(zé)復(fù)雜的業(yè)務(wù)模塊,而類B和類C是底層模塊,負(fù)責(zé)基本的原子操作.實(shí)際工程中類A作為業(yè)務(wù)模塊,往往是非常復(fù)雜,如果修改類A可能會(huì)牽一發(fā)而動(dòng)全身,
,進(jìn)而帶來(lái)不必要的業(yè)務(wù)風(fēng)險(xiǎn).

那么這類問(wèn)題該如何應(yīng)該呢?下來(lái)看看大師們提出的原則:依賴倒置.


那什么是依賴倒置呢?

依賴倒置(Dependence Inversion Principle,簡(jiǎn)稱DIP),其定義如下:

High level modules should not depend upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions.

這句話講的是什么呢,其實(shí)就是以下三點(diǎn):

  • 高層模塊不應(yīng)該直接依賴底層模塊,兩者都應(yīng)該依賴抽象.
  • 抽象不應(yīng)依賴細(xì)節(jié).
  • 細(xì)節(jié)應(yīng)該依賴抽象.

看起來(lái)有點(diǎn)玄奧,實(shí)則不然,重點(diǎn)是理解其中談到的高層,底層,抽象和細(xì)節(jié)分別是什么.

高層:通常是系統(tǒng)的負(fù)責(zé)具體業(yè)務(wù)邏輯的,它通常依賴一些更為底層的模塊;從調(diào)用的角度來(lái)說(shuō),高層是調(diào)用者,底層是被調(diào)用者.在最常見(jiàn)的用戶管理系統(tǒng)中,權(quán)限控制邏輯相對(duì)數(shù)據(jù)存儲(chǔ)是高層模塊,而數(shù)據(jù)模塊則是底層模塊,權(quán)限控制模塊調(diào)用數(shù)據(jù)存儲(chǔ)模塊.

現(xiàn)在再來(lái)看"高層模塊不應(yīng)該依賴底層模塊,兩者都應(yīng)該依賴抽象".放在這里就是,權(quán)限控制模塊中不應(yīng)該直接含有數(shù)據(jù)庫(kù)鏈接的對(duì)象,同時(shí)高層和底層都應(yīng)該具有其抽象更高層,這樣的好處在與"越抽象,越高層,變化的可能性越小".如果你了解EIT模型,你會(huì)發(fā)現(xiàn),EIT模型嚴(yán)格遵循了這一點(diǎn).

如果說(shuō)高層和底層是從宏觀業(yè)務(wù)的角度來(lái)看,那么抽象和細(xì)節(jié)則更偏重實(shí)現(xiàn)的角度.當(dāng)然,其實(shí)兩者的存在緊密的聯(lián)系,高層通常意味著抽象,底層則意味著細(xì)節(jié).

抽象:在java中就是抽象類或者接口,兩者無(wú)法直接實(shí)例化,必須通過(guò)其子類.這樣的好處在于一個(gè)抽象(抽象類或者接口)可能存在多種子類,這中一對(duì)多的方式明顯比1對(duì)1更具有選擇性,而更多的選擇性意味這靈活.

細(xì)節(jié):即具體的實(shí)現(xiàn)類,也就是上面提到的子類,可以通過(guò)new關(guān)鍵字直接創(chuàng)建響應(yīng)的實(shí)例.

其實(shí)不難發(fā)現(xiàn),依賴導(dǎo)致的核心就是面向抽象編程(面向接口屬于面向抽象中的一環(huán)).
很多人談到面向接口編程這,覺(jué)得已經(jīng)足夠了,實(shí)際上我們可以更簡(jiǎn)練一點(diǎn):面向抽象編程的就是為了解決對(duì)象之間的直接依賴.

相信你現(xiàn)在應(yīng)該對(duì)DIP有認(rèn)識(shí)了,但是對(duì)于剛接觸的開(kāi)發(fā)者而言,依賴這個(gè)詞看起并不是那么友好.所以,仍然有必要再對(duì)其通俗話.
像小學(xué)生造句一樣,我們利用依賴這個(gè)詞造個(gè)句子:小顏依賴電腦工作.
這個(gè)句子中出現(xiàn)了"小顏","電腦"兩個(gè)對(duì)象,這句話的意思也就是,小顏需要主動(dòng)借助電腦才能工作,換句話說(shuō),電腦影響小顏的工作.

在實(shí)際生活中,我們幾乎每時(shí)每刻的都在依賴其他事物(對(duì)象)幫我們達(dá)成目的,一旦我們所依賴的事物發(fā)生改變(事物消失,內(nèi)部結(jié)構(gòu)變化等等),我們就不得不改變?cè)凶鍪碌姆绞?
在軟件工程中同樣如此,每個(gè)功能的實(shí)現(xiàn),都意味著不同的對(duì)象相互依賴.其中,在某些"壞代碼",你會(huì)發(fā)現(xiàn)幾個(gè)變化頻繁的對(duì)象竟然被直接耦合在一些,這種情況下,后果可想而知.


困難才露尖尖角

到現(xiàn)在,你已經(jīng)重新溫習(xí)了dip原則,也許還在努力嘗試背下來(lái)?現(xiàn)在呢,我希望你忘記上面所說(shuō)的一切,忘記所謂的準(zhǔn)確準(zhǔn)則.

現(xiàn)在我們來(lái)考慮這樣么一種簡(jiǎn)單的需求:男人開(kāi)奧迪.相信你很快用OOD的思想,直接劃分出Man類和Audi類.那么緊接著,你會(huì)寫(xiě)出如下的代碼:

public class Audi {
    public String getName() {
        return "audi";
    }
}

public class Man {
    public void drive(Audi audi) {
        System.out.println("man drive audi");
    }
}

public class Client {

    public static void main(String[] args) {
        Audi audi = new Audi();
        Man man = new Man();
        man.drive(audi);
    }
}

ok,我們很快寫(xiě)完了這段代碼,現(xiàn)在回過(guò)頭來(lái)想想發(fā)生了什么,你又沒(méi)有覺(jué)得在正式開(kāi)始編寫(xiě)Man類之前需要編寫(xiě)Audi類?

現(xiàn)在呢,正當(dāng)你感慨于自己寫(xiě)代碼之快之際,你的客戶說(shuō),女人也可以開(kāi)車啊,不但可以開(kāi)Audi還可以開(kāi)QQ呢.你一想,這還不容易,改改代碼不久很快就解決了?改改代碼當(dāng)然可以解決,但是你剛改完,你的老板卻說(shuō)女人開(kāi)車多不好,還是不讓她開(kāi)了吧.于是,你一邊大罵領(lǐng)導(dǎo)**,一遍含淚改代碼.如果到現(xiàn)在,你還可以忍的話,那下面的需求你可能就有點(diǎn)想發(fā)狂的沖動(dòng)了"男人女人都可以開(kāi)各種各樣的車"?

到現(xiàn)在,相信你也不難發(fā)現(xiàn),讓你發(fā)怒的不是頻繁的需求變動(dòng),而恰恰是不斷的改代碼,大體就是:修改-推到-復(fù)制等一系列無(wú)意義的修改操作,換句話說(shuō)上面的代碼太過(guò)于脆弱了,那么脆弱的原因是什么呢,恰恰就是Man和Audi直接耦合性太高了,Man中需要Audi,所以你就在Man中粗暴的new了一個(gè)Audi的對(duì)象,不是嗎?

上面僅僅是一個(gè)簡(jiǎn)單的demo,在實(shí)際工程中往往是數(shù)萬(wàn)行的模塊,這時(shí)候,修改的代碼的工程量和風(fēng)險(xiǎn)可就不像現(xiàn)在這么簡(jiǎn)單了.

早有方案立上頭

好吧,為了解決這個(gè)問(wèn)題該怎么辦呢?我們來(lái)嘗試尋求一種"以不變應(yīng)萬(wàn)變"的方案.那么在上面的代碼中"變"發(fā)生的地方在哪里呢?不難發(fā)現(xiàn)恰好是Man和Audi,那解決方案就很明顯了,將Man和Audi做成不變的,很快,你就想到了接口.讓我們?cè)囋嚹懿荒芙鉀Q:

public interface Human {
    void drive(Car car);
}

public interface Car {
    String getName();
}

public class Audi implements Car{
    @Override
    public String getName() {
        return "audi";
    }
}

public class Man implements Human {
    @Override
    public void drive(Car car) {
        System.out.println("man drive " + car.getName());
    }
}

public class Client {

    public static void main(String[] args) {
        Human human = new Man();
        Car car = new Audi();
        human.drive(car);
    }
}

一番折磨之后,我們將Man抽象為不變的Human,Audi抽象為不變的Car,通過(guò)Human和Car這兩個(gè)抽象產(chǎn)生依賴來(lái)避免Man和Audi直接產(chǎn)生依賴.

現(xiàn)在再來(lái)想想我們上面提到的依賴倒置原則:

Human是高層模塊Man的抽象,Car是底層模塊Audi的抽象.不難發(fā)現(xiàn)高層的Man并不直接依賴底層的Audi,其兩者都有各自的抽象,這就是上文提到的"高層不應(yīng)該直接依賴底層,雙方應(yīng)該依賴抽象"

另外,Human和Car的子類的增刪改,并不會(huì)影響Human和Car,這也就是所謂的抽象不依賴細(xì)節(jié).

最后,Human和Car的子類必須實(shí)現(xiàn)其接口所定義的方法,這就是所謂的細(xì)節(jié)的實(shí)現(xiàn)依賴抽象.

到現(xiàn)在為止,相信你對(duì)依賴有了一些理解.實(shí)際上DIP這個(gè)詞在軟件開(kāi)發(fā)早期是沒(méi)有的,后來(lái)大家發(fā)現(xiàn)經(jīng)常面對(duì)這種依賴問(wèn)題,故而將其定義成規(guī)則.那么為什么又加上"倒置"一詞的.

實(shí)際上,不難發(fā)現(xiàn)第一種代碼是按我們常規(guī)思維寫(xiě)出來(lái)的(常規(guī)思維是線性的,人認(rèn)識(shí)事物的本質(zhì)是從具體到抽象),而二種則不然,因此"倒置"一詞其實(shí)反映的是逆常規(guī)思維(從抽象到具體),即:將類與類直接打交道改編為抽象與抽象打交道,也就是所謂的面向抽象(抽象類或接口)編程.


控制反轉(zhuǎn)和依賴注入

何為控制反轉(zhuǎn)

熟悉Spring的開(kāi)發(fā)者很快對(duì)控制反轉(zhuǎn)(IOC)和依賴注入(DI)應(yīng)該非常了解钠龙,在這里我并不準(zhǔn)備多說(shuō).盡管如此布持,仍然簡(jiǎn)單的說(shuō)明下:
控制反轉(zhuǎn)即IOC,它把傳統(tǒng)上由程序來(lái)直接操控的對(duì)象的調(diào)用權(quán)交給外部的容器去控制床三,通過(guò)容器實(shí)現(xiàn)對(duì)象的組件和裝配.說(shuō)白了之景,也就是將對(duì)象的控制權(quán)從程序本身轉(zhuǎn)移到外部容器.這樣說(shuō)起來(lái)還是很抽象,那我們以最簡(jiǎn)單用戶注冊(cè)模塊來(lái)說(shuō)明.
大體的業(yè)務(wù)邏輯是誉察,UserService負(fù)責(zé)用戶信息的校驗(yàn),對(duì)于校驗(yàn)通過(guò)的用戶碌识,通過(guò)UserDao存入數(shù)據(jù)庫(kù).那么代碼大體如下:

public interface UserDao{
    void insert(User user);
}

public class UserDaoImpl implements UserDao{
    @override
    public void insert(User user){
        //存儲(chǔ)的具體實(shí)現(xiàn)
    }
}

public interface Uservice{
    void storeUser(User user);
}

public class UserServiceImpl implments UserService{
    private UserDao userDao;
    
    public UserService(){
        userDao=new UserDaoImpl();
    }
    
    @override
    public void storeUser(User user){
        //用戶信息校驗(yàn)通過(guò)
        userDao.insert(user);
    }
}

不難在上面的代碼中我們已經(jīng)盡可能的遵循DIP原則,但是這樣就夠了么?
你會(huì)發(fā)現(xiàn)在UserServiceImpl中,為了我們通過(guò)new來(lái)創(chuàng)建userDao對(duì)象,即userDao這個(gè)對(duì)象的控制權(quán)實(shí)際在UserServiceImpl中.另外,我們知道凡是在一個(gè)類中用new創(chuàng)建另一種對(duì)象的地方也就意味著耦合,那么有沒(méi)有其他的方式來(lái)解除這個(gè)耦合的?在不引入外部容器的情況下,好像暫無(wú)其他解決方案.既然這樣,我們就引入一個(gè)第三方的容器,該容器存儲(chǔ)了所有注冊(cè)的對(duì)象.在運(yùn)行時(shí),代碼哪里需要某個(gè)對(duì)象,就從容器中取出就好了.

回歸上面的代碼就是UserServiceImpl不再負(fù)責(zé)userDao對(duì)象的創(chuàng)建工作,userDao對(duì)象的創(chuàng)建交給了第三方容器,即userDao對(duì)象的控制權(quán)從UserServiceImpl轉(zhuǎn)移到了第三方容器,這就是控制反轉(zhuǎn)的內(nèi)涵.
不難發(fā)現(xiàn)控制反轉(zhuǎn)實(shí)在DIP原則上的進(jìn)一步升級(jí),旨在解決對(duì)象依賴這問(wèn)題.

控制反轉(zhuǎn)和依賴倒置皆提現(xiàn)了常規(guī)線性思維的轉(zhuǎn)變.
控制意味著誰(shuí)來(lái)控制對(duì)象的創(chuàng)建,即對(duì)象的控制者是誰(shuí)?是程序本身,還是第三方容器?
反轉(zhuǎn)則意味對(duì)象的控制者從程序本身變?yōu)榈谌饺萜?


依賴注入

如果說(shuō)控制反轉(zhuǎn)是一種思維方式的變化,那么依賴注入則是該思想的具體實(shí)踐.
到現(xiàn)在為止,實(shí)現(xiàn)控制反轉(zhuǎn)通常有兩種方式:依賴查找和依賴注入.

  • 依賴查找:容器提供回調(diào)接口和上下文給組件,需要組件自己實(shí)現(xiàn)查找合適的對(duì)象的過(guò)程,此類以EJB為代表.
  • 依賴注入:組件不做任何定位查詢,只需向容器提供普通的java方法,然后容器根據(jù)這些方法自行決定依賴關(guān)系.此類以Spring為代表.

通過(guò)以上定義,不難發(fā)現(xiàn)兩者雖然都實(shí)現(xiàn)了控制反轉(zhuǎn),但是依賴注入重在自動(dòng)注入,更為自動(dòng)和簡(jiǎn)單,基本能夠?qū)崿F(xiàn)對(duì)原有代碼的無(wú)侵入,這也是為什么依賴注入更為流行的原因.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市虱而,隨后出現(xiàn)的幾起案子筏餐,更是在濱河造成了極大的恐慌,老刑警劉巖牡拇,帶你破解...
    沈念sama閱讀 212,294評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件魁瞪,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡惠呼,警方通過(guò)查閱死者的電腦和手機(jī)佩番,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,493評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)罢杉,“玉大人趟畏,你說(shuō)我怎么就攤上這事√沧猓” “怎么了赋秀?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,790評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)律想。 經(jīng)常有香客問(wèn)我猎莲,道長(zhǎng),這世上最難降的妖魔是什么技即? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,595評(píng)論 1 284
  • 正文 為了忘掉前任著洼,我火速辦了婚禮,結(jié)果婚禮上而叼,老公的妹妹穿的比我還像新娘身笤。我一直安慰自己,他們只是感情好葵陵,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,718評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布液荸。 她就那樣靜靜地躺著,像睡著了一般脱篙。 火紅的嫁衣襯著肌膚如雪娇钱。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,906評(píng)論 1 290
  • 那天绊困,我揣著相機(jī)與錄音文搂,去河邊找鬼。 笑死秤朗,一個(gè)胖子當(dāng)著我的面吹牛煤蹭,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 39,053評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼疯兼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了贫途?” 一聲冷哼從身側(cè)響起吧彪,我...
    開(kāi)封第一講書(shū)人閱讀 37,797評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎丢早,沒(méi)想到半個(gè)月后姨裸,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,250評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡怨酝,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,570評(píng)論 2 327
  • 正文 我和宋清朗相戀三年傀缩,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片农猬。...
    茶點(diǎn)故事閱讀 38,711評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡赡艰,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出斤葱,到底是詐尸還是另有隱情慷垮,我是刑警寧澤,帶...
    沈念sama閱讀 34,388評(píng)論 4 332
  • 正文 年R本政府宣布揍堕,位于F島的核電站料身,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏衩茸。R本人自食惡果不足惜芹血,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,018評(píng)論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望楞慈。 院中可真熱鬧幔烛,春花似錦、人聲如沸囊蓝。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,796評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)慎颗。三九已至乡恕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間俯萎,已是汗流浹背傲宜。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,023評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留夫啊,地道東北人函卒。 一個(gè)月前我還...
    沈念sama閱讀 46,461評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像撇眯,于是被迫代替她去往敵國(guó)和親报嵌。 傳聞我的和親對(duì)象是個(gè)殘疾皇子虱咧,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,595評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容