談談對IOC及DI的理解與思考
阿新 • • 發佈:2021-03-30
# 一、前言
在實際的開發過程中,我們經常會遇到這樣的情況,在進行除錯分析問題的時候,經常需要記錄日誌資訊,這時可以採用輸出到控制檯。
因此,我們通常會定義一個日誌類,來實現輸出日誌。
定義一個生成驗證的邏輯處理方法,
```c#
public class Logger
{
public void AddLogger()
{
Console.WriteLine("日誌新增成功!");
}
}
```
然後在控制檯中輸出結果。
```c#
static void Main(string[] args)
{
Logger logger = new Logger();
logger.AddLogger();
Console.Read();
}
```
![](https://gitee.com/i3yuan/blogimg/raw/master/img/image-20210320162032113.png)
看著實現的結果,我們以為完成任務了,當其實這才是剛剛開始。
# 二、開始
相信大家在開發中,都會遇到這種情況,有時需要控制檯輸出,但也有可能要你輸出到文字,資料庫或者遠端伺服器等等,這些都是有可能。因此最初採用直接輸出到控制檯已經不能滿足條件了,所以我們需要將上述程式碼進行重寫改造,實現不同的輸出方式。
### 2.1 第一種方式
#### 2.1.1 控制檯方式
用到控制檯方式輸出的時候:
```c#
///
/// 控制檯輸出
///
public class ConsoleLogger
{
public void AddLogger()
{
Console.WriteLine("控制檯輸出:日誌新增成功!");
}
}
```
定義一個獲取輸出日誌的處理邏輯類,因此我們需要定義`ConsoleLogger`類並初始化
```c#
///
/// 定義一個輸出日誌的統一類
///
public class LoggerServer
{
private readonly ConsoleLogger consoleLogger = new ConsoleLogger();//新增一個私有變數的物件 個私有變數的數字物件
public void AddLogger()
{
consoleLogger.AddLogger();
}
}
```
控制檯輸出結果:
```c#
static void Main(string[] args)
{
LoggerServer loggerServer = new LoggerServer();
loggerServer.AddLogger();
Console.Read();
}
```
> **控制檯輸出:日誌新增成功!**
>
> ![](https://gitee.com/i3yuan/blogimg/raw/master/img/image-20210320162534525.png)
#### 2.1.2 文字輸出
當用到文字輸出日誌的時候,我們再次定義一個生成文字日誌的方式
```c#
///
/// 文字輸出
///
public class FileLogger
{
public void AddLogger()
{
Console.WriteLine("文字輸出:日誌新增成功!");
}
}
```
然後再次定義一個獲取驗證的處理邏輯類,因此我們需要定義`ImageVerification`類並初始化
```c#
///
/// 定義一個輸出日誌的統一類
///
public class LoggerServer
{
private readonly FileLogger fileLogger = new FileLogger();//新增一個私有變數的物件
public void AddLogger()
{
fileLogger.AddLogger();
}
}
```
最後輸出結果:
> **文字輸出:日誌新增成功!**
通過以上的方式,我們實現了不同方式輸出不同日誌方式,但是仔細觀察可以發現,這種方式不是一種良好的軟體設計方式。
所以可能有人會改成下面第二種方式,以介面來實現。
### 2.2 第二種方式
定義一個`ILogger`介面並宣告一個`AddLogger`方法
```c#
public interface ILogger
{
void AddLogger();
}
```
在控制檯輸出方式中`ConsoleLogger`類,實現`ILogger`介面
```c#
///
/// 控制檯輸出
///
public class ConsoleLogger : ILogger
{
public void AddLogger()
{
Console.WriteLine("控制檯輸出:日誌新增成功!");
}
}
```
在文字輸出日誌方式`FileLogger`類中,實現`ILogger`介面
```c#
///
/// 文字輸出
///
public class FileLogger : ILogger
{
public void AddLogger()
{
Console.WriteLine("文字輸出:日誌新增成功!");
}
}
```
定義一個統一的輸出日誌類`LoggerServer`類
```c#
///
/// 定義一個獲取驗證的統一類
///
public class VerificationServer
{
private readonly ConsoleLogger consoleLogger = new ConsoleLogger();//新增一個私有變數的物件
//private readonly FileLogger fileLogger = new FileLogger();//新增一個私有變數的物件
public void AddLogger()
{
_logger.AddLogger();
}
}
```
最後,控制檯呼叫輸出:
```c#
static void Main(string[] args)
{
LoggerServer loggerServer = new LoggerServer();
loggerServer.AddLogger();
Console.Read();
}
```
> **控制檯輸出:日誌新增成功!**
雖然第二種方式中,採用了介面來實現,降低耦合,但還是沒有達到我們想要的效果,因此以上的兩種方式,都不是很好的軟體設計方式。
程式碼可拓展性比較差,以及元件之間存在高度耦合,違反了開放關閉原則,在設計的時候,也應當考慮**對擴充套件開放,對修改關閉**。
### 2.3 思考
既然要遵循開放關閉原則,那上面的寫法,選擇採用控制檯日誌輸出方式所需要的`ConsoleLogger`建立和依賴都是在統一的日誌類`LoggerServer`內部進行的,既然要使內部不直接存在繫結依賴,那有沒有什麼方式從外部傳遞的方式給`LoggerServer`類內部引用使用呢?
# 三、引入
## 3.1 依賴注入
**依賴注入** : 它提供一種機制,將需要依賴物件的引用傳遞給被依賴物件。
下面我們先看看具體的幾種注入方式,再做小結說明。
### 3.1.1 建構函式注入
在`LoggerServer`類中,定義一個私有變數`_logger`, 然後通過建構函式的方式傳遞依賴
```c#
public class LoggerServer
{
private ILogger _logger; //1. 定義私有變數
//2.建構函式
public LoggerServer(ILogger logger)
{
//3.注入 ,傳遞依賴
this._logger = logger;
}
public void AddLogger()
{
_logger.AddLogger();
}
}
```
通過控制檯程式呼叫,先在外部建立依賴物件,而後通過構造的方式注入依賴
```c#
static void Main(string[] args)
{
#region 建構函式注入
// 注入控制檯輸出方式
// 外部建立依賴的物件 -> ConsoleLogger
ConsoleLogger console = new ConsoleLogger();
// 通過建構函式注入 -> LoggerServer
LoggerServer loggerServer1 = new LoggerServer(console);
loggerServer1.AddLogger();
// 注入 檔案輸出方式
FileLogger file = new FileLogger();
// 通過建構函式注入 -> LoggerServer
LoggerServer loggerServer2 = new LoggerServer(file);
loggerServer2.AddLogger();
#endregion
Console.Read();
}
```
輸出:
> 控制檯輸出:日誌新增成功!
>
> 文字輸出:日誌新增成功!
顯然的發現,通過這種建構函式注入的方式,在外部定義依賴,降低內部的耦合度,同時也增加了擴充套件性,只需從外部修改依賴,就可以實現不同的驗證結果。
### 3.1.2 屬性注入
即通過定義一個屬性來傳遞依賴
```c#
///
/// 定義一個輸出日誌的統一類
///
public class LoggerServer
{
//1.定義一個屬性,可接收外部賦值依賴
public ILogger _logger { get; set; }
public void AddLogger()
{
_logger.AddLogger();
}
}
```
通過控制檯,定義不同的方式,通過不同依賴賦值,實現不同的驗證結果:
```c#
static void Main(string[] args)
{
#region 屬性注入
// 注入 控制檯輸出方式
//外部建立依賴的物件 -> ConsoleLogger
ConsoleLogger console = new ConsoleLogger();
LoggerServer loggerServer1 = new LoggerServer();
//給內部的屬性賦值
loggerServer1._logger = console;
loggerServer1.AddLogger();
// 注入 檔案輸出方式
//外部建立依賴的物件 -> FileLogger
FileLogger file = new FileLogger();
LoggerServer loggerServer2 = new LoggerServer();
//給內部的屬性賦值
loggerServer2._logger = file;
loggerServer2.AddLogger();
#endregion
Console.Read();
}
```
輸出
> 控制檯輸出:日誌新增成功!
>
> 文字輸出:日誌新增成功!
### 3.1.3 介面注入
先定義一個介面,包含一個設定依賴的方法。
```c#
public interface IDependent
{
void SetDepend(ILogger logger);//設定依賴項
}
```
這個與之前的注入方式不一樣,而是通過在類中**繼承並實現這個介面**。
```c#
public class VerificationServer : IDependent
{
private ILogger _logger;
// 繼承介面,並實現依賴項方法,注入依賴
public void SetDepend(ILogger logger)
{
_logger = logger;
}
public void AddLogger()
{
_logger.AddLogger();
}
}
```
通過呼叫,直接通過依賴項方法,傳遞依賴
```c#
static void Main(string[] args)
{
#region 介面注入
// 注入 控制檯輸出方式
//外部建立依賴的物件 -> ConsoleLogger
ConsoleLogger console = new ConsoleLogger();
LoggerServer loggerServer1 = new LoggerServer();
//給內部賦值,通過介面的方式傳遞
loggerServer1.SetDepend(console);
loggerServer1.AddLogger();
//注入 檔案輸出方式
//外部建立依賴的物件 -> FileLogger
FileLogger file = new FileLogger();
LoggerServer loggerServer2 = new LoggerServer();
//給內部賦值,通過介面的方式傳遞
loggerServer2.SetDepend(file);
loggerServer2.AddLogger();
#endregion
Console.Read();
}
```
輸出
> 控制檯輸出:日誌新增成功!
>
> 文字輸出:日誌新增成功!
### 3.1.4 小結
**依賴注入(DI—Dependency Injection)**
**它提供一種機制,將需要依賴物件的引用傳遞給被依賴物件**通過DI,我們可以在`LoggerServer`類在外部`ConsoleLogger`物件的引用傳遞給`LoggerServer`類物件。 注入某個物件所需要的外部資源(包括物件、資源、常量資料)
> 依賴注入把物件的創造交給外部去管理,很好的解決了程式碼緊耦合的問題,是一種讓程式碼實現鬆耦合的機制。
> 鬆耦合讓程式碼更具靈活性,能更好地應對需求變動,以及方便單元測試。
![](https://gitee.com/i3yuan/blogimg/raw/master/img/image-20210320180537120.png)
## 3.2 IOC
**控制反轉**(Inversion of Control,縮寫為**IoC**),在面向物件程式設計中,是一種**軟體設計模式**,教我們如何設計出更優良,更具有鬆耦合的程式。
在上文的例子中,我們發現如果在獲取物件的過程中靠類內部主動建立依賴物件,則會導致程式碼直接高度耦合並且期存在難以維護這種隱患,所以為了避免這種問題,我們採用了由外部提供依賴物件,內部物件類被建立的時候,將其所依賴的物件引用傳遞給它,實現了依賴被注入到物件中去。
> 通俗的說明:
>
> 在類A中用到了類B的物件時候,一般情況下,需要在A的程式碼中顯式的new一個B的物件。這種方式都是通過我們自己主動創建出來的,建立合作物件的主動權在自己手上,自己需要哪個物件,就主動去建立,建立物件的主動權和建立時機是由自己把控的,而這樣就會使得物件間的耦合度高了,A物件需要使用物件B來共同完成一件事,A要使用B,那麼A就對B產生了依賴,也就是A和B之間存在一種耦合關係,並且是緊密耦合在一起。
>
> ```c#
> public class A
> {
> private B b = new B();//主動的new一個B的物件。主動創建出來
> public void Get()
> {
> B.Create();
> }
> }
> ```
>
> 採用依賴注入技術之後,A的程式碼只需要定義一個私有的B物件,不需要直接new來獲得這個物件,而是通過相關的容器控制程式來將B物件在外部new出來並注入到A類裡的引用中。現在建立物件而是有第三方控制建立,你要什麼物件,它就給你什麼物件,依賴關係就變了,原先的依賴關係就沒了,A和B之間耦合度也就減少了。
>
> ```c#
> public class A
> {
> private B b;//外部new出來, 注入到引用中
> public void Get()
> {
> B.Create();
> }
> }
> ```
## 3.3 關係
**控制反轉(IoC)** 是一種軟體設計的模式,指導我們設計出更優良,更具有鬆耦合的程式,
而具體的*實現方式*有**依賴注入**和**依賴查詢**。
在這一篇主要說的是常用的**依賴注入**方式。
你在實際開發中,可能還會聽到另一名詞叫 **IoC容器**,這其實是一個依賴注入的**框架**,
用來對映依賴,管理物件建立和生存週期。 (在後續篇章會具體說明)
# 四、思考
說到依賴,就想到依賴注入和工廠模式這兩者的區別?
這是網上有一個對比例子:
| | 工廠設計模式 | 依賴注入 |
| :-------------- | :-------------------------------------------------------- | :------------------------------------- |
| 物件建立 | 它用於建立物件。我們有單獨的Factory類,其中包含建立邏輯。 | 它負責建立和注入物件。 |
| 物件的狀態 | 它負責建立有狀態物件。 | 負責建立無狀態物件 |
| 執行時/編譯時間 | 在編譯時建立物件 | 在執行時配置物件 |
| 程式碼變更 | 如果業務需求發生變化,則可能會更改物件建立邏輯。 | 無需更改程式碼 |
| 機制 | 類依賴於工廠方法,而工廠方法又依賴於具體類 | 父物件和所有從屬物件可以在單個位置建立 |
好啦,這篇文章就先講述到這裡吧,**在後續篇章中會對常用的IOC容器進行使用說明**,希望對大家有所幫助。
如果有不對的或不理解的地方,希望大家可以多多指正,提出問題,一起討論,不斷學習,共同進