[Software Architecture]IoC and DI

  • 27162
  • 0
  • 2014-05-24

[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」。
 

這個時候,按照正常的相依性設計的系統,要修改系統時,通常會

  1. 把Floppy這個類別的「備份資料」方法,改成使用DVD的邏輯來備份資料。
    然後越後面維護的人頭越大,會覺得這是個四不像的類別,邏輯也不通。
    最慘的是,這個系統改完了,過一陣子User又想改回來用Floppy,那就要再改一次CODE。
    要怎麼解決這問題,如果今天的例子,不是存取裝置與備份,而是需要不同的演算法、商業邏輯呢?
    有沒嗅到一點點Strategy Pattern的味道了?
  2. 為了維護邏輯的合理性,咱們新增一個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:
2009-10-22_181857

 

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();
    }
}

畫面其實沒啥意義,當我們按了兩個按鈕,雖然兩個設計方式不一樣,但是結果都是一樣的。

Ioc1

就在這個時候,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 物件的程式碼改掉。
這個改動幅度是無法評估的,小系統或許就改一行,大系統就很難說了。

跑出來的結果也是一樣:

Ioc2

一樣的目的,一樣的物件導向,
不同的設計概念,就可以影響系統發展和未來維護的彈性與成本。

 

 

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/