使用Microsoft的IoC框架:Unity來對.NET應用進行解耦

FrankYou發表於2016-12-08

1、IoC/DI簡介

IoC 即 Inversion of Control,DI 即 Dependency Injection,前一箇中文含義為控制反轉,後一個譯為依賴注入,可以理解成一種程式設計模式,詳細的說明可參見大牛Martin Fowler的強文 http://martinfowler.com/articles/injection.html,借用Hollywood的名言:Don't call us, we'll call you,意即你呆著別動,到時我會找你。控制反轉的核心是控制權的轉移,從原有的應用程式轉移到框架如IoC容器,從而實現模組間的解耦。

2、Unity是什麼?

Unity是微軟Patterns & Practices團隊所開發的一個輕量級的,並且可擴充套件的依賴注入(Dependency Injection)容器,它支援常用的三種依賴注入方式:構造器注入(Constructor Injection)、屬性注入(Property Injection),以及方法呼叫注入(Method Call Injection)。現在Unity最新的版本的3.5版,可以在微軟的開源站點:https://github.com/unitycontainer/unity下載最新的釋出版本和文件。

它有助於構建鬆耦合的應用程式和為開發者提供以下便利:

  • 簡化物件的建立,特別在分層物件結構和依賴的情形下;
  • 它支援需求的抽象化,這允許開發人員在執行時或在配置檔案中指定依賴,簡化橫切關注點(crosscutting concerns)的管理;
  • 它通過把元件配置推給容器來決定,增加了靈活性;
  • 服務定位能力; 這使客戶端能夠儲存或快取容器;
  • 輕鬆構建鬆耦合結構的程式,從而讓整個程式框架變得清晰和易於維護。

3、如何使用Unity?

下面我們用一個簡單的例子來演示如何使用Ioc框架:Unity。我們的大多數應用程式都是由兩個或是更多的類通過彼此的合作來實現業務邏輯,這使得每個物件都需要獲取與其合作的物件(也就是它所依賴的物件)的引用。如果這個獲取過程要靠自身實現,那麼這將導致程式碼高度耦合並且難以維護和除錯。使用Unity就是要解決這個這個問題。

Martin Fowler在那篇著名的文章《Inversion of Control Containers and the Dependency Injection pattern》中將具體依賴注入劃分為三種形式,即構造器注入、屬性(設定)注入和介面注入,習慣將其劃分為一種(型別)匹配和三種注入:

  • 型別匹配(Type Matching):雖然我們通過介面(或者抽象類)來進行服務呼叫,但是服務本身還是實現在某個具體的服務型別中,這就需要某個型別序號產生器制來解決服務介面和服務實現型別之間的匹配關係
  • 構造器注入(Constructor Injection):IoC容器會智慧選擇選擇和呼叫適合的建構函式以建立依賴的物件。如果被選擇的建構函式具有相應的引數,IoC容器在呼叫建構函式之前解析註冊的依賴關係並自行獲得相應引數物件;
  • 屬性注入(Property Injection):如果需要使用到被依賴物件的某個屬性,在被依賴物件被建立之後,IoC容器會自動初始化該屬性
  • 方法注入(Method Injection):如果被依賴物件需要呼叫某個方法進行相應的初始化,在該物件建立之後,IoC容器會自動呼叫該方法。

依賴翻轉的核心原則:

1、高層模組不應該依賴底層模組,兩個都應該依賴抽象(抽象類或介面)

2、抽象不應該依賴細節,細節應該依賴抽象。

下面我採用“屬性注入(Property Injection)”的方式演示如何使用Unity框架。

3.1、定義介面類

一個簡單日誌記錄介面類,用於記錄請求的報文。

    public interface ILogger
    {
        Tuple<string, string> GetLogContent(AtomRequest request, AtomResponse response = null);
        void Log(Tuple<string, string> logInfo);
    }

3.2、定義介面的實現類

定義2個介面的實現類,分別用來實現對請求的響應的報文進行日誌記錄。

internal class LogRequest : ILogger
{
    public Tuple<string, string> GetLogContent(AtomRequest request, AtomResponse response = null)
    {
        var reqType = request.GetType().Name;
        switch (reqType)
        {
            case "AtomQueryRequest":
            {
                var req = (AtomQueryRequest) request;
                return Tuple.Create(request.CollectionId, string.Format("流水號:{0} 交易型別:{1}", req.TransNo, req.TransType));
            }
            case "AtomSaleRequest":
            {
                var req = (AtomSaleRequest) request;
                return Tuple.Create(req.CollectionId, string.Format("流水號:{0} 訂單號:{1} 金額:{2}", req.TransNo, req.OrderNo, req.LocalAmount));
            }
            default:
            {
                throw new CheckRequestException("無效的交易型別:".Contact(reqType));
            }
        }
    }

    public void Log(Tuple<string, string> logInfo)
    {
        LogManager.InfoRequest(logInfo.Item1, logInfo.Item2);
    }
}

3.3、定義容器並註冊介面及介面實現類之間的對映關係

專案引用:Microsoft.Practices.Unity.dll

internal class ServiceContainer
{
    // 核心容器型別的定義,設定為靜態的,公開的,可為其它任何型別呼叫。
    public static UnityContainer RtpContainer;

    // 初始化容器並在容器中註冊專案程式中所有的依賴關係
    static ServiceContainer()
    {
        RtpContainer = new UnityContainer();

        // 每次呼叫,容器都會生成一個新的物件例項
        RtpContainer.RegisterType<IResponseProcessor, ResponseProcessor>();

        // 註冊為單例,任何時候呼叫都使用同一個物件例項
        RtpContainer.RegisterType<ILogger, LogRequest>("Request", new ContainerControlledLifetimeManager());
        RtpContainer.RegisterType<ILogger, LogResponse>("Response", new ContainerControlledLifetimeManager());
    }
}

 什麼時候註冊為單例,我的個人標準為:實現類,比如:LogRequest類,沒有靜態成員或者靜態成員沒有併發寫的可能都可以用,使用單例可以減少頻繁建立物件可能造成的開銷。在註冊型別時,如果要註冊為單例模式,額外傳入一個:new ContainerControlledLifetimeManager() 引數即可,表示建立物件的生命週期由容器來控制。另外如果一個介面由多個實現類,如上面的LogRequest和LogResponse都實現了ILogger介面。這樣在註冊map對映關係時,需要額外使用一個name引數(比如上面的“Request”,“Response”)來唯一標識map關係。使用XML也可以實現依賴關係的註冊,但我更傾向於使用:約定優於配置(convention over configuration)的原則,也稱作按約定程式設計,是一種軟體設計正規化,旨在減少軟體開發人員需做決定的數量,獲得簡單的好處,而又不失靈活性。

3.4、依賴注入

internal abstract class AbstractRequest
{
    static AbstractRequest()
    {
        JsConfig.EmitCamelCaseNames = true;
        JsConfig.IncludeNullValues = true;
    }

    [Dependency("Request")]
    public ILogger Logger { get; set; }

    [Dependency]
    public IRequestCheck RequestCheck { get; set; }

    protected virtual void CheckRequest(AtomRequest request)
    {
        if (null == request)
        {
            throw new ArgumentNullException("request", "請求實體不能為NULL");
        }
    }
}

上面的程式碼我採用了“屬性注入(Property Injection)”的方式注入了一個Logger屬性,並且Dependency屬性類的name引數為:Request,標識當AbstractRequest的實現類被例項化時,IoC容器自動初始化該Logger屬性為一個LogRequest物件例項。

3.5、使用注入的屬性

internal class ProcessAtomSaleRequest : AbstractRequest, IAtomRequest
{
    [Dependency("AtomSale")]
    public MessageProviderFactory MessageProviderFactory { get; set; }

    public object Execute(AtomRequest request)
    {
        CheckRequest(request);
        
        // 使用AbstractRequest類中注入的屬性Logger,ProcessAtomSaleRequest被例項化時Logger屬性自動被初始化為LogRequest例項物件
        Logger.Log(Logger.GetLogContent(request));
    }
}

3.6、使用容器來解析並建立物件例項

public class AtomSale : AtomTransaction
{
    protected override object ProcessRequest(AtomRequest request)
    {
        return ((IAtomRequest)ServiceContainer.RtpContainer.Resolve(typeof(IAtomRequest), "AtomSale")).Execute(request);
    }
}

 ServiceContainer.RtpContainer就是我們前面定義的靜態的、公共的容器類(型別為:UnityContainer),在第一次被呼叫時初始化。Resolve方法通過指定抽象型別及對應的name屬性來確定唯一對映關係並建立物件,最後執行物件的Execute方法。

 

 總結:

使用IoC框架後,使用相同架構模型的應用,其高層抽象可以完全移植,只須關注實現類的業務細節即可,業務邏輯修改時也只須改動實現的部分,這樣就實現了抽象層和實現層的分離,同樣物件建立職責也做了轉移。系統解耦或鬆耦合也就順理成章了。如果所有程式碼都揉合在一起,任何程式碼的修改都可能對其它程式碼造成影響,如果沒能細緻和全面的迴歸測試,線上故障也難免發生。

 

幫助到您了嗎?

打賞作者(支付寶):

相關文章