[Software Architecture]IoC and DI
2014/5/24 更正
為避免小弟幾年前的文章誤導大家,這邊附上更正觀念後的文章,供大家參考一下:[30天快速上手TDD][Day 5]如何隔離相依性 - 基本的可測試性,我覺得後來寫的這一篇,才比較有表達出 Dependency Injection,相依物件由外部注入(相依物件控制權轉由外部控制)的味道。
前言
IoC的全名是Inversion of control,對岸翻作「控制反轉」,
DI的全名則是Dependency injection,對岸翻作「相依注射」(感謝小朱大幫忙訂正)。
小的向來對一些很難直覺瞭解的term很頭大,啥叫做控制反轉,啥叫做相依注射,整個丈八金剛摸不著頭緒。
這邊就要來舉一些簡單的例子,來解釋到底啥是IoC,啥是DI,這兩個有啥關係,為什麼這樣的方式比較有彈性。
情境
這邊我會盡量用物件導向的方式來說明目的與需求。
假設現在在我的系統上,我想要「備份資料」,但是備份資料的方式有很多種,
我可能用「Floppy」來備份資料,我也可能用「DVD」來備份資料,未來可能還會有N種可能的方式來備份資料。
如果今天,我要使用「Floppy」這個物件,來執行「備份資料」這個方法,
按照傳統的方式,我需要new Floppy物件,再去呼叫Floppy.備份資料()。
請注意,因為為了達到我的目的「備份資料」,我得透過先將「Floppy」物件初始化,再呼叫Floppy.備份資料(),
這樣的方式,備份資料的抽象邏輯,就依賴於「Floppy」這個物件。
我得設計一個類別叫做「Floopy」,並且public一個method叫做「備份資料」給外面使用。
這個就是正常的相依性,IoC,控制反轉,要反轉的就是這樣的相依性。
正常的相依性,有什麼問題呢?一樣可以跑啊,我們使用跟設計物件的方式,不都是這樣嗎?
有,當這個系統隨著科技進步,從原本使用Floopy備份資料,到DVD相當普遍時,
使用者希望可以把系統改成用DVD來備份資料,系統其他邏輯跟設計都不變,只是單純要把備份的裝置從「Floppy」換成「DVD」。
這個時候,按照正常的相依性設計的系統,要修改系統時,通常會
-
把Floppy這個類別的「備份資料」方法,改成使用DVD的邏輯來備份資料。
然後越後面維護的人頭越大,會覺得這是個四不像的類別,邏輯也不通。
最慘的是,這個系統改完了,過一陣子User又想改回來用Floppy,那就要再改一次CODE。
要怎麼解決這問題,如果今天的例子,不是存取裝置與備份,而是需要不同的演算法、商業邏輯呢?
有沒嗅到一點點Strategy Pattern的味道了? -
為了維護邏輯的合理性,咱們新增一個DVD類別,把所有的code從Floppy複製貼過來DVD類別,再修改「備份資料」的method,
接著把系統所有Floopy,用DVD來取代。
這兩種方式,應該是最常看到的,但是維護的工程師一定幹幹叫的,覺得User真煩,
需求一直變更,加上如果這個動作是這系統的核心功能,
那修改的風險就會很高,如果是長期的產品或是長期的大型專案,那現在邪魔歪道的解法,可能未來3年後就會失之毫釐,差之千里。
so,偉大的Martin Fowler(俗稱馬丁花)就提出了一個概念,IoC,
系統架構設計應該以抽象的邏輯概念為主,才能更貼近現實世界,才能更符合domain model,
當Business logic不變時,需求變更、技術變更、DB變更,都應該要把風險和成本壓到最低。
控制反轉就是,我的目的是「備份資料」,我只要input容器,該容器去實做「備份」的method,原本Method相依於物件,
現在相依性就被反轉過來了。我在乎的是「備份」,至於用什麼備份,則看當時的需求,不需要在被那個物件綁住。
現在的例子,「備份資料」就是我們的目的,也就是domain,有沒有可能把存取裝置獨立出來,
讓我未來不管換啥裝置,前端的code都不用改?
對啦,用口訣啦,就是使用Interface,透過不同的實作,就可以呼叫同一個方法。
不管interface背後是哪一個物件,哪一個Device在實作「備份資料」這個方法,對前端來說,
就是使用Interface,這樣後端只要指定現在實作的是哪個物件就可以達到Strategy Pattern 演算法獨立的目的。
至於怎麼抽換到底要用哪個物件,就是透過injection(注入),把容器注入到interface裡,DI(相依注射)就是在做這件事,到底要使用哪一個物件去實做interface。
使用這個interface的前端程式(最常見的是Presentation layer),完全不用管inteface後的商業邏輯(也就是Business Logic Layer),
這樣的方式,才能夠更抽象的去設計與使用Service,
對工程師來說也可以達到同時開發3-layer的架構。
總結,原本每個物件裡面有哪些method,我們都管不著,所以那一些method都相依於那些物件上。
IoC就是反過來看,是看抽象行為,再來決定用那個物件。
注入,就是把物件注給他,注進去容器裡面。
這樣的設計方式,才會抽象。
未來需求變動,邏輯行為不用修改,只是實體修改,只需要注入修改或新增的物件即可,也能符合Strategy Pattern的目的。
假設舊系統的備份,都是new Floppy,call Floppy.Backup(),
現在換成DVD備份,則所有使用Floppy.Backup()的code,都要換成DVD.Backup(),
但是沒法對整個系統全文取代,因為風險太高。
用IoC與DI的設計方式,則只要把原本注入Floppy的部分,改為DVD即可。
interface的名字可能就叫做「備份裝置」或「存取裝置」,未來再有新的device,也只需要改注入的設定和撰寫新的device備份行為。
這邊補充一下Strategy Pattern的Class Diagram:
Play it
這邊我的範例使用Spring.Net來處理IoC、DI與Factory的部分。
怎麼使用Spring.Net可以參考這篇文章:使用Spring.Net輔助切層的專案架構
首先,我們先新增一個interface叫做IDevice。
using System.Collections;
namespace Core.Domain.Interface
{
public interface IDevice
{
string getDeviceInforamtion();
}
}
接著我們新增一個Floopy的類別與DVD的類別,兩個都實作IDevice介面。
Floppy.cs
namespace Core.Domain
{
public class Floppy:Interface.IDevice
{
#region IDevice 成員
public string getDeviceInforamtion()
{
return "I am Floppy";
}
#endregion
}
}
DVD.cs
namespace Core.Domain
{
public class DVD:Interface.IDevice
{
#region IDevice 成員
public string getDeviceInforamtion()
{
return "I am DVD";
}
#endregion
}
}
假設現在的系統是使用Floppy來作備份資料的物件,那麼我們要在Spring的設定檔加上下面這段,也就是當使用Device這個物件時,
我們是用Core.Domain.Floppy這個物件來實作IDevice。
<object id="Device" type="Core.Domain.Floppy, Core">
</object>
接著我們在頁面上,去讀取目前的Device information,如果是Floppy,就會回傳「I am Floppy」,如果是DVD則是「I am DVD」。
我們也在這個範例示範,傳統的方法怎麼使用Floppy呼叫取得資訊的方法。
.aspx
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server">
傳統用法:<asp:Button ID="Button2" runat="server" Text="我是傳統"
onclick="Button2_Click" /><br />
IoC的用法:<asp:Button ID="Button1" runat="server" Text="IoC" onclick="Button1_Click" />
</asp:Content>
.cs
public partial class test : System.Web.UI.Page
{
private Core.Domain.Interface.IDevice myDevice;
protected void Button1_Click(object sender, EventArgs e)
{
myDevice = (Core.Domain.Interface.IDevice)Core.WebUtility.Repository.Domain("Device");
this.Button1.Text = myDevice.getDeviceInforamtion();
}
protected void Button2_Click(object sender, EventArgs e)
{
Core.Domain.Floppy deviceFloppy = new Core.Domain.Floppy();
this.Button2.Text = deviceFloppy.getDeviceInforamtion();
}
}
畫面其實沒啥意義,當我們按了兩個按鈕,雖然兩個設計方式不一樣,但是結果都是一樣的。
就在這個時候,User要改成DVD了,
傳統的方法,需要把所有的
Core.Domain.Floppy deviceFloppy = new Core.Domain.Floppy(); 改成下面這行:
Core.Domain.DVD deviceDVD = new Core.Domain.DVD();
如果你的系統很大,這個改動幅度可能上千支…
用IoC跟DI去實作Strategy Pattern的方式呢?
只需要把config改成
<object id="Device" type="Core.Domain.DVD, Core">
</object>
我們來看頁面的code有什麼差異:
public partial class test : System.Web.UI.Page
{
private Core.Domain.Interface.IDevice myDevice;
protected void Button1_Click(object sender, EventArgs e)
{
myDevice = (Core.Domain.Interface.IDevice)Core.WebUtility.Repository.Domain("Device");
this.Button1.Text = myDevice.getDeviceInforamtion();
}
protected void Button2_Click(object sender, EventArgs e)
{
Core.Domain.DVD deviceDVD = new Core.Domain.DVD();
this.Button2.Text = deviceDVD.getDeviceInforamtion();
}
}
大家可以看到,Button1.Click的程式碼,完全沒有改到。
Button2.Click裡面,卻因為我的getDeviceInforamtion()依賴於Floppy,要改成DVD,就需要把new 物件的程式碼改掉。
這個改動幅度是無法評估的,小系統或許就改一行,大系統就很難說了。
跑出來的結果也是一樣:
一樣的目的,一樣的物件導向,
不同的設計概念,就可以影響系統發展和未來維護的彈性與成本。
Reference
保哥的這篇文章有著相當豐富的Reference,雖然保哥介紹的是Unity Application Block,我這邊使用的是Spring.Net。
但是這樣的觀念,是沒有語言或平台的限制,無關Winform、Webform。
也希望透過這篇文章的介紹,可以達到聖殿祭司老師所說的「大法」,
讓大家對這兩個term不再這麼陌生,或是不知其所以然。
[註]2009/12/7,補充黃忠成老師的文章連結:blog.csdn.net/code6421/archive/2006/09/25/1282139.aspx
blog 與課程更新內容,請前往新站位置:http://tdd.best/