透過故事情境的方式,演練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是一個解決方案,就來看看該如何實作。直接把上面的範例稍作修改,註冊完後多呼叫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,其餘程式碼都無須修改,透過更新資料庫資料,就能讓我的系統擁有新的擴充邏輯,達到對修改而封閉,對擴充而開放的效果。