如果要說Dotnet Core中最重要的概念是什麼
我想最基礎也最常用到的就是Dependency Injection(DI,相依性注入)了
在介紹DI之前
有幾個原則必須了解一下
為SOLID中的D,這邊偷懶不特別介紹,可以參考小弟的拙作
Don't Call Us, We'll Call You (不要打電話給我們,我們會主動找你)
以我本人剛畢業時一開始找工作,四處投遞履歷
但公司卻對我不屑一顧,此時的我「依賴」著公司給我一份工作。
但隨著工作幾年後,翅膀硬了,只要坐在位置上,把履歷放上求職網站,
就會有合適的工作送到你的手中,此時是公司依賴著你
(但事實上並沒有,社畜就是社畜,做夢比較實在)
這個例子可能有點模糊,主要想表達的是高階的物件不該主動去依賴另一個低階物件,而是低階的物件要自己找到高階物件並把自己丟進去。
再舉個例子
當你各位去當兵的時候,槍需要自己準備嗎?不用,軍營就會提供 (台中人可能都自己帶)
IOC是一種設計模式,將流程控制重定向到外部處理程序或控制器來提供控制結果,而不是透過控制項來直接得到結果
如果說DIP 是解偶物件之間的依賴
IOC 就是對 物件依賴流程控制的反轉
以常見的web框架而言,將監聽http 請求,解析請求等,包裝進框架,一般使用者並不用主動去處理解析請求,處理http請求的流程,由主動處理轉交給了框架,因此,框架其實也是一種IoC的設計
IoC還有一個特性是你可以自訂框架的步驟
舉例而言有A(監聽Http請求)B(解析請求)C(回應請求)
今天你對框架原生的解析請求不滿意,決定自己造輪子寫一個自訂的解析方法D然後把B拔掉換成C
這個框架依舊能work的好好的
這邊舉個例子,
這是一段
範例的交易程式碼
讀取資料發送Http請求並解析其回應的程式
public class TransactionService
{
public async Task<TransactionResult> CreateTransaction()
{
//// Get Sender Info
ESUNBankRepositoryImplement bankRepository = new ESUNBankRepository();
var bank = bankRepository.GetBank();
//// Set Sender Info
var httpRequestMessage = new HttpRequestMessage();
httpRequestMessage.RequestUri = bank.Url;
httpRequestMessage.Method = HttpMethod.Post;
httpRequestMessage.Content = JsonContent.Create(new { Amount = 100});
httpRequestMessage.Headers.Add("Authorization", bank.Token);
using var httpClient = new HttpClient();
//// Send Request
var response = await httpClient.SendAsync(httpRequestMessage);
//// Handle Response
try
{
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadFromJsonAsync<TransactionResult>();
return result;
}
}
catch (Exception e)
{
Logger.Error(e.Message);
throw;
}
}
}
我們先來定義一下我們所要反轉的 控制流程
其實每個方法或大或小都可以拆成一個個步驟
由這些步驟所組成的就是流程
以上面的建立交易的方法大致上可以拆成4個步驟
那以上面的例子,控制流是這樣子的
可以注意到我們現在有幾個部分依賴了實體,而非抽象
ESUNBankRepositoryImplement bankRepository = new ESUNBankRepositoryImplement();
var httpClient = new HttpClient();
var httpRequestMessage = new HttpRequestMessage();
第二點因為dotnet core有http factory 我們暫時先不處理他
第三點姑且先把HttpRequestMesswage當作資料結構而非物件也先不處理他
第一點明顯違反了DIP
所以我們使用一個介面去隔離她
將ESUNBankRepositoryImplement bankRepository = new ESUNBankRepositoryImplement();
改成IBankRepository bankRepository = new ESUNBankRepositoryImplement();
好的,我們現在依賴介面了.... 並沒有
我們雖然改用介面宣告了,但實際上 還是透過 new BankRepositoryImplement();
來產生實作,因此我們依舊是透過控制項來取的我實際要使用的介面IBankRepository
那麼讓我們來看看如何將控制反轉吧
要實現控制反轉主要有幾種方式,主要的反轉方式都是透過IoC Container 去做處理
Service locator pattern 是透過查註冊表的方式來找到所相依的服務,將對物件的依賴轉移至服務表中
以上面的Sample 為例
我們要透過Service locator pattern
拿到 IBankRepository
public static class ServiceLocator
{
/// <summary>
/// Type 註冊表
/// </summary>
private static readonly Dictionary<Type, Type> _mapping = new();
/// <summary>
/// 實作註冊表
/// </summary>
private static readonly Dictionary<Type, object> _instances = new();
public static void Register<TInterface, TImplementation>(TImplementation instance)
{
if(instance is null)
throw new ArgumentNullException(nameof(instance));
_mapping.Add(typeof(TInterface), typeof(TImplementation));
_instances.Add(typeof(TInterface), instance);
}
public static TInterface Resolve<TInterface>()
{
var type = typeof(TInterface);
//// 檢查是否有註冊
if (!_mapping.ContainsKey(type))
{
throw new Exception($"Type {type} is not registered");
}
return (TInterface) _instances[type];
}
}
先寫一個簡單的註冊表
之後所需要的服務從這個高階模組ServiceLocator
取得
public class Program
{
void Main()
{
ServiceLocator.Register<IBankRepository, ESUNBankRepositoryImplement>(new ESUNBankRepositoryImplement());
}
}
public class TransactionService
{
public async Task<TransactionResult> CreateTransaction()
{
//// Get Sender Info
var bankRepository = ServiceLocator.Resolve<IBankRepository>();
var bank = bankRepository.GetBank();
//// Set Sender Info
var httpRequestMessage = new HttpRequestMessage();
httpRequestMessage.RequestUri = bank.Url;
httpRequestMessage.Method = HttpMethod.Post;
httpRequestMessage.Content = JsonContent.Create(new { Amount = 100});
httpRequestMessage.Headers.Add("Authorization", bank.Token);
using var httpClient = new HttpClient();
//// Send Request
var response = await httpClient.SendAsync(httpRequestMessage);
//// Handle Response
try
{
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadFromJsonAsync<TransactionResult>();
return result;
}
}
catch (Exception e)
{
Logger.Error(e.Message);
throw;
}
}
}
可以看到我們將取得IBankRepository的實作的職責,從TransactionService
轉移到 ServiceLocator
身上了
Service Locator Pattern 實際上被不少人視為一種反模式,所以並沒有這麼推薦使用
本文重點,放後面講
我們上面將送交易的方法拆成了四個步驟
這邊稍微改寫一下原本的送交易方法
將每步驟拆成一個method
public class TransactionService
{
public async Task<TransactionResult> CreateTransaction()
{
//// Get Sender Info
var bank = GetSenderInfo();
//// Set Sender Info
var httpRequestMessage = SetSenderInfo(bank);
//// Send Request
var response = await SendRequest(httpRequestMessage);
//// Handle Response
return await HandleResponse(response);
}
private static async Task<TransactionResult> HandleResponse(HttpResponseMessage response)
{
try
{
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadFromJsonAsync<TransactionResult>();
return result;
}
}
catch (Exception e)
{
Logger.Error(e.Message);
throw;
}
}
private static async Task<HttpResponseMessage> SendRequest(HttpRequestMessage httpRequestMessage)
{
using var httpClient = new HttpClient();
var response = await httpClient.SendAsync(httpRequestMessage);
return response;
}
private static HttpRequestMessage SetSenderInfo(object bank)
{
var httpRequestMessage = new HttpRequestMessage();
httpRequestMessage.RequestUri = bank.Url;
httpRequestMessage.Method = HttpMethod.Post;
httpRequestMessage.Content = JsonContent.Create(new { Amount = 100 });
httpRequestMessage.Headers.Add("Authorization", bank.Token);
return httpRequestMessage;
}
private static object GetSenderInfo()
{
IBankRepository bankRepository = new ESUNBankRepositoryImplement();
var bank = bankRepository.GetBank();
return bank;
}
}
可以看到我們主流程被簡化成4個method
然後我們近一步抽象化
將私有方法改成virtual 或 abstract
public abstract class TransactionService
{
public async Task<TransactionResult> CreateTransaction()
{
//// Get Sender Info
var bank = GetSenderInfo();
//// Set Sender Info
var httpRequestMessage = SetSenderInfo(bank);
//// Send Request
var response = await SendRequest(httpRequestMessage);
//// Handle Response
return await HandleResponse(response);
}
protected virtual async Task<TransactionResult> HandleResponse(HttpResponseMessage response)
{
try
{
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadFromJsonAsync<TransactionResult>();
return result;
}
}
catch (Exception e)
{
Logger.Error(e.Message);
throw;
}
}
protected virtual async Task<HttpResponseMessage> SendRequest(HttpRequestMessage httpRequestMessage)
{
using var httpClient = new HttpClient();
var response = await httpClient.SendAsync(httpRequestMessage);
return response;
}
protected virtual HttpRequestMessage SetSenderInfo(object bank)
{
var httpRequestMessage = new HttpRequestMessage();
httpRequestMessage.RequestUri = bank.Url;
httpRequestMessage.Method = HttpMethod.Post;
httpRequestMessage.Content = JsonContent.Create(new { Amount = 100 });
httpRequestMessage.Headers.Add("Authorization", bank.Token);
return httpRequestMessage;
}
protected abstract Bank GetSenderInfo();
}
我今天如果要對玉山銀行發起交易
void Main(){
TransactionService transactionService = new ESUNTransactionServiceImpl();
transactionService.CreateTransaction();
}
class ESUNTransactionServiceImpl : TransactionService
{
protected override IBankRepository GetSenderInfo()
{
IBankRepository bankRepository = new ESUNBankRepositoryImplement();
var bank = bankRepository.GetBank();
return bank;
}
}
我們將取得SenderInfo的隱藏到了抽象的範本TransactionService
中,細節隱藏在實作中。
流程的控制從ESUNTransactionService
=> TransactionService
且通過實作不同的細節,可以做到不同的自訂流程
舉例而言,我今天想要送台灣銀行的交易
他的Bank的資訊來自File
且要求使用HTTPGET的方式送出且須加密
我們可以這樣做
class TaiwanBankTransactionServiceImpl : TransactionService
{
protected override Bank GetSenderInfo()
{
var taiwanBankSecret = File.ReadAllText("TaiwanBank.json");
return JsonSerializer.Deserialize<Bank>(taiwanBankSecret);
}
protected override HttpRequestMessage SetSenderInfo(object bank)
{
var httpRequestMessage = new HttpRequestMessage();
var uriBuilder = new UriBuilder(bank.Url) ;
var encodeText = Encrypt(JsonSerializer.Serialize(new { Amount = 100 }));
uriBuilder.Query = $"info={encodeText}";
httpRequestMessage.Method = HttpMethod.Get;
httpRequestMessage.RequestUri = uriBuilder.Uri.AbsoluteUri;
httpRequestMessage.Headers.Add("Authorization", bank.Token);
return httpRequestMessage;
}
}
累了,明天再說