這是Spring中的有特點的一部份。IoC又被翻譯成“控制反轉”,也不知道是誰翻譯得這麼彆扭,感覺很深奧的詞。其實,原理很簡單,用一句通俗的話來說:就是用XML來定義生成的物件。IoC其實是一種設計模式,Spring只是實現了這種設計模式。
這種設計模式是怎麼來的呢?是實踐中逐漸形成的。
第一階段:用普通的無模式來寫Java程式。一般初學者都要經過這個階段。
第二階段:頻繁的開始使用介面,這時,介面一般都會伴隨著使用工廠模式。
第三階段:使用IoC模式。工廠模式還不夠好:(1)因為的類的生成程式碼寫死在程式裡,如果你要換一個子類,就要修改工廠方法。(2)一個介面常常意味著一個生成工廠,會多出很多工廠類。
可以把IoC模式看做是工廠模式的昇華,可以把IoC看作是一個大工廠,只不過這個大工廠裡要生成的物件都是在XML檔案中給出定義的,然後利用Java的“反射”程式設計,根據XML中給出的類名生成相應的物件。從實現來看,IoC是把以前在工廠方法裡寫死的物件生成程式碼,改變為由XML檔案來定義,也就是把工廠和物件生成這兩者獨立分隔開來,目的就是提高靈活性和可維護性。
這裡引用知乎的例子:Spring IoC有什麼好處呢?
要了解控制反轉( Inversion of Control ), 我覺得有必要先了解軟體設計的一個重要思想:依賴倒置原則(Dependency Inversion Principle )。
什麼是依賴倒置原則?假設我們設計一輛汽車:先設計輪子,然後根據輪子大小設計底盤,接著根據底盤設計車身,最後根據車身設計好整個汽車。這裡就出現了一個“依賴”關係:汽車依賴車身,車身依賴底盤,底盤依賴輪子。
這樣的設計看起來沒問題,但是可維護性卻很低。假設設計完工之後,上司卻突然說根據市場需求的變動,要我們把車子的輪子設計都改大一碼。這下我們就蛋疼了:因為我們是根據輪子的尺寸設計的底盤,輪子的尺寸一改,底盤的設計就得修改;同樣因為我們是根據底盤設計的車身,那麼車身也得改,同理汽車設計也得改——整個設計幾乎都得改!
我們現在換一種思路。我們先設計汽車的大概樣子,然後根據汽車的樣子來設計車身,根據車身來設計底盤,最後根據底盤來設計輪子。這時候,依賴關係就倒置過來了:輪子依賴底盤, 底盤依賴車身, 車身依賴汽車。
這時候,上司再說要改動輪子的設計,我們就只需要改動輪子的設計,而不需要動底盤,車身,汽車的設計了。
這就是依賴倒置原則——把原本的高層建築依賴底層建築“倒置”過來,變成底層建築依賴高層建築。高層建築決定需要什麼,底層去實現這樣的需求,但是高層並不用管底層是怎麼實現的。這樣就不會出現前面的“牽一髮動全身”的情況。
控制反轉(Inversion of Control) 就是依賴倒置原則的一種程式碼設計的思路。具體採用的方法就是所謂的依賴注入(Dependency Injection)。其實這些概念初次接觸都會感到雲裡霧裡的。說穿了,這幾種概念的關係大概如下:
為了理解這幾個概念,我們還是用上面汽車的例子。只不過這次換成程式碼。我們先定義四個Class,車,車身,底盤,輪胎。然後初始化這輛車,最後跑這輛車。程式碼結構如下:
這樣,就相當於上面第一個例子,上層建築依賴下層建築——每一個類的建構函式都直接呼叫了底層程式碼的建構函式。假設我們需要改動一下輪胎(Tire)類,把它的尺寸變成動態的,而不是一直都是30。我們需要這樣改:
由於我們修改了輪胎的定義,為了讓整個程式正常執行,我們需要做以下改動:
由此我們可以看到,僅僅是為了修改輪胎的建構函式,這種設計卻需要修改整個上層所有類的建構函式!在軟體工程中,這樣的設計幾乎是不可維護的——在實際工程專案中,有的類可能會是幾千個類的底層,如果每次修改這個類,我們都要修改所有以它作為依賴的類,那軟體的維護成本就太高了。
所以我們需要進行控制反轉(IoC),及上層控制下層,而不是下層控制著上層。我們用依賴注入(Dependency Injection)這種方式來實現控制反轉。所謂依賴注入,就是把底層類作為引數傳入上層類,實現上層類對下層類的“控制”。這裡我們用構造方法傳遞的依賴注入方式重新寫車類的定義:
這裡我們再把輪胎尺寸變成動態的,同樣為了讓整個系統順利執行,我們需要做如下修改:
看到沒?這裡我只需要修改輪胎類就行了,不用修改其他任何上層類。這顯然是更容易維護的程式碼。不僅如此,在實際的工程中,這種設計模式還有利於不同組的協同合作和單元測試:比如開發這四個類的分別是四個不同的組,那麼只要定義好了介面,四個不同的組可以同時進行開發而不相互受限制;而對於單元測試,如果我們要寫Car類的單元測試,就只需要Mock一下Framework類傳入Car就行了,而不用把Framework, Bottom, Tire全部new一遍再來構造Car。
這裡我們是採用的建構函式傳入的方式進行的依賴注入。其實還有另外兩種方法:Setter傳遞和介面傳遞。這裡就不多講了,核心思路都是一樣的,都是為了實現控制反轉。
看到這裡你應該能理解什麼控制反轉和依賴注入了。那什麼是控制反轉容器(IoC Container)呢?其實上面的例子中,對車類進行初始化的那段程式碼發生的地方,就是控制反轉容器。
顯然你也應該觀察到了,因為採用了依賴注入,在初始化的過程中就不可避免的會寫大量的new。這裡IoC容器就解決了這個問題。這個容器可以自動對你的程式碼進行初始化,你只需要維護一個Configuration(可以是xml可以是一段程式碼),而不用每次初始化一輛車都要親手去寫那一大段初始化的程式碼。這是引入IoC Container的第一個好處。
IoC Container的第二個好處是:我們在建立例項的時候不需要了解其中的細節。在上面的例子中,我們自己手動建立一個車instance時候,是從底層往上層new的:
這個過程中,我們需要了解整個Car/Framework/Bottom/Tire類建構函式是怎麼定義的,才能一步一步new/注入。
而IoC Container在進行這個工作的時候是反過來的,它先從最上層開始往下找依賴關係,到達最底層之後再往上一步一步new(有點像深度優先遍歷):
這裡IoC Container可以直接隱藏具體的建立例項的細節,在我們來看它就像一個工廠:
我們就像是工廠的客戶。我們只需要向工廠請求一個Car例項,然後它就給我們按照Config建立了一個Car例項。我們完全不用管這個Car例項是怎麼一步一步被建立出來。
實際專案中,有的Service Class可能是十年前寫的,有幾百個類作為它的底層。假設我們新寫的一個API需要例項化這個Service,我們總不可能回頭去搞清楚這幾百個類的建構函式吧?IoC Container的這個特性就很完美的解決了這類問題——因為這個架構要求你在寫class的時候需要寫相應的Config檔案,所以你要初始化很久以前的Service類的時候,前人都已經寫好了Config檔案,你直接在需要用的地方注入這個Service就可以了。這大大增加了專案的可維護性且降低了開發難度。
這裡只是很粗略的講了一下我自己對IoC和DI的理解。主要的目的是在於最大限度避免晦澀難懂的專業詞彙,用盡量簡潔,通俗,直觀的例子來解釋這些概念。如果讓大家能有一個類似“哦!原來就是這麼個玩意嘛!”的印象,我覺得就OK了。想要深入瞭解的話,可以上網查閱一些更權威的資料。這裡推薦一下 Dependency injection 和 Inversion of Control Containers and the Dependency Injection pattern 這兩篇文章,講的很好很詳細。
IoC最大的好處是什麼?因為把物件生成放在了XML裡定義,所以當我們需要換一個實現子類將會變成很簡單(一般這樣的物件都是現實於某種介面的),只要修改XML就可以了,這樣我們甚至可以實現物件的熱插撥(有點象USB介面和SCIS硬碟了)。
IoC最大的缺點是什麼?(1)生成一個物件的步驟變複雜了(其實上操作上還是挺簡單的),對於不習慣這種方式的人,會覺得有些彆扭和不直觀。(2)物件生成因為是使用反射程式設計,在效率上有些損耗。但相對於IoC提高的維護性和靈活性來說,這點損耗是微不足道的,除非某物件的生成對效率要求特別高。(3)缺少IDE重構操作的支援,如果在Eclipse要對類改名,那麼你還需要去XML檔案裡手工去改了,這似乎是所有XML方式的缺憾所在。
總的來說IoC無論原理和實現都還算是很簡單的。一些人曾認為IoC沒什麼實際作用,這種說法是可以理解的,因為如果你在程式設計中很少使用介面,或很少使用工廠模式,那麼你根本就沒有使用IoC的強烈需要,也不會體會到IoC可貴之處。有些人也說要消除工廠模式、單例模式,但是都語焉不詳、人云亦云。但如果你看到IoC模式和用上Spring,那麼工廠模式和單例模式的確基本上可以不用了。但它消失了嗎?沒有!Spring的IoC實現本身就是一個大工廠,其中也包含了單例物件生成方式,只要用一個設定就可以讓物件生成由普通方式變單一例項方式,非常之簡單。
總結:
(1)IoC原理很簡單,作用的針對性也很強,不要把它看得很玄乎。
(2)要理解IoC,首先要了解“工廠、介面、反射”這些概念。