ユースケースコントローラをWFで作成してみる(3)

取りあえず、細かいところはいい加減ですが、ほぼシーケンス図のように実装してみた。テクニカルな点はいろいろあるが、ユースケースのフローをそのまま処理を記述できる点は非常に直感的で分かりやすくて良い。たとえば、請求書発行後にオーダを処理完了にする場合、WFで請求書発行→オーダ処理完了のシーケンスを書いて各アクティビティに対応する処理を書くことになる。このアクティビティごとに分けてフローでつながっているのが明記できるのがうれしい。

    private void 請求書発行_ExecuteCode(object sender, EventArgs e)
    {
        BillData = new Bill(OrderData);
    }

    private void オーダ処理完了_ExecuteCode(object sender, EventArgs e)
    {
        OrderData.Complete(DateTime.Now);
    }

もし、このようなことができない場合、画面からのイベントのアクションで請求書発行とオーダ処理完了を順次呼び出すことになると思うが、それぞれのアクティビティの独立性や依存性が明示されにくくなる。(それ以上にフローの前後関係の見通しが悪いのも問題ではあるが...)

上記においてWFの重要性は高いと思うが、ユースケースコントローラの導入自体が価値があるようにも感じた。WFを利用しないユースケースコントローラの導入の検討もやってみたい。

あと、ドメインモデルとの連携についても、大きな処理フロー制御をWFで行い、細かな処理をドメインモデルで行うことで非常に分かりやすくなっていると考えている。一点気になったのは、ドメインモデルで積極的に作成しようとしないと、WFの処理フローに制御ロジックに引きずられ、本来ドメインモデルのロジックをWF側に含めてしなう危険性がある。WFは(mediatorとして)処理の交通整理だけを行い、計算処理などは含めないようにすべきですからね。

ソースコードの抜粋

ユースケースコントローラのワークフロー処理

public sealed partial class OrderUseCaseFlow: SequentialWorkflowActivity
{
    ...

    public Order OrderData
    {
        get { return _orderData; }
        set { _orderData = value; }
    }

    public Shipment ShipmentData
    {
        get { return _shipmentData; }
        set { _shipmentData = value; }
    }

    public Bill BillData
    {
        get { return _billData; }
        set { _billData = value; }
    }

    private void オーダ登録_MethodInvoking(object sender, EventArgs e)
    {
        Console.WriteLine("オーダ登録");
    }

    private void 在庫引き当て指示_ExecuteCode(object sender, EventArgs e)
    {
        Console.WriteLine("在庫引き当て指示");

        Console.WriteLine("オーダ状態:" + OrderData.Status.ToString());
    }

    private void 納期回答_ExecuteCode(object sender, EventArgs e)
    {
        Console.WriteLine("納期取得:"+OrderData.GetDeliverableDate().ToString());

        Console.WriteLine("納期回答");
    }

    private void 在庫引き当て通知_Invoked(object sender, ExternalDataEventArgs e)
    {
        Console.WriteLine("在庫引き当て通知");
        OrderPickedEventArgs pe = (OrderPickedEventArgs)e;
        _orderPickedInfo = pe.Info;

        Console.WriteLine("引き当て済みに設定");
        OrderData.Pick();
        Console.WriteLine("確定納期:" + OrderData.GetDeliverableDate().ToString());

        Console.WriteLine("オーダ状態:" + OrderData.Status.ToString());
    }

    private void 発送指示_ExecuteCode(object sender, EventArgs e)
    {
        Console.WriteLine("発送先取得:"+OrderData.ShipAddress);

        Console.WriteLine("発送指示");

        Console.WriteLine("出荷作成");
        ShipmentData = new Shipment(OrderData, _orderPickedInfo);
    }

    private void オーダ処理完了通知_Invoked(object sender, ExternalDataEventArgs e)
    {
        Console.WriteLine("オーダ処理完了通知");
    }

    private void 請求書発行_ExecuteCode(object sender, EventArgs e)
    {
        Console.WriteLine("請求先取得:" + OrderData.BillAddress);

        Console.WriteLine("請求書発行");

        Console.WriteLine("請求作成");
        BillData = new Bill(OrderData);
    }


    private void オーダ処理完了_ExecuteCode(object sender, EventArgs e)
    {
        Console.WriteLine("オーダ処理完了");            
        OrderData.Complete(DateTime.Now);
        Console.WriteLine("オーダ状態:" + OrderData.Status.ToString());
    }


    private void オーダキャンセル通知_Invoked(object sender, ExternalDataEventArgs e)
    {
        Console.WriteLine("オーダキャンセル通知");
        OrderCanceledEventArgs ce = (OrderCanceledEventArgs)e;
        OrderData.Cancel(DateTime.Now, ce.Comment); 
    }        

    private void キャンセル指示_ExecuteCode(object sender, EventArgs e)
    {
        Console.WriteLine("キャンセル指示");
    }
}

ユースケースコントローラのサービス

class OrderUseCaseService
{
    ...

    public WorkflowInstance EntryOrder(WorkflowRuntime workflowRuntime, Order orderData)
    {
        Dictionary parameters = new Dictionary();
        parameters.Add("OrderData", orderData);
        parameters.Add("CorelId", CorelId);
        WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(OrderEntryWorkflow.OrderUseCaseFlow), parameters);
        instance.Start();
        return instance;
    }

    public void NotifyPicked(WorkflowInstance instance, PickedInfo info)
    {
        OrderService svc = (OrderService)instance.WorkflowRuntime.GetService(typeof(IOrderService));
        svc.RaisePickedEvent(new OrderPickedEventArgs(instance.InstanceId, CorelId, info));
    }

    public void NotifyCompleted(WorkflowInstance instance)
    {
        OrderService svc = (OrderService)instance.WorkflowRuntime.GetService(typeof(IOrderService));
        svc.RaiseCompletedEvent(new OrderEventArgs(instance.InstanceId, CorelId));
    }

    public void NotifyCanceled(WorkflowInstance instance,string comment)
    {
        OrderService svc = (OrderService)instance.WorkflowRuntime.GetService(typeof(IOrderService));
        svc.RaiseCanceledEvent(new OrderCanceledEventArgs(instance.InstanceId, CorelId, comment));
    }

}

ドメインモデル

public class Order
{
    ...
    public string OrderID
    {
        get { return _orderID; }
        set { _orderID = value; }
    }

    public Item OrderItem
    {
        get { return _orderItem; }
        set { _orderItem = value; }
    }

    public int Amount
    {
        get { return _amount; }
        set { _amount = value; }
    }
    
    public OrderStatus Status
    {
        get { return _status; }
    }

    public string ShipAddress
    {
        get { return _shipAddress; }
        set { _shipAddress = value; }
    }

    public string BillAddress
    {
        get { return _billAddress; }
        set { _billAddress = value; }
    }

    /// 納期計算
    public DateTime GetDeliverableDate()
    {
        if (Status == OrderStatus.Ordered)
        {
            //未引き当ての場合 引当可能な在庫があれば3日後、なければ10日後
            if (OrderItem.AvailableStock >= this.Amount)
            {
                return DateTime.Now.AddDays(3);
            }
            else
            {
                return DateTime.Now.AddDays(10);
            }
        }
        else if (Status == OrderStatus.Canceled)
        {
            throw new ApplicationException("キャンセルされたオーダです");
        }
        else
        {
            //引き当て済み 引き当て時に設定した納期
            return _deliveryDate;
        }
    }

    /// 引き当て解除
    public void Unpick()
    {
        OrderItem.Unpick(Amount);
        _status = OrderStatus.Ordered;
    }

    /// 引き当て
    public void Pick()
    {
        _deliveryDate = GetDeliverableDate();
        OrderItem.Pick(Amount);
        _status = OrderStatus.Picked;
    }

    /// 完了
    public void Complete(DateTime completedDate)
    {
        _status = OrderStatus.Completed;
        _completedDate = completedDate;
    }

    /// キャンセル
    public void Cancel(DateTime canceledDate, string comment)
    {
        if (Status == OrderStatus.Completed || Status == OrderStatus.Canceled)
        {
            throw new ApplicationException("完了したオーダはキャンセルできません");
        }
        OrderItem.Unpick(Amount);
        _status = OrderStatus.Canceled;
        _canceledDate = canceledDate;
        _cancelComment = comment;
    }
}

public class Item
{
    ...
    public string ItemName
    {
        get { return _itemName; }
        set { _itemName = value; }
    }

    public int Stock
    {
        get { return _stock; }
        set { _stock = value; }
    }

    public int PickedStock
    {
        get { return _pickedStock; }
        set { _pickedStock = value; }
    }

    public int AvailableStock
    {
        get { return Stock - PickedStock; }
    }

    public void Pick(int amount)
    {
        if (AvailableStock< amount)
        {
            throw new ApplicationException("在庫不足です。引き当てできません");
        }
        PickedStock += amount;
    }

    public void Unpick(int amount)
    {
        PickedStock -= amount;
    }

    public void Deliver(int amount)
    {
        Stock -= amount;
        PickedStock -= amount;
    }
}