大家寫程式寫一陣子以後就會開始聽到一些名詞
控制反轉(Inversion of Control,縮寫為IoC)
相依性注入(Dependency Injection,簡稱DI)
也許再加上很久以前也許就沒認真搞懂的介面 (Interface)
關於這些東西到底是什麼意思 我想中文解釋大家都會背
但我可能就是搞不懂 到底把程式搞這麼複雜有什麼好處?
原本的程式也跑得很好 為什麼大家總是說得這樣寫才好?
我想用最簡單的例子來做個說明
假設我們的程式碼分三層
DB → (1).Repository →(2).Service → (3).實際程式
也就是說 (3).實際程式 不能跳過 (2).Service 直接取 (1).Repository
更不可能跳過 (2). Service + (1).Repository 直接碰DB
(1). Repository 實際提供資料的一層
=>不管你是要寫SQL、Stored Procedure、ORM...... 反正你資料怎取出就是在這層
(2). Service 商業邏輯
=> 我隨便假設一個例子 我的真實姓名叫'陳大寶' (存在DB裡) 但因為個資法的關係 我對外只能顯示 '陳**'
(3). 實際程式
=> 這不用解釋吧...
我們的題目如下
DB裡有一張會員基本資料表 裡面存了會員的個資
其中名子的部份 我希望只顯示姓** (兩個星號)
為了講解方便
我把DB、(1). Repository、(2). Service、(3). 實際程式,通通放在同一個檔案,並且通通都設public
實際上他們通常會分散在不同cs檔、或不同專案中、並且不會通通是public,這點請特別注意
我們很快的可以完成第一版的程式碼
using System;
using System.Collections.Generic;
using System.Linq;
namespace MySample.Console
{
public class Sample
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime Birthday { get; set; }
}
/// <summary>
/// 實際存取DB
/// </summary>
public class Repository
{
public List<Sample> GetSample(int param)
{
//為了方便講解 (否則這些資料應該是從 DB 取出)
var source = new List<Sample>
{
new Sample { Id = 1, Name = "陳大寶", Birthday = DateTime.Parse("2001/1/1") },
new Sample { Id = 2, Name = "林小寶", Birthday = DateTime.Parse("2001/2/1") },
new Sample { Id = 3, Name = "王聰明", Birthday = DateTime.Parse("2001/3/1") },
};
return source.Where(q => q.Id == param).ToList();
}
}
/// <summary>
/// 透過Repository從DB取資料後 做一些商業邏輯
/// </summary>
public class Service
{
Repository repo = new Repository();
public List<Sample> GetSample(int param)
{
var samples = repo.GetSample(param);
//假設有一個商業邏輯是大家不能知道會員的全名 只能知道 姓氏 + **
foreach (var item in samples)
{
if (item.Name != null && item.Name.Length >=1 )
{
item.Name = item.Name.Substring(0, 1) + "**";
}
}
return samples;
}
}
/// <summary>
/// DB → Repository → Service → 實際程式
/// </summary>
class Program
{
static void Main(string[] args)
{
Service service = new Service();
var result = service.GetSample(1);
}
}
}
這程式碼很完美的解決了我們的問題
但我們從程式碼中發現
1. 實際程式 相依於 Service ... 第一行我就 new Service()
2. Service 相依於 Repository ... 第一行我就 new Repository()
這程式碼肯定不是什麼控制反轉 也不會是什麼相依性注入 但那又如何 我的程式依舊跑的很好 沒造成困擾
而故事的進行肯定會有些轉折
某天有人反應 你的程式是不是寫錯了?
說好要顯示陳 ** 怎麼昨天從半夜開始 頁面通通變成陳大寶了 !?!?!?!?
oh 那可不得了
秀出完整姓名等於違反個資法 我得馬上測一下這段程式有沒有問題
但你觀察了一下這段程式 你該從何測起?
1. Repository 相依於 DB
=> 這倒簡單 我從config裡 改一改連線參數 指到我的測試DB 再手動塞一些假資料即可
2. Service 相依於 Repository
=> 嗯... 好像暫時想不到該怎麼測... 但沒關係 反正現在問題也不在這裡 並不是輸入'陳大寶' 卻取出 '林小寶' 的資料
3. 實際程式 相依於 Service
=> 這就是出問題的地方!!! 把名子變成**的程式碼就在這!!
雖然暫時還沒辦法想出所有答案 但至少現在我們已經有解決的辦法了
解法如下:
1. 到測試DB做一筆假資料 ( Id=1、Name=陳大寶) 然後把config的連線字串改成測試DB
2. 從實際程式輸入參數 Id=1、跑一次Service.GetSample()
便可知道是哪裡出錯!
但為什麼這樣可以讓測試成立呢?
原來 Repository 相依於 DB
但 Repository 是透過 連線字串 去開DB
故注入 Repository 的物體 其實就是 連線字串(放在config中)
所以當我修改了連線字串 => 傳進Repository裡的東西不一樣了 => 讓正式資料庫 變成 測試資料庫
即 Repository→連線字串 (可人為操作)→DB ...是因為這樣的設計讓我們可以操控 config 進行測試
那如果很不幸的 今天你換到測試機以後 發現輸出結果真的是陳大寶 而非陳**
但該死的 這一個月來有10個PG 進了20個版本 程式碼變成10萬行
public class Repository
{
public List<Sample> GetSample(int param)
{
//多了兩百行程式
//好像又多開 4 張 Table
//我也看不懂他到底在串什麼
//該不會其實是這裡壞掉吧?
return source.Where(q => q.Id == param).ToList();
}
}
public List<Sample> GetSample(int param)
{
//最近上版10次增加了500行!!!!
var samples = repo.GetSample(param);
//為什麼這裡又要去call一些我覺得根本用不到的函式?
foreach (var item in samples)
{
if (item.Name != null && item.Name.Length >=1 )
{
item.Name = item.Name.Substring(0, 1) + "**";
//做這些運算不是沒意義嗎? 為什麼這裡又多了50行???????
}
}
//上次他們好像說有個專案要加新功能 所以這裡的100行是在搞那個功能?????
return samples;
}
雖然我透過切換測試DB 確定程式真的被改壞了
但我只能確定問題不在DB (因為DB是我連到測試環境手動造假 ( Mock ) 的)
那問題到底是Repository最近上版被改壞? 還是Service被改壞?
那我有沒有辦法像剛剛改連線字串一樣 先再排除一個可能?
我可以手動做一個已知的 肯定不會錯的 Repository層 嗎?
就像剛剛我自己手 key 測試 DB 一樣 讓我再排除一個可能的選項嗎?
於是我們來看看第二版的程式碼
先到Nuget下載Moq
改變的部份如下
Repository層
Service層
我的程式
測試程式
完整程式碼如下
using Moq;
using System;
using System.Collections.Generic;
using System.Linq;
namespace MySample.Console
{
public class Sample
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime Birthday { get; set; }
}
public interface IRepository
{
List<Sample> GetSample(int param);
}
/// <summary>
/// 實際存取DB
/// </summary>
public class Repository : IRepository
{
public List<Sample> GetSample(int param)
{
//為了方便講解 (否則這些資料應該是從 DB 取出)
var source = new List<Sample>
{
new Sample { Id = 1, Name = "張小華", Birthday = DateTime.Parse("2001/1/1") },
new Sample { Id = 2, Name = "林小寶", Birthday = DateTime.Parse("2001/2/1") },
new Sample { Id = 3, Name = "王聰明", Birthday = DateTime.Parse("2001/3/1") },
};
return source.Where(q => q.Id == param).ToList();
}
}
/// <summary>
/// 透過Repository從DB取資料後 做一些商業邏輯
/// </summary>
public class Service
{
IRepository repo;
public Service(IRepository pRepo)
{
repo = pRepo;
}
public List<Sample> GetSample(int param)
{
var samples = repo.GetSample(param);
//假設有一個商業邏輯是大家不能知道會員的全名 只能知道 姓氏 + **
foreach (var item in samples)
{
if (item.Name != null && item.Name.Length >=1 )
{
item.Name = item.Name.Substring(0, 1) + "**";
}
}
return samples;
}
}
/// <summary>
/// DB → Repository → Service → 實際程式
/// </summary>
class Program
{
static void Main(string[] args)
{
Service service = new Service(new Repository());
var result = service.GetSample(1);
var test = MyTest();
System.Diagnostics.Debug.WriteLine("Hello World!");
}
/// <summary>
/// 假設這是一個test case
/// </summary>
/// <returns></returns>
public static bool MyTest()
{
//arrange
var source = new List<Sample>
{
new Sample { Id = 1, Name = "陳大寶", Birthday = DateTime.Parse("2001/1/1") },
};
var mockRepo = new Mock<IRepository>();
mockRepo.Setup(repo => repo.GetSample(It.IsAny<int>())).Returns(source);
var service = new Service(mockRepo.Object);
//act
var actual = service.GetSample(1);
//assert
return actual.First().Name == "陳**";
}
}
}
最後我們整理了一下剛剛發生什麼事
連線字串config的發想
A. 若來源是可以被抽換的 ( 正式DB 與 測試DB ) => 控制反轉
B. 透過config的變更 => 相依性注入
C. 因為真假DB都是一模一樣的 所以能達到一行程式都不用改 也能驗證程式對不對
=>正式DB 與 測試DB 的 Table Schema 一模一樣
=>繼承同一介面 IRepo 的 真Repo (原程式) 與 假Repo (Mock) 結構也一模一樣
=>所以程式才會一行都不用改
總結一下
在要測試的程式碼 (Service) 中
會變的變因 (Repository) 都被我控制住了
所以當結果出錯時 ( 輸出 陳大寶 而非 陳** )
我便可確定 Service 有錯
因為 Repository 從頭到尾就根本沒跑 他是一個被我控制住的變因 是我自己造假結果 ( Mock ) 以後傳進去的
從頭到尾就只有Service在變 所以若結果出錯 則 Service 必然有問題
這也就是控制反轉、相依性注入之所以有意義的地方