iT邦幫忙

2022 iThome 鐵人賽

DAY 2
0
Modern Web

擁抱 .Net Core系列 第 2

[Day2] 從好萊塢開始反轉你的人生,談談IoC 與DI - 1

  • 分享至 

  • xImage
  •  

如果要說Dotnet Core中最重要的概念是什麼
我想最基礎也最常用到的就是Dependency Injection(DI,相依性注入)了
在介紹DI之前
有幾個原則必須了解一下

  • Dependency Inversion Principle (DIP,依賴反轉原則)
  • Hollywood Principle (好萊塢法則)
  • Inverse of Control(IoC,控制反轉)

Dependency Inversion Principle (DIP,依賴反轉原則)

為SOLID中的D,這邊偷懶不特別介紹,可以參考小弟的拙作

Hollywood Principle(好萊塢法則)

Don't Call Us, We'll Call You (不要打電話給我們,我們會主動找你)

以我本人剛畢業時一開始找工作,四處投遞履歷
但公司卻對我不屑一顧,此時的我「依賴」著公司給我一份工作。

但隨著工作幾年後,翅膀硬了,只要坐在位置上,把履歷放上求職網站,
就會有合適的工作送到你的手中,此時是公司依賴著你
(但事實上並沒有,社畜就是社畜,做夢比較實在/images/emoticon/emoticon02.gif)

這個例子可能有點模糊,主要想表達的是高階的物件不該主動去依賴另一個低階物件,而是低階的物件要自己找到高階物件並把自己丟進去。

再舉個例子
當你各位去當兵的時候,需要自己準備嗎?不用,軍營就會提供 (台中人可能都自己帶)

Inverse of Control(IoC,控制反轉)

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個步驟

  1. Get Sender Info
  2. Set Sender Info
  3. Send Request
  4. Handle Response

那以上面的例子,控制流是這樣子的

  1. ESUNBankRepositoryImplement 控制了Bank 物件,Bank物件為其控制結果 (Get Sender Info)
  2. Bank 物件控制了 HttpRequestMessage (Set Sender Info)
  3. HttpRequestMessage 跟 HttpClient 則控制了 response (Send Request)
  4. response 控制了 result (Handle Response)

可以注意到我們現在有幾個部分依賴了實體,而非抽象

  1. ESUNBankRepositoryImplement bankRepository = new ESUNBankRepositoryImplement();
  2. var httpClient = new HttpClient();
  3. var httpRequestMessage = new HttpRequestMessage();

第二點因為dotnet core有http factory 我們暫時先不處理他
第三點姑且先把HttpRequestMesswage當作資料結構而非物件也先不處理他

第一點明顯違反了DIP
所以我們使用一個介面去隔離她

ESUNBankRepositoryImplement bankRepository = new ESUNBankRepositoryImplement();
改成
IBankRepository bankRepository = new ESUNBankRepositoryImplement();
好的,我們現在依賴介面了.... 並沒有
我們雖然改用介面宣告了,但實際上 還是透過 new BankRepositoryImplement();
來產生實作,因此我們依舊是透過控制項來取的我實際要使用的介面IBankRepository
那麼讓我們來看看如何將控制反轉吧

如何反轉

要實現控制反轉主要有幾種方式,主要的反轉方式都是透過IoC Container 去做處理

  1. Service locator pattern (服務定位模式)
  2. Dependency injection (相依性注入)
  3. Template method pattern (範本方法)
  4. Strategy pattern/Factory pattern (策略/工廠模式 取決於方法Or物件)

Service locator pattern (服務定位模式)

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 實際上被不少人視為一種反模式,所以並沒有這麼推薦使用

Dependency injection (相依性注入)

本文重點,放後面講

Template method 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;
    }
}

Strategy pattern/Factory pattern (策略/工廠模式 取決於方法Or物件)

累了,明天再說


上一篇
[Day1] 從.Net Framework走向.Net Core
下一篇
[Day3] 工廠與相依性注入,談談IoC 與DI - 2
系列文
擁抱 .Net Core30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言