StateModelApplicationController(C#) (^o^)

PofEAAのStateModelApplicationControllerC#で記述してみた。

まずは入力コントローラとなるFrontControllerのハンドラから。ApplicationControllerのインスタンスは名前における制約に基づいてリフレクションで動的に生成される。ハンドラは、生成されたApplicationControllerのインスタンスからコマンド文字列とAssetの状態に対応したDomainCommandのインスタンスを取得して実行し、同様にApplicationControllerから取得したViewのURLに遷移を行う。

今回の例では使用していないが、DomainCommandでセッションにアクセスしたい場合もあるだろう。このような場合にはハンドラにIRequiresSessionStateインターフェースを実装しておけば、DomainCommandのExecuteメソッドのパラメータとなるHttpContextのインスタンスでセッションを参照することができる。

using System;
using System.Web;
using System.Web.SessionState;
using System.Collections.Specialized;
using System.Reflection;
using System.Runtime.Remoting;

namespace ApplicationControllerSample.Web.ApplicationController
{
    public class FrontHandler : IHttpHandler, IRequiresSessionState
    {
        public bool IsReusable
        {
            get { return true; }
        }

        public void ProcessRequest(HttpContext context)
        {
            IApplicationController applicationController = GetApplicationController(context.Request.Params);
            string commandString = context.Request.Params["command"];
            IDomainCommand command = applicationController.GetDomainCommand(commandString, context.Request.Params);
            command.Execute(context);
            string viewPage = string.Format("{0}.aspx", applicationController.GetView(commandString, context.Request.Params));
            context.Server.Transfer(viewPage);
        }

        private IApplicationController GetApplicationController(NameValueCollection parms)
        {
            string controllerName = string.Format("ApplicationControllerSample.Web.ApplicationController.{0}Controller.{0}ApplicationController", parms["controller"]);
            IApplicationController controller = null;

            try
            {
                Type controllerType = Assembly.GetExecutingAssembly().GetType(controllerName);
                controller = (IApplicationController) controllerType.GetMethod("CreateApplicationController", BindingFlags.Public | BindingFlags.Static).Invoke(null, null);
            }
            catch (Exception)
            {
                throw;
            }

            return controller;
        }
    }
}

ApplicationControllerのインターフェース。ASP.NETランタイムに依存していないので具象コントローラはユニットテストが可能である。

using System;
using System.Collections.Specialized;

namespace ApplicationControllerSample.Web.ApplicationController
{
    public interface IApplicationController
    {
        IDomainCommand GetDomainCommand(string commandString, NameValueCollection parms);

        string GetView(string commandString, NameValueCollection parms);
    }
}

具象コントローラ内部のコレクションに格納され、DomainCommandの型とビューのURLとなる文字列を保持するResponseクラス

using System;
using System.Reflection;
using System.Runtime.Remoting;

namespace ApplicationControllerSample.Web.ApplicationController
{
    public class Response
    {
        private Type dommainCommandType_;
        private string viewUrl_;

        public Response(Type dommainCommandType, string viewUrl)
        {
            this.dommainCommandType_ = dommainCommandType;
            this.viewUrl_ = viewUrl;
        }

        public string ViewUrl
        {
            get { return viewUrl_; }
        }

        public IDomainCommand GetDomainCommand()
        {
            return Activator.CreateInstance(dommainCommandType_) as IDomainCommand;
        }
    }
}

もちろん、このクラスもテスト可能である。

using System;
using NUnit.Framework;
using NUnit.Framework.SyntaxHelpers;
using ApplicationControllerSample.Web.ApplicationController;
using ApplicationControllerSample.Web.ApplicationController.AssetController;

namespace ApplicationControllerSample.Tests.Web.ApplicationController
{
    [TestFixture]
    public class ResponseTest
    {
        private Response target_;

        [SetUp]
        public void Responseのインスタンス生成()
        {
            target_ = new Response(typeof(GatherReturnDetailsCommand), "ReturnPage");
        }

        [Test]
        public void ドメインコマンドのインスタンスが取得できるべき()
        {
            Assert.That(target_.GetDomainCommand(), Is.InstanceOfType(typeof(GatherReturnDetailsCommand)));
        }

        [Test]
        public void ViewのUrlが取得できるべき()
        {
            Assert.That(target_.ViewUrl, Is.EqualTo("ReturnPage"));
        }
    }
}

それでは、具象コントローラを見ていこう。コマンド文字列とAssetの状態によって対象となるResponseを取得し、DomainCommandのインスタンスとビューのURLとなる文字列を返す。コントローラのインスタンスは、必要なResponse群を自身に登録するCreationMethodによって取得される。このCreationMethodはハンドラからメソッド名によるリフレクション呼び出しが行われるため、各コントローラ間の実装において名前における制約が課せられることになる。

using System;
using System.Web;
using System.Collections.Generic;
using System.Collections.Specialized;
using ApplicationControllerSample.Web.Model;

namespace ApplicationControllerSample.Web.ApplicationController.AssetController
{
    public class AssetApplicationController : IApplicationController
    {
        private Dictionary<string, Dictionary<AssetStatus, Response>> events_ = new Dictionary<string, Dictionary<AssetStatus, Response>>();

        public static AssetApplicationController CreateDefault()
        {
            return new AssetApplicationController();
        }

        public static AssetApplicationController CreateApplicationController()
        {
            AssetApplicationController controller = CreateDefault();
            controller.AddResponse("return", AssetStatus.OnLease, typeof(GatherReturnDetailsCommand), "ReturnPage");
            controller.AddResponse("return", AssetStatus.InInventory, typeof(NullAssetCommand), "IllegalActionPage");
            controller.AddResponse("damage", AssetStatus.OnLease, typeof(LeaseDamageCommand), "LeaseDamagePage");
            controller.AddResponse("damage", AssetStatus.InInventory, typeof(InventoryDamageCommand), "InventoryDamagePage");

            return controller;
        }

        private AssetApplicationController()
        { }

        public void AddResponse(string commandString, AssetStatus status, Type dommainCommandType, string viewUrl)
        {
            Response response = new Response(dommainCommandType, viewUrl);

            if (!events_.ContainsKey(commandString))
                events_.Add(commandString, new Dictionary<AssetStatus, Response>());

            GetResponseDictionary(commandString).Add(status, response);
        }

        public IDomainCommand GetDomainCommand(string commandString, NameValueCollection parms)
        {
            return GetResponse(commandString, GetAssetStatus(parms)).GetDomainCommand();
        }

        public string GetView(string commandString, NameValueCollection parms)
        {
            return GetResponse(commandString, GetAssetStatus(parms)).ViewUrl;
        }

        private Dictionary<AssetStatus, Response> GetResponseDictionary(string commandString)
        {
            return events_[commandString];
        }

        private Response GetResponse(string commandString, AssetStatus status)
        {
            return GetResponseDictionary(commandString)[status];
        }

        private AssetStatus GetAssetStatus(NameValueCollection parms)
        {
            string id = parms["assetID"];
            Asset asset = Asset.FindBy(id);

            return asset.Status;
        }
    }
}

具象コントローラのユニットテストでは、主に状態モデル実現の確認を行っている。ここでは、正しいコマンドのインスタンスとViewのURLが取得できることが分かればよい。

using System;
using System.Collections.Specialized;
using NUnit.Framework;
using NUnit.Framework.SyntaxHelpers;
using ApplicationControllerSample.Web.ApplicationController.AssetController;
using ApplicationControllerSample.Web.Model;

namespace ApplicationControllerSample.Tests.Web.ApplicationController.AssetController
{
    [TestFixture]
    public class AssetApplicationControllerTest
    {
        [Test]
        public void 追加したResponseから対象となるコマンドのインスタンスとViewのUrlが取得できるべき()
        {
            AssetApplicationController controller = AssetApplicationController.CreateDefault();
            controller.AddResponse("return", AssetStatus.OnLease, typeof(GatherReturnDetailsCommand), "ReturnPage");
            NameValueCollection parameters = new NameValueCollection();
            parameters["assetID"] = "0001";   // AssetStatus.OnLease

            Assert.That(controller.GetDomainCommand("return", parameters), Is.InstanceOfType(typeof(GatherReturnDetailsCommand)));
            Assert.That(controller.GetView("return", parameters), Is.EqualTo("ReturnPage"));
        }

        [Test]
        public void コマンド名とパラメータに対応するコマンドのインスタンスとViewのUrlが取得できるべき()
        {
            AssetApplicationController controller = AssetApplicationController.CreateApplicationController();
            NameValueCollection parameters = new NameValueCollection();

            parameters["assetID"] = "0001";   // AssetStatus.OnLease

            Assert.That(controller.GetDomainCommand("return", parameters), Is.InstanceOfType(typeof(GatherReturnDetailsCommand)));
            Assert.That(controller.GetView("return", parameters), Is.EqualTo("ReturnPage"));

            parameters["assetID"] = "0002";   // AssetStatus.InInventory

            Assert.That(controller.GetDomainCommand("return", parameters), Is.InstanceOfType(typeof(NullAssetCommand)));
            Assert.That(controller.GetView("return", parameters), Is.EqualTo("IllegalActionPage"));

            parameters["assetID"] = "0001";   // AssetStatus.OnLease

            Assert.That(controller.GetDomainCommand("damage", parameters), Is.InstanceOfType(typeof(LeaseDamageCommand)));
            Assert.That(controller.GetView("damage", parameters), Is.EqualTo("LeaseDamagePage"));

            parameters["assetID"] = "0002";   // AssetStatus.InInventory

            Assert.That(controller.GetDomainCommand("damage", parameters), Is.InstanceOfType(typeof(InventoryDamageCommand)));
            Assert.That(controller.GetView("damage", parameters), Is.EqualTo("InventoryDamagePage"));
        }
    }
}

続いて、FrontControllerで実行されるDomainCommandのインターフェースと具象コマンド。これらはASP.NETランタイムに依存しているためユニットテストはあきらめる。

using System;
using System.Web;

namespace ApplicationControllerSample.Web.ApplicationController
{
    public interface IDomainCommand
    {
        void Execute(HttpContext context);
    }
}

具象コマンドは実際はもっと複雑になるだろう。あくまでApplicationControllerの挙動を理解するためのサンプルなので実装を簡略化している。

using System;
using System.Web;
using ApplicationControllerSample.Web.Model;

namespace ApplicationControllerSample.Web.ApplicationController.AssetController
{
    public class GatherReturnDetailsCommand : IDomainCommand 
    {
        public void Execute(HttpContext context)
        {
            string id = context.Request.Params["assetID"];
            Asset asset = Asset.FindBy(id);

            context.Items.Add("gatherReturn", asset.GetGatherReturnDetails());
        }
    }

    public class NullAssetCommand : IDomainCommand
    {
        public void Execute(HttpContext context)
        {
            context.Items.Add("message", "不正なアクションが実行されました。");
        }
    }

    public class LeaseDamageCommand : IDomainCommand
    {
        public void Execute(HttpContext context)
        {
            string id = context.Request.Params["assetID"];
            Asset asset = Asset.FindBy(id);

            context.Items.Add("leaseDamage", asset.GetLeaseDamege());
        }
    }

    public class InventoryDamageCommand : IDomainCommand
    {
        public void Execute(HttpContext context)
        {
            string id = context.Request.Params["assetID"];
            Asset asset = Asset.FindBy(id);

            context.Items.Add("inventoryDamage", asset.GetInventoryDamege());
        }
    }
}

最後はモデル。これも簡易実装である。

using System;
using System.Collections.Generic;

namespace ApplicationControllerSample.Web.Model
{
    public enum AssetStatus
    {
        OnLease,
        InInventory
    }

    public class Asset
    {
        private readonly static List<Asset> assets_ = new List<Asset>();

        static Asset()
        {
            Asset onLease = new Asset("0001", AssetStatus.OnLease);
            assets_.Add(onLease);

            Asset inInventory = new Asset("0002", AssetStatus.InInventory);
            assets_.Add(inInventory);
        }

        public static Asset FindBy(string id)
        {
            return assets_.Find(delegate(Asset asset) { return asset.Id == id; });
        }

        private string id_;
        private AssetStatus status_;

        public Asset(string id, AssetStatus status)
        {
            this.id_ = id;
            this.status_ = status;
        }

        public string Id
        {
            get { return id_; }
        }

        public AssetStatus Status
        {
            get { return status_; }
        }

        public string GetGatherReturnDetails()
        {
            return string.Format("Asset ID: {0}の収益に関する情報を表示します。", id_);
        }

        public string GetLeaseDamege()
        {
            return string.Format("Asset ID: {0}のLeaseDamageに関する情報を表示します。", id_);
        }

        public string GetInventoryDamege()
        {
            return string.Format("Asset ID: {0}のInventoryDamageに関する情報を表示します。", id_);
        }
    }
}

もちろん各ビューと受け入れテストも記述したがここでは割愛する。