控制反轉(IoC) & 相依性注入(DI)

Posted by Ryan Tseng on 2017-09-22

前言

記得幾年以前還在當小小碼農的時候,曾經無數次為了公司趕上線寫過類似這樣的程式

  • Television.cs
1
2
3
4
5
6
7
8
9
10
11
public class Television
{
public void Open() { Console.WriteLine("電視機打開了"); }

public void Close() { Console.WriteLine("電視機關閉了");}

public void ChangeChannel(int channel)
{
Console.WriteLine($"切換至頻道 {channel}");
}
}
  • Person.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Person 
{
private readonly Television _television;
public Person()
{
_television = new Television();
}

public void OpenTv()
{
_television.Open();
}

public void CloseTv()
{
_television.Close();
}

public void ChangeChannel(int channel)
{
_television.ChangeChannel(channel);
}
}

而出社會寫了幾年程式之後,你開始會耳聞 IoC,DI 這樣的字眼出現,但礙於當時程度還不夠,因此沒有辦法對這樣的詞產生什麼感覺,今天就透過這篇文章講解什麼是 IoC,DI,以及我們怎麼樣使用 IoC 的技巧避免程式 寫得太黏

什麼是耦合? (What is coupling)

耦合

以前言所描述的例子來說,就是一個耦合的例子。

搭配著上圖來看,ServiceB 直接在 ClassA 當中建立實體,就跟例子中的 Television 類別直接在 Person 類別中被實體化,並且使用其中的方法。這些都會造成耦合的情況。

如何減少耦合? (How to loose coupling)

既然我們知道所謂的耦合就像是上面這種你泥中有我我泥中有你的狀況,我們應該採取策略來避免這些情形,以下將會介紹兩種解開耦合的方式

  • 服務定位器 Service Locator
  • 依賴注入 Dependency Injection

控制反轉 (Inversion of Control)

控制反轉實際上是一種程式設計的風格,它的目的是為了要降低模組之間的耦合度,而以下的 Service Locator Pattern 以及 Dependency Injection 就是實現 IoC 概念的方式。

服務定位器模式 (Service Locator Pattern)

第一個方法是 服務定位器 模式

服務定位器

由上圖可以知道 ClassA 改使用服務定位器來取得想要得到的實體,他並不需要知道實際上 ServiceAServiceB 是怎麼樣實作或是怎麼樣產生的,顧名思義,Service Locator 的作用就是幫助我們定位到我們要使用的 Service,然後回傳給我他的實體。

以下簡單的實作一個簡易版的 Service Locator

  • ServiceLocator.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static class ServiceLocator
{
private readonly static Dictionary<Type, object> _container = new Dictionary<Type, object>();

public static void Register<T>(object service)
{
if (!_container.ContainsKey(typeof(T)))
_container.Add(typeof(T), service);

_container[typeof(T)] = service;
}

public static T Resolve<T>()
{
T instance = default(T);
if (_container.ContainsKey(typeof(T)))
{
instance = (T)_container[typeof(T)];
}
return instance;
}
}

我們透過 ServiceLocator 的靜態類別,來幫我們做到註冊以及取得實體的動作,大致上是使用到一個 Dictionary 的資料結構,幫助我們可以使用 Key 來取得相應的實體,在這個例子當中,我們就把介面當作 Key,把實作當成 Value,讓我們可以簡單的取得要建立實體的型別,最後直接把字典中相應的實體回傳給使用到他的類別。

  • Person.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Person
{
//private readonly Television _television;
private readonly ITelevision _television;
public Person()
{
//_television = new Television();
_television = ServiceLocator.Resolve<ITelevision>();
}

public void OpenTv()
{
_television.Open();
}

public void CloseTv()
{
_television.Close();
}

public void ChangeChannel(int channel)
{
_television.ChangeChannel(channel);
}
}

然後調整一下我們的 Person 類別,將原本直接耦合的地方透過 ServiceLocator 來幫我們產生所需要的實體,而且這個過程當中並不與實際的 Television 類別有直接的相依,

  • Program.cs
1
2
3
4
5
6
7
8
9
10
void Main()
{
ServiceLocator.Register<ITelevision>(new Television());

Person p = new Person();

p.OpenTv();
p.CloseTv();
p.ChangeChannel(52);
}

最後先在 Program.cs 當中註冊介面以及實作該介面的類別,然後馬上來手動建立 Person 類別的實體,並且呼叫其中的方法看看結果如何。

1
2
3
電視機打開了
電視機關閉了
切換至頻道 52

相依性注入 (Dependency Injection)

上面的作法雖然抽開了 PersonTelevision 的相依關係,但是反而造成 Person 類別直接相依 ServiceLocator,有沒有更漂亮的方式可以解決這個問題呢?

再提出解法之前我們先來看我們期望最後程式會長什麼樣子

  • Person.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Person
{
//private readonly Television _television;
private readonly ITelevision _television;
public Person(ITelevision television)
{
//_television = new Television();
//_television = ServiceLocator.Resolve<ITelevision>();
_television = television;
}

public void OpenTv()
{
_television.Open();
}

public void CloseTv()
{
_television.Close();
}

public void ChangeChannel(int channel)
{
_television.ChangeChannel(channel);
}
}

上面這樣的解法,才能夠真的算得上解開類別的直接相依問題,因為我們透過 DI 框架建構式注入的方式,自動將實體注入給 television 的建構式參數,並且賦值給私有的 _television 欄位,以下就來看 DI 要怎麼做到這件事情。

這邊使用 Autofac 作為範例,其他的 DI 框架概念亦相去不遠,讀者可自行研究其他的 DI 框架

  • Program.cs
1
2
3
4
5
6
7
8
9
10
11
12
void Main()
{
var builder = new ContainerBuilder();
builder.RegisterType<Television>().As<ITelevision>();
builder.RegisterType<Person>().AsSelf();
var container = builder.Build();
var person = container.Resolve<Person>();

person.OpenTv();
person.CloseTv();
person.ChangeChannel(52);
}

來看看這段程式會產生怎樣的結果

1
2
3
電視機打開了
電視機關閉了
切換至頻道 52

由此可知,在 DI 框架的幫忙之下,你根本不需要知道誰實作了 ITelevision 介面,而且你也不會和 ServiceLocator 這類的靜態類別產生相依。

換言之,假設今天你有另外一台電視類別叫做 SmartTelevision,同樣可以開電視、關電視、切換頻道,只是做這些動作的方式不一樣,這時候你可以在 DI 註冊的時候輕易的將 Television 類別改為 SmartTelevision 類別,執行之後就完全得到不一樣的結果,相較於一開始的版本,是不是更具彈性了呢?

參考連結