エンティティ・リレーについて

はじめに

名著「セキュア・バイ・デザイン」に複雑な状態管理の解決策として「エンティティ・リレー」が紹介されています。

セキュア・バイ・デザインにはサンプルコードが多数記載されていますが、何故かエンティティ・リレーについてはサンプルコードがありません。

そこで勉強のためにコードにしてみました。

シナリオ

セキュア・バイ・デザインではエンティティ・リレーのシナリオとして注文状態を挙げています。

注文状態には以下のパターンがあります。

  • 作成中
  • 作成済み(未払い)
  • 作成済み(支払い却下)
  • 支払い済み
  • 配送中
  • 配送間違い
  • 消失
  • 配送済み
  • 受取拒否
  • 返品中
  • 返品済み
  • 返金済み

これを3つのstate objectに分けています。

①「仮注文」

  • 作成中
  • 作成済み(未払い)
  • 作成済み(支払い却下)
  • 支払い済み

②「本注文」

  • 支払い済み
  • 配送中
  • 配送間違い
  • 消失
  • 配送済み

③「注文取消」

  • 受取拒否
  • 返品中
  • 返品済み
  • 返金済み

そして状態が変わる条件は次の通りです。

  • 「支払い済み」になった場合に「本注文」になる。
  • 「配送済み」が「受取拒否」された場合に「注文取消」になる。

実装例

言語はC#です。

public interface IOrderState
{
    IOrderState Handle(OrderAction action);
}

public enum OrderAction
{
    StartCreation,
    Unpay,
    Reject,
    Pay,
    StartShipment,
    ReportShipmentError,
    Lost,
    Delivere,
    RefusedDelivery,
    StartReturn,
    Returned,
    Refund
}

public class ProvisionalOrderState : IOrderState
{
    private bool creationInProgress = false;
    private bool createdUnpaid = false;
    private bool createdPaymentRejected = false;
    private bool paid = false;
    
	public IOrderState Handle(OrderAction action)
    {
		switch (action)
        {
            case OrderAction.StartCreation:
                creationInProgress = true;
                break;
            case OrderAction.Unpay:
                createdUnpaid = true;
                break;
            case OrderAction.Reject:
                createdPaymentRejected = true;
                break;
            case OrderAction.Pay:
                paid = true;
                return new ActualOrderState();
            default:
                throw new InvalidOperationException();
        }

        return this;
    }
}

public class ActualOrderState : IOrderState
{
    private bool paid = true;
    private bool shipmentInProgress = false;
    private bool shipmentError = false;
    private bool losted = false;
    private bool delivered = false;

	public IOrderState Handle(OrderAction action)
    {
        switch (action)
        {
            case OrderAction.StartShipment:
                shipmentInProgress = true;
                break;
            case OrderAction.ReportShipmentError:
                shipmentError = true;
                break;
            case OrderAction.Lost:
                lost = true;
                break;
            case OrderAction.Delivery:
                delivered = true;
            case OrderAction.RefusedDelivery:
                return new CancellationOrderState();
            default:
                throw new InvalidOperationException();
        }

        return this;
    }
}

public class CancellationOrderState : IOrderState
{
    private bool refusedDelivered = true;
    private bool returning = false;
    private bool returned = false;
    private bool refunded = false;
    
	public IOrderState Handle(OrderAction action)
    {
        switch (action)
        {
            case OrderAction.StartReturn:
                returning = true;
                break;
            case OrderAction.Returned:
                returned = true;
                break;
            case OrderAction.Refund:
                refunded = true;
            default:
                throw new InvalidOperationException();
        }

        return this;
    }
}

public class Order
{
    private IOrderState state;

    public Order()
    {
        state = new ProvisionalOrderState();
    }

    public void Handle(OrderAction action)
    {
        state = state.Handle(action);
    }
}

実装例(ボツ)

最初は書籍内で紹介されているstate objectの実装方法を真似て考えました。

下記のコードでは一旦「注文取消」にするケースは除外して考えています。

public interface IOrderState
{
    IOrderState UpdateState(Order order);
}

public class ProvisionalOrderState : IOrderState
{
    private bool creationInProgress = false;
    private bool createdUnpaid = false;
    private bool createdPaymentRejected = false;
    private bool paid = false;

    public IOrderState UpdateState(Order order)
    {
        if (paid)
        {
            return new ActualOrderState();
        }
        return this;
    }
}

public class ActualOrderState : IOrderState
{
    private bool paid = true;
    private bool shipmentInProgress = false;
    private bool shipmentError = false;
    private bool lost = false;
    private bool delivered = false;

    public IOrderState UpdateState(Order order)
    {
        return this;
    }
}

public class CancellationOrderState : IOrderState
{
    private bool refusedDelivery = true;
    private bool returning = false;
    private bool returned = false;
    private bool refunded = false;

    public IOrderState UpdateState(Order order)
    {
        return this;
    }
}

public class Order
{
    private IOrderState state;

    public Order()
    {
        state = new ProvisionalOrderState();
    }
}

このコードの場合に難しいのは、

  • 「配送済み」が「受取拒否」された場合に「注文取消」になるケース
    • 「本注文」には「受取拒否」という状態がないため、何らかの工夫が必要になります。
  • 各bool値を更新するメソッドを用意すると、IOrderState にメソッドを追加するか別のインターフェースを用意するなどの工夫が必要になります。

「注文取消」する例ですが、セキュア・バイ・デザインはDDDの本なので、DDDに倣ってドメインイベントにすればいいのかなと思いました。

if (domainEvent is RefusedDeliveryEvent) の箇所は好みではないのですが、ここを解消しようとすると複雑になってしまうためここでは割愛しています。

public interface IOrderState
{
    IOrderState UpdateState(Order order, IDomainEvent domainEvent);
}

public interface IDomainEvent {}

public class RefusedDeliveryEvent : IDomainEvent {}

public interface IOrderState
{
    IOrderState UpdateState(Order order, IDomainEvent domainEvent);
}

public class ActualOrderState : IOrderState
{
    public IOrderState UpdateState(Order order, IDomainEvent domainEvent)
    {
        if (domainEvent is RefusedDeliveryEvent)
        {
            return new CancellationOrderState();
        }
        return this;
    }
}

public class Order
{
    private IOrderState state;
    private IDomainEvent event;

    public Order()
    {
        state = new ProvisionalOrderState();
    }

		public void UpdateState()
    {
        state = state.UpdateState(this, event);
        domainEvent = null;
    }

    public void RefuseDelivery()
    {
				domainEvent = new RefusedDeliveryEvent();
        UpdateState();
    }
}

ここまで考えましたが、各bool値を更新するメソッドを良い感じに用意する方法が思いつかなかったのでボツ。

未分類

Posted by ababa