[相依性注入] 從無到有

  • 5121
  • 0
  • 2016-10-30

為你的程式附魔吧,談相依性注入(DI)

前言

第一次接觸DI(Dependency Injection),一開始覺得非常麻煩!因為在定義全新的獨立類別時,需要先額外抽象出介面,然後宣告此類別實作此介面,有點多此一舉的感覺。

而且抽象在系統設計裡是一把兩面刃,它帶來更高的彈性,但也相對提升其他開發者對程式碼理解的難度。

那這樣DI到底有甚麼好處?思考了許久,終於有點領悟。

 

情境案例

假設今天接到外包的案子,是一款三國歷史背景的策略遊戲,剛上線的時候,軍隊兵種只有步兵、騎兵、弓兵,以下是需求

  1. 步兵提供防禦功能
  2. 騎兵提供衝鋒功能
  3. 弓兵提供多重射擊功能。

在台灣開發遊戲,因為要靠改版坑錢,喔~不,是因為敏捷開發思維,所以要迅速先推出一個半殘版,然後再推出新的改版元素來吸引玩家。所以,已經知道這個案子的遊戲兵種一定會做大幅度的增加,假設事先就把擴充點建立起來,將會是一個很好的賺錢機會...笑

 

環境建置

這個遊戲的開發,我們就採取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},其餘就不贅述。

在建構子處,我們可以看到注入的服務都是介面型別,代表完全相依於介面,所以日後在做功能擴充時,只需要調整注入的類別即可,Api無需修改。
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第一個遇到的卡點,通常頭上都會有個問號,為何程式碼會知道要用哪個類別去實作此介面?這也是本段落的重點所在,大致分為三個步驟來做說明。

  1. 設定Startup,讓ASP.NET WebApi把處理相依性注入的功能交給Autofac服務來處理
  2. 設定WebConfig,定義Autofac要注入的module資訊
  3. 實作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下載完整程式