Alan Tsai 的學習筆記


學而不思則罔,思而不學則殆,不思不學則“網貸” 為現任微軟最有價值專家 (MVP)、微軟認證講師 (MCT) 、Blogger、Youtuber:記錄軟體開發的點點滴滴 著重於微軟技術、C#、ASP .NET、Azure、DevOps、Docker、AI、Chatbot、Data Science

[iThome 第七屆鐵人賽 02] IoC基本概念介紹

在實際開始進入Asp .net Mvc之前,我們需要先來看一下一個很重要的概念,那就是IoC。

可以說IoC是框架的核心,基本上只要具備一定規模的框架,通常都會使用IoC和DI的搭配,因此我們需要先瞭解它的概念才能夠實際開始我們的框架開發。

什麽是IoC?

IoC是Inversion Of Control(控制翻轉)的縮寫。

在介紹IoC之前,我們先來一段歷史回顧,看看彈性的程式是如何演進。

傳統New Class的方法

我們寫了一支方法,而這一支方法(Write(string text))的功能非常簡單,就是把傳入的參數輸出到console:

class Program
{
    static void Main(string[] args)
    {
        ConsoleOutput output = new ConsoleOutput();
        output.Write("Hello World");
    }
}
 
class ConsoleOutput
{
    public void Write(string text)
    {
        Console.WriteLine(text);
    }
}

開發出來了,一切都很美好,但是有一天有個使用者和你反應:「這個輸出能不能輸出到Text檔案裡面啊?」

身為一個厲害的工程師,這個當然沒有問題,馬上幫他寫出另外一個版本是輸出到Text檔案:

class Program
{
    static void Main(string[] args)
    {
        // ConsoleOutput output = new ConsoleOutput();
        TextOutput output = new TextOutput();
        output.Write("Hello World");
    }
}
 
class TextOutput
{
    public void Write(string text)
    {
        File.WriteAllText("Filex.txt", text);
    }
}
 
// ConsoleOutput 和之前一樣..

到目前為止,其實已經暴露出好的Design需要避免的問題:

loose coupling原則

主要的程式非常依賴(Depend)實際的class,這導致了:

當輸出到不同目的地的時候,開發者需要重新編譯整個程式碼,甚至連呼叫的地方都需要重新修改,當程式碼和邏輯不複雜的時候問題不大,但是當程式變大的時候 這其實是很麻煩的一件事情。這違反了OO裡面常說的loose coupling的原則。

從硬體學到的概念:Interface

爲了避免掉這一種直接的依賴關係,我們像硬體界學了一個概念,也就是Interface(介面)。

我們可以想像,世界上面那麼多的廠商都在做HD,我們怎麼能夠保證廠商 A和廠商 B的HD都能夠在我電腦上面使用呢?靠的就是每一家廠商的HD都會依照開出來的Interface(介面)去做產品。因此,只要你的電腦有SATA(interface),就可以讀的到SATA的HD。

同樣道理用在軟體上面,我管你是如何實作(管你是哪家生產的廠商),只要最後你的結果是有Write(HD能夠和SATA接上)的Class就好。這樣我們就可以彈性話的切換我們想要的實作。

因此,我們的程式碼變成:

interface IWriter
{
    void Write(string text);
}
 
class TextOutput : IWriter
{
    public void Write(string text)
    {
        File.WriteAllText("Filex.txt", text);
    }
}
 
class ConsoleOutput : IWriter
{
    public void Write(string text)
    {
        Console.WriteLine(text);
    }
}

因為我們現在完全把實作功能拆出來了,我們可以動態的讓使用者選擇輸出方式:

static void Main(string[] args)
{
    IWriter output;
 
    // 使用者選擇要用檔案輸出還是Console輸出
    if (args.Count() > 0 && args[0] == "text")
    {
        output = new TextOutput();
    }
    else
    {
        output = new ConsoleOutput();
    }
 
    output.Write("Hello World");
}

回來看IoC的概念

看完了傳統的、沒有彈性的寫法和用了Interface的寫法對比,應該會感覺到其實這兩個流程是不太一樣:

  • 傳統、沒有彈性的寫法在定義(Declare)參數的瞬間就已經決定了這個參數的具體使用方式。因此和一般的程式flow是一樣的,重上到下
  • 使用interface的寫法,是在實例(也就是new())的時候才決定具體的使用方法。因此它的flow是到要用的時候才會new,所以是依照 後面要什麽來控制的。

這時候我們在想想IoC的意思是控制翻轉,就能夠大概瞭解意思了。傳統的方法屬於重上到下,而interface則是由new()他的類別來決定實作,因此控制的流程翻轉了。(不再是重上到下了)

因此,IoC在框架裡面非常重要,因為透過interface把功能抽離,我們的框架才能夠使用IoC的方式來決定什麽的實作(也就是實際的方法)是我們框架要的。

DI是什麽?

我們從上面IoC看到了,利用interface的方式,可以再使用的時候在決定要使用的實作是那一個。這時候,我們下一個問題是,那決定實作的時刻是如何使用的呢?

這個時候我們又有一個新的名詞,就是Dependency Injection(相依性注入)。

基本上,這邊會介紹的DI只有透過你Constructor和Property的這兩種方式。為了這兩種方法的介紹,我們把程式碼在包一層(之前是直接寫在Main裡面),所以假設我們有一個library class,他依賴一個IWriter的物件。

// 此class 會用到IWriter
public class LibraryWrapper
{
    // ....
}

Constructor injection

這個的意思是透過建構子來注入依賴的物件:

// 此class 會用到IWriter
public class LibraryWrapper
{
    private IWriter _writer;
    // 透過建構子注入相依的物件
    public LibraryWrapper(IWriter inWriter)
    {
        _writer = inWriter;
    }
}
 
.....
// 使用上就是
 
LibraryWrapper output;
 
// 使用者選擇要用檔案輸出還是Console輸出
if (args.Count() > 0 && args[0] == "text")
{
    output = new LibraryWrapper (new TextOutput());
}
else
{
    output = new LibraryWrapper (new ConsoleOutput());
}
...

Property injection

相較於建構子的時候注入,這種類型是透過property來注入:

// 此class 會用到IWriter
public class LibraryWrapper
{
    public IWriter Writer { get; set; }
}
 
.....
// 使用上就是
 
LibraryWrapper output;
 
// 使用者選擇要用檔案輸出還是Console輸出
if (args.Count() > 0 && args[0] == "text")
{
    output = new LibraryWrapper ();
    output.Writer = new TextOutput();
}
else
{
    output = new LibraryWrapper ();
    output.Writer = new ConsoleOutput();
}
...

DI Container

到目前為止,我們介紹了透過interface來讓我們可以做到IoC達到框架來選擇實際的實作是那一個。再來,我們介紹了所謂的DI來決定interface的實作是什麽時候決定。最後這一部份我們要介紹所謂的DI Container。

雖然DI我們可以用手動的方式達到,但是其實還是不彈性。舉例來說,當我要切換實作的時候,還是需要改code並且重新編譯。因此本質上還是沒有彈性的效果。

再來,假設我有一些實作希望是以全域的方式注入。換句話說,就是所有的這個interface的實作,都用同一個instance的話,靠我們手動是沒有辦法簡單做到的。

因此有了所謂DI Container。DI Container會記錄我們所有的interface和實作的對應(可以用xml方式設定,因此不需要重新compile程式就可以改interface的對應實作),並且管理這些實作的instance scope(例如,剛剛提到的,某些實作要以單一全域的方式存在)。

有了DI Container,我們才能真的做到彈性。

DI Container有很多,比較常見的有:

  • Autofac
  • Unity
  • Ninject
  • Structuremap
  • Spring(Java派比較常用)
  • 等等...
這些Container各有優勢,不過本質上都是一樣的作用。

結語

透過這一篇,我希望大家對於Ioc和DI有了一些基本的概念,在下一篇我會介紹Autofac這個DI Container,並且如何開始和我們的Mvc框架整合。

Reference


如果文章對您有幫助,就請我喝杯飲料吧
街口支付QR Code
街口支付QR Code
台灣 Pay QR Code
台灣 Pay QR Code
Line Pay 一卡通 QR Code
Line Pay 一卡通 QR Code
街口支付QR Code
支付寶QR Code
街口支付QR Code
微信支付QR Code
comments powered by Disqus