[Autofac套件介紹-5] Autofac Metadata情境演練

透過故事情境的方式,演練Autofac的Metadata功能

故事案例

假設有一間遊戲公司要設計了一個角色扮演的遊戲,目前有兩個職業,戰士、弓箭手,而我們目前接到的需求,單純只是要設計角色模組,提供"攻擊"基本動作,程式碼簡單如下所示。

//定義角色的基本行為 需要具備攻擊模式
public interface IRoleService
{
    string Attack();
}

//實作弓箭手的角色
class ArcherService : IRoleService
{
    public string Attack()
    {
        return "射擊";
    }
}

//實作戰士的角色
class WarriorService : IRoleService
{
    public string Attack()
    {
        return "劈砍";
    }
}

接著把ArcherService、WarriorService都註冊起來,接著就可以透過Resolve方法取得實體類別。

var builder = new ContainerBuilder();
//註冊弓箭手
builder.RegisterType<ArcherService>().As<IRoleService>();
//註冊戰士
builder.RegisterType<WarriorService>().As<IRoleService>();

var container = builder.Build();

using (var scope = container.BeginLifetimeScope())
{
    var warrior = scope.Resolve<IRoleService>();
    var archer = scope.Resolve<IRoleService>();

    Console.WriteLine(string.Format("戰士採取攻擊: {0}", warrior.Attack()));
    Console.WriteLine(string.Format("弓箭手採取攻擊:{0}", archer.Attack()));
    Console.ReadLine();
}

執行之後,執行結果如下,出乎意料外弓箭手也採取劈砍的方式攻擊。看來一次註冊多組Component成為Service時,似乎存在某些問題需要解決。

該如何註冊多個Component?

仔細觀察上面的例子,我們發現,透過Resolve取得Component時,因註冊了多個Component,且Autofac預設"後"註冊的Component (WarriorService)會取代"前"註冊的Component (ArcherService),而導致弓箭手的攻擊模式也變成"劈砍"的Bug。

但如果改由直接判定目前玩家角色,如果是戰士就用WarriorService,反之則用ArcherService來實作,則又回到原來的老路,程式又跟實作類別再度相依,也不會是我們想要的結果。

所以,最好的方式就是透過Metadata,來把各個Component都增加一些標記來描述,之後就可以依照使用情境來判斷Metadata的資訊,來取得對應的Component。

Metadata中文翻譯成中繼資料,大陸用語為元數據。其實講白話就是用來描述資料的資料。

 既然Metadata是一個解決方案,就來看看該如何實作。直接把上面的範例稍作修改,註冊完後多呼叫WithMetadata的方法,它接受兩個參數,是一組Key、Value的形式,我們就把ArcherService賦予一個Key叫做"Role",Value標記成"Archer";WarriorService同樣賦予一個Key叫做"Role",Value則標記成"Warrior"。

builder.RegisterType<ArcherService>().As<IRoleService>().WithMetadata("Role", "Archer"); ;

builder.RegisterType<WarriorService>().As<IRoleService>().WithMetadata("Role", "Warrior"); ;

因為我們註冊了多組Component,因此在呼叫Resolve時,泛型的地方需宣告為"一組IRoleService的Component陣列",它的簽名是IEnumerable<Meta<IRoleService>>,這是固定寫法,中間夾著的Meta是用來存放Metadata使用,請直接參考範例。

 var warrior = scope.Resolve<IEnumerable<Meta<IRoleService>>>();

 var archer = scope.Resolve<IEnumerable<Meta<IRoleService>>>();

接下來,便可透過Lambda方式,判斷Metadata,然後透過存取Value屬性,取得真正的實體類別,請參考範例

                    //先取得Component的陣列
var warrior = scope.Resolve<IEnumerable<Meta<IRoleService>>>())
                   //透過Lambda判斷Metadata 
                   .First(x=>x.Metadata["Role"].Equals("Warrior")
                   //存取Value屬性可取得Component
                   .Value;

var archer = scope.Resolve<IEnumerable<Meta<IRoleService>>>()
                  .First(x => x.Metadata["Role"].Equals("Archer"))
                  .Value;

Console.WriteLine(string.Format("戰士採取攻擊: {0}", warrior.Attack()));
Console.WriteLine(string.Format("弓箭手採取攻擊:{0}", archer.Attack()));
Console.ReadLine();

執行結果如下所示,攻擊方式終於正確了。

小結

透過這個簡單的範例演練,可以發現,比對Metadata屬性時,上述例子是用寫死的方式來做比對。而在現實生活裡,資料的來源大部分是來自資料庫,因此一旦資料庫資料更新,Autofac就能動態變更系統的運作邏輯,而無須修改任何程式碼。

程式設計原則中的"開閉原則",便可透過這樣的方式實現。例如,當我有新的職業加入時,我只需要新增一個該職業的Component,其餘程式碼都無須修改,透過更新資料庫資料,就能讓我的系統擁有新的擴充邏輯,達到對修改而封閉,對擴充而開放的效果。