[Autofac套件介紹-2] 深入討論Autofac Registering Components

Autofac Registering Components的方式繁多,本文將根據各個不同的註冊方式探討其中的優缺點和適用情境,以及附帶上實作範例。

導讀

前文介紹Autofac整體圖像時,描述了透過Register Type的方式,建立Component和Service之間的關聯。一般來說已能應付大部分的狀況。但除了Register Type的方式之外,還有多種註冊的方式,本文將額外探討下列共4種註冊Component的範例以及適用的情境。

  • 以實體物件方式註冊
  • 以Lambda Expression方式註冊
  • 以Module方式註冊
  • 以掃描Assembly方式註冊

 

以實體物件方式註冊

跟型別方式註冊非常類似,根據上文的例子來比較,如下圖所示,可以看到用物件方式註冊似乎比較麻煩,

var builder = new ContainerBuilder();

//型別方式註冊
builder.RegisterType<YourCar>().As<ICar>();


//物件方式註冊
var car = new YourCar()
builder.RegisterInstance(car).As<ICar>();

但還是有些情境,無法使用型別方式註冊,比如說,希望註冊的物件是來自某個工廠類別。

public class CarFactory
{
    public static ICar CreateCar(string carType)
    {
        ICar car;
        if (carType== "MyCar")
            return  new MyCar();
        if (carType== "YourCar")
            return  new YourCar();
        else
            return null;
    }
}

因為無法確切知道真正回傳的類別為何,所以就必須使用實體方式來註冊。 

//實體類別來自工廠
var yourCar = CarFactory.CreateCar("YourCar");

builder.RegisterInstance(yourCar).As<ICar>();

 

以Lambda Expression方式註冊

使用Lambda Expression來註冊物件,請參考下列範例,範例的前置動作,簡單建立一個IDriver的介面,並且宣告HankDriver實作此介面,提供一個方法叫做SayName()。

public interface IDriver
{
    void SayName();
}

public class HankDriver : IDriver
{
    public void SayName()
    {
        Console.WriteLine("My name is Hank");
    }
}

接著註冊HankDriver和IDriver

builder.RegisterType<HankDriver>().As<IDriver>();

最後修改MyCar的類別,簡單宣告建構子需要注入IDriver,並在MyCar內的Drive()方法呼叫IDriver的SayName()。

public class MyCar : ICar
{
    private IDriver Driver;

    public MyCar(IDriver driver)
    {
        this.Driver = driver; 
    }


    public void Drive()
    {
        this.Driver.SayName();

        Console.Write("Drive BMW");
    }
}

到此,完成了範例的前置作業,現在需求是註冊MyCar成為ICar時,需額外注入Driver類別,此時便可使用Lambda expression,下面範例中的c是IComponentContext物件,只要在Register區塊有註冊的物件,在此都可以透過Resolve方式取得實體類別,來協助建立Component,並將它註冊成Service。

 builder.Register(c=> new MyCar(c.Resolve<IDriver>())).As<ICar>();

 以下為完整Autofac範例內容,以及Console輸出。

var builder = new ContainerBuilder();
builder.RegisterType<HankDriver>().As<IDriver>();

builder.Register(c=> new MyCar(c.Resolve<IDriver>())).As<ICar>();

var container = builder.Build();

using (var scope = container.BeginLifetimeScope())
{
    var car = container.Resolve<ICar>();
    var driver = new Driver(car);
    driver.Dirve();
    Console.ReadLine();
}

 

以Module方式註冊

 為何會有Module的需求?

前面所描述的註冊方式,假如實作的邏輯需要抽換,則必須要修改程式碼並且重新編譯,但這會造成不好的使用者體驗。但如果將註冊的邏輯寫成JSON的Config檔案,的確可以省去需要重新編譯的困擾,但JSON檔的可維護性相對是比較差的,而且也違背封裝的意義,系統的邏輯寫在Config裡,怎麼看都怪怪的。為了解決這些問題,所以,還有一種很酷的註冊方式,就是使用Module來註冊。

甚麼是Module?

Module說穿了只是一個類別,主要功用是把Component和Service相關聯的註冊封裝起來。而且很方便的一點是Module之間是互通的,也就是說A Module註冊了A Service,B Module註冊了B Service,就代表系統如需注入A、B Service,可直接透過Autofac的Reslove來達到,即使兩個Service是隸屬於不同Module也沒關係。

Module的建構子如有需要注入相依的Component,需要自己處理,因為它本身是用來設定Container的,所以無法像其他Component一樣,讓Container自動注入相依的Component。

 那使用Module有那些好處?

  • 相關聯的Component \ Service註冊,可以封裝在同一個Module的類別裡,大幅提升可讀性。
  • Component可統一集中管理注入的Parameter,在維護上方便許多。
  • Module是類別,須通過編譯檢查,所以是強型別。假如設定錯誤,編譯時就會發現,不會像JSON或XML爆掉了才會知道。
  • 系統在Run Time時選用的Module可透過JSON或XML來設定,動態抽換,後面會用範例詳述。

實作一個範例來玩玩

基本環境安裝,需在Nuget下載下列三個套件

  • Microsoft.Extensions.Configuration
  • Microsoft.Extensions.Configuration.Json
  • Autofac.Configuration

新增一個CarModule檔案,先來定義我們的Module,重點只有兩個,繼承Autofac Module,然後override Load方法。剩下的註冊方式跟前面一樣就不多說了。

public class CarModule:Autofac.Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterType<HankDriver>().As<IDriver>();
        builder.Register(c => new MyCar(c.Resolve<IDriver>())).As<ICar>();
    }
}

接下來新增JSON檔案,type的輸入方式,第一個參數是命名空間+類別名稱,第二個參數是組件名稱,可請參考下面兩個示意圖。

{
  "modules": [
    {
      "type": "DemoAutofac.Modules.CarModule, DemoAutofac"
    }
  ]
}

最後,調整主程式,相關說明直接寫在註解當中。

var config = new ConfigurationBuilder();
//取得目前專案的路徑,原本預設是在bin的Debug資料夾
string basePath = Path.GetDirectoryName(Path.GetDirectoryName(System.IO.Directory.GetCurrentDirectory()));
//設定成root路徑
config.SetBasePath(basePath);

//參數是與root路徑相對的位置
config.AddJsonFile("autofac.json");

//透過Config產生Module
var module = new ConfigurationModule(config.Build());

var builder = new ContainerBuilder();
//將Module註冊到Container內
builder.RegisterModule(module);

以上,就完成了Module的實作,當系統在執行時,抽換掉autofac.json檔案,便可即時改變系統的運作邏輯,雖然也不建議這樣做。另外,應該有發現,使用Module之後,相關聯的Component和Service都會封裝到一個具備意義的Module類別中,在後續維護系統時,也更加容易了解彼此的相依關係。

 

以掃描Assembly方式註冊

使用這種方式,狠一點根本不需要寫Component和Service間的註冊關係,可直接掃描dll後,自動用有實作該介面的類別來註冊。寫法很簡單,只需要載入指定的dll名稱,並宣告成AsImplementedInterfaces()即可。

//載入dll
var module = Assembly.Load("DemoAutofac");

//自動以實作介面的類別來註冊
builder.RegisterAssemblyTypes(module)
.AsImplementedInterfaces();

不過一般來說,會搭配Filter來使用,向本文的例子MyCar和HankDriver類別分別是Car和Driver結尾,所以,如果我只想註冊Car和Driver其他類別並不想註冊的話,可以添加Fiter來達到。所以採分層式開發的系統,可以直接用結尾名稱為Repository,自動對應註冊所有Repository的相關類別,其餘像Service等等都可以利用這種以限定結尾名稱的方式來自動對應。

var module = Assembly.Load("DemoAutofac");

builder.RegisterAssemblyTypes(module)
        //此處搭配Filter使用
        .Where( i=> i.Name.EndsWith("Car")|| i.Name.EndsWith("Driver"))
        .AsImplementedInterfaces();
假如一個介面,有多個類別同時實作的話,用這種方式會預設取最後一個定義的類別。換句話說,如果宣告了XX介面,然後又宣告A類別實作此介面,而後又宣告了B類別也實作此介面,預設會採用B類別註冊。

 

小結

本文介紹了一些常見的註冊Component和Serivce的方式,各有優缺點和適合的情境,就看開發者如何規劃自己的系統,找出最適合的模式。談完了Registering,接下來的主題會再對Resolving做深入的探討。