為你的程式附魔吧,談相依性注入(DI)
前言
第一次接觸DI(Dependency Injection),一開始覺得非常麻煩!因為在定義全新的獨立類別時,需要先額外抽象出介面,然後宣告此類別實作此介面,有點多此一舉的感覺。
而且抽象在系統設計裡是一把兩面刃,它帶來更高的彈性,但也相對提升其他開發者對程式碼理解的難度。
那這樣DI到底有甚麼好處?思考了許久,終於有點領悟。
情境案例
假設今天接到外包的案子,是一款三國歷史背景的策略遊戲,剛上線的時候,軍隊兵種只有步兵、騎兵、弓兵,以下是需求
- 步兵提供防禦功能
- 騎兵提供衝鋒功能
- 弓兵提供多重射擊功能。
在台灣開發遊戲,因為要靠改版坑錢,喔~不,是因為敏捷開發思維,所以要迅速先推出一個半殘版,然後再推出新的改版元素來吸引玩家。所以,已經知道這個案子的遊戲兵種一定會做大幅度的增加,假設事先就把擴充點建立起來,將會是一個很好的賺錢機會...笑
環境建置
這個遊戲的開發,我們就採取WebApi的架構,首先建立MVC WebApi專案並用nuget安裝DI相關套件,這個範例是用Autofac來做介紹。
再來,新增Modules資料夾,等會有關DI的相關邏輯都會封裝在這一層中。最後,新增Services資料夾,我們會把兵種相關的邏輯都封裝在Service層裡。
實作遊戲
在Services資料夾內新增步兵的資料夾,宣告步兵的介面,根據需求,它需要有一個防禦的功能,所以提供一個Guard方法。
public interface IInfantry
{
string Guard();
}
接著定義步兵的實作,單純只回傳一個字串。
/// <summary>
/// 步兵
/// </summary>
public class Infantry: IInfantry
{
public string Guard()
{
return "增加自身格檔能力";
}
}
接著建立騎兵的介面,提供一個Charge方法,Charge的翻譯這裡是衝鋒的意思,而不是收費,不過中文似乎把對方買單也有幹掉對方的意思在,但我也不是很懂,反正不是在教英文就別那麼計較了。
public interface ICavalry
{
string Charge();
}
然後實作騎兵的類別
public class Cavalry: ICavalry
{
public string Charge()
{
return "騎兵衝鋒";
}
}
最後一個是弓兵,就快速帶過
public interface IArcher
{
string MultiShot();
}
public class Archer: IArcher
{
public string MultiShot()
{
return "多重射擊";
}
}
遊戲的Service部分已經實作完成,接下來我們要實作Api,我們在建構子的地方注入了步兵、騎兵、弓兵的服務,並且簡單提供一個SendOrder(發送命令)的Api,使用HttpGet,走預設的route=>api/{controller}/{id},其餘就不贅述。
public class ArmyCommandController : ApiController
{
private IArcher ArcherService;
private ICavalry CavalryService;
private IInfantry InfantryService;
public ArmyCommandController(IInfantry infantryService, ICavalry cavalryService, IArcher archerService)
{
this.InfantryService = infantryService;
this.CavalryService = cavalryService;
this.ArcherService = archerService;
}
[HttpGet]
[ResponseType(typeof(string))]
public string SendOrder(ArmyType Id)
{
var result = "";
switch (Id)
{
case ArmyType.Infantry:
result = this.InfantryService.Guard();
break;
case ArmyType.Cavalry:
result = this.CavalryService.Charge();
break;
case ArmyType.Archer:
result = this.ArcherService.MultiShot();
break;
default:
break;
}
return result;
}
}
最後,再宣告一下ArmyType是一個Enum
public enum ArmyType
{
Infantry = 0,
Cavalry = 1,
Archer=2
}
最後的資料夾架構如下所示,到這裡已經完成了遊戲程式碼撰寫。
將服務注入
這個階段會是玩DI第一個遇到的卡點,通常頭上都會有個問號,為何程式碼會知道要用哪個類別去實作此介面?這也是本段落的重點所在,大致分為三個步驟來做說明。
- 設定Startup,讓ASP.NET WebApi把處理相依性注入的功能交給Autofac服務來處理
- 設定WebConfig,定義Autofac要注入的module資訊
- 實作Autofac module
第一個步驟,設定Startup,可參考下列程式碼。
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
ConfigureAutofac(app);
}
private void ConfigureAutofac(IAppBuilder app)
{
var builder = new ContainerBuilder();
//擴充WebApi的Controller提供額外的建構子可注入我們定義的服務
builder.RegisterApiControllers(Assembly.GetExecutingAssembly());
//註冊module來源,這個例子是從Web.config讀取 參數是標籤名稱
builder.RegisterModule(new ConfigurationSettingsReader("autofac"));
var container = builder.Build();
//相依性注入的服務交給我們定義的container來處理
var config = GlobalConfiguration.Configuration;
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
}
第二個步驟,設定Web Config。
先定義section,name可自訂,這個例子是"autofac",必須跟上面ConfigurationSettingsReader的參數名稱一致。
<configSections>
<section name="autofac" type="Autofac.Configuration.SectionHandler, Autofac.Configuration" />
</configSections>
承上,再定義對應的標籤內容。標籤的名稱必須跟上方section內定義的name要一致,因為上方的name是定義為"autofac",所以標籤名稱就會是<autofac>,它的結構是modules內可以含多個module,module的type設定包含兩個參數,用逗點隔開,第一個參數是型別名稱,第二個參數是這個型別在哪個dll內。
<autofac>
<modules>
<module type="DemoAutofac.Modules.ServiceModule,DemoAutofac"> </module>
</modules>
</autofac>
最後一個步驟,就是定義module,把注入的邏輯封裝在module內。之前有建立一個Modules的資料夾,現在就在那新增一個ServiceModule類別,並實作注入的邏輯。
public class ServiceModule: Module
{
protected override void Load(ContainerBuilder builder)
{
//當遇到IArcher介面時,以Archer類別注入
builder.RegisterType<Archer>().As<IArcher>();
//依此類推
builder.RegisterType<Infantry>().As<IInfantry>();
builder.RegisterType<Cavalry>().As<ICavalry>();
}
}
到此大功告成,來測試一下呼叫Api並且給參數Archer,測試結果如下所示。
第一次改版
上線之後,玩的人不少,這是一個很好的改版時機,讓大家儲值更多的錢,在虛擬世界拚得你死我活,我們開發遊戲的人才能在現實世界當大爺。
一開始大家都練弓兵,騎兵都沒人練,因為技能太爛,所以最簡單的賺大錢方式,就是藉由修改平衡性為由,把騎兵改成神,讓那些花錢的玩家再繼續儲值來練騎兵。最快速的做法,新增一個騎兵的子類別。
public class PowerCavalry : ICavalry
{
public string Charge()
{
return "衝鋒後進行踐踏";
}
}
然後把註冊的ServiceModule做修改,將原本的程式碼註解(最好是直接刪掉,但這裡是為了讓說明更加清楚),改由PowerCavalry的類別對ICavalry實作。
public class ServiceModule: Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<Archer>().As<IArcher>();
builder.RegisterType<Infantry>().As<IInfantry>();
//builder.RegisterType<Cavalry>().As<ICavalry>();
builder.RegisterType<PowerCavalry>().As<ICavalry>();
}
}
大功告成,Api商業邏輯的部分完全不用動,然後來測試看看。
恩,騎兵明顯變強了,接下來就可以看到別人的錢不斷地跑入自己的口袋囉。
結論
藉由DI(相依性注入),可以把new 物件這類的邏輯,把它從商業邏輯中隔離開來,這樣一來如果要更改實作的類別,便不會如果有1000個地方有new 這個類別,就得修改1000次。
另外,在開發大型架構,常會將很多服務模組化,所以常會在建構子注入多項服務的類別,如果是用傳統的寫法,就會在new的過程需要帶一堆參數,會造成使用上的嚴重不便利。單純以上面那個三國遊戲的為例,可以想像一下,未來兵種可能動不動就1~20種跑不掉,所以在建構子內就要塞20幾個參數才能new出來,真的是很恐怖,而利用DI的方式,可以把這些邏輯都封裝到module的相關類別內,Api可以更專注在商業邏輯,整體的架構會更加簡潔乾淨。
因上方的程式部分只有片段,如有需要可直接從Github下載完整程式。