web-dev-qa-db-ja.com

集約ルートにステートマシンパターンを実装する方法

予想どおり、他のエンティティに対して操作を実行するいくつかのアクションを持つ集約ルートをモデリングしています。ただし、アグリゲートには状態があり、これらの操作のいくつかは、アグリゲートが特定の状態にある場合にのみ実行できます。

状態パターンの実装を作成したので、集約は単にアクションを状態具象オブジェクトに委任します。しかし、それを実装した今、私は次の懸念に気づきました:

  • 複数の状態で呼び出すことができる操作があるため、実装を繰り返すことになりました。
  • ドメインイベントを生成するオペレーションがあるため、イベントを適切に追加できるように、ルートのイベントコレクションを渡す必要がありました。
  • 一部の操作では集約ルートのプライベートメンバーへのアクセスが必要なため、内部(C#)としてそれらを宣言するか、プライベートメンバーを変更する内部メソッドを作成することになりました。

そのため、実装に価値があるかどうか、または状態オブジェクトにCanPerformOperation1プロパティのみを含める必要があるかどうかを確認し、集約ルートにこのプロパティを確認させ、falseの場合はInvalidOperationExceptionをスローします。

次のコードは、私が試みていることの要約です。

interface IState {
    void Register(DomainName domain, CustomerCode code);
    void Activate(ActivationManifest manifest);
    void Lock();
    void Unlock();
    void EnsureConsistency();
}
class NewState : IState {
    // can only call Register method, transitions to RegisteredState
}
class RegisteredState : IState {
    // can only call Activate method, transitions to ActiveState
}
class ActiveState : IState {
    // can call Lock or EnsureConsistency
    // Lock transitions to locked state
    // EnsureConsistency can transition to RestrictedState or ActiveState
}
class LockedState : IState {
    // can only call Unlock, transitions to ActiveState
}
class RestrictedState : IState {
    // can only call EnsureConsistency, which can transition
    // to ActiveState or RestrictedState
}

class Tenant {
    private IState _state = new NewState(this);
    private readonly UserAccountCollection _accounts;
    private readonly LicenseCollection _licenses;
    private readonly ApplicationCollection _applications;

    // had to make these internal accesors to be used by
    // EnsureConsistency in ActiveState and RestrictedState
    internal UserAccountCollection _accounts => _accounts;

    internal Application RegisterApplication(AppKey key, UserAccount admin){
        // this method is called by the RegisteredState.Activate method
        // so what's the point of delegating?
    }
    internal License RegisterLicense(LicenseKey key) {
        // this method is also called by the RegisteredState.Activate
        // method, just like the one above.
    }
    // etc
}

これで、状態に依存するメソッドをさらに追加するように顧客に要求されたため、これは複雑さが増すだけです。したがって、CanRegisterApplication、CanRegisterLicenseなどのプロパティを追加するだけでよいのかと思ったところ、状態はフラグスイッチとしてのみ機能します。

私が達成しようとしていることを実装する適切な方法は何でしょうか?それとも、状態パターンが間違っているのでしょうか?

7
Fernando Gómez

私見、「お客様がメソッドを追加する必要がある」が、そもそも状態パターンが必要な理由です。状態パターンがないと、新しく追加されたメソッドでswitchまたはif/elseを繰り返す必要がある可能性があります。

実装の観点から見ると、抽象クラスは状態パターンのインターフェースよりもはるかに簡単です。

アクセス修飾子については、個々の状態をクライアントの内部クラスとして作成できます(この場合、テナントクラス、 ネストされたタイプ を参照)。このようにして、外部の世界が実際の状態の詳細を知る必要がないので、実際にはより良いカプセル化が得られます。

abstract class State {
    void Register(DomainName domain, CustomerCode code){};
    void Activate(ActivationManifest manifest){};
    void Lock(){};
    void Unlock(){};
    void EnsureConsistency(){};
}


class Tenant {
    class NewState : State {
        // can only call Register method, transitions to RegisteredState
    }
    class RegisteredState : State {
        // can only call Activate method, transitions to ActiveState
    }
    class ActiveState : State {
        // can call Lock or EnsureConsistency
        // Lock transitions to locked state
        // EnsureConsistency can transition to RestrictedState or ActiveState
    }
    class LockedState : State {
        // can only call Unlock, transitions to ActiveState
    }
    class RestrictedState : State {
        // can only call EnsureConsistency, which can transition
        // to ActiveState or RestrictedState
    }
    private State _state = new NewState(this);
    private readonly UserAccountCollection _accounts;
    private readonly LicenseCollection _licenses;
    private readonly ApplicationCollection _applications;

    // Should this go to abstract class State 
    private UserAccountCollection _accounts => _accounts;

    private Application RegisterApplication(AppKey key, UserAccount admin){
        // Should this go to abstract class State 
    }
    private License RegisterLicense(LicenseKey key) {
        // Should this go to abstract class State 
    }
    // etc
}

PS:Javaでは、人々は通常、列挙型を使用して状態パターンを実装します。ただし、.netでは列挙型でのポリモーフィズムは許可されていません。

3
ivenxu

私はしばしば同じ問題に遭遇します:私は、いくつかのビジネスプロセスを持っています。これらのビジネスプロセスでは、集計がいくつかの状態を通過する必要があり、いくつかの動作は各状態に固有です。

Saga

私がよく行うのは、この集合体とそのすべての状態クラスを複数の個別のクラスに分割することです。各クラスは特定の状態の集合体を表します。たとえば、FinancialTransactionRegisteredCheckedForFraudなどの状態を持つ1つのAuthorized集合体の代わりに、RegisteredTransactionCheckedForFraudTransactionAuthorizedTransactionなど.

それを非常に明確にするために、それはそれがどのように機能するかです。最初に、コマンドはクライアントによって発行されます。 Register transactionのようになります。コマンドが検証され、RegisteredTransactionが作成されてdbに保存されます。 Transaction is registeredと共に、新しく作成されたtransaction_idとともにイベントが公開されます。このトランザクションが不正であるかどうかを確認したいリスナーによって消費されます。たとえば、サードパーティのサービスをリクエストする必要があり、問題がなければ、CheckedForFraudTransactionが作成されます。内部的には、同じtransaction_idを使用して(必要ではありませんが)同じdbテーブル行を操作します。

私はこのアプローチが好きです。新しい動作が追加されるたびにStateのインターフェイスを変更する必要はありません。さらに、各ステップは十分に分離されており、統合テスト中にモックされる可能性があり、ビジネスプロセス全体がより明確になります。

5
Zapadlo

ビルドしようとしているのはステートマシンであり、akka .Net Frameworkはこの機能を非常にきれいに提供します。

これを読んでください link とそこにあるパターンがあなたの質問に答えます

0
techagrammer

すべての新しい状態の実装では、他のすべての実装を変更する必要があるため、実装は オープン/クローズの原則 に違反します。 戦略パターン に基づく別のアプローチを提案しています。

つまり、あなたのTenantはいつでもStateにあり、すべてのStateはそれが遷移する可能性のある状態のみを知っています。これにより、Tenantの状態遷移アルゴリズムが解放されます。ただし、Tenantは状態遷移を制御するものであり、制御下にあるものです。

一方、StatesTenantについて知っているべきではありません。別のStateへの移行を許可または拒否するという責任が1つしかないためです。

このパターンに従って、Tenantは非常にクリーンになります。

以下は実装例です(PHP内):

<?php

interface IState
{
    /**
     * @param IState $newState
     * @throws \Exception
     */
    public function tryTransition(IState $newState): void;
}

class NewState implements IState
{
    public function tryTransition(IState $newState): void
    {
        if (!($newState instanceof RegisteredState)) {
            throw new \Exception("Can only register");
        }
    }
}

class RegisteredState implements IState
{
    public function tryTransition(IState $newState): void
    {
        if (!($newState instanceof ActiveState)) {
            throw new \Exception("Can only activate");
        }
    }
}

class ActiveState implements IState
{
    public function tryTransition(IState $newState): void
    {
        if (!($newState instanceof RestrictedState) && !($newState instanceof ActiveState)) {
            throw new \Exception("Can only restrict or activate");
        }
    }
}

class LockedState implements IState
{
    public function tryTransition(IState $newState): void
    {
        if (!($newState instanceof ActiveState)) {
            throw new \Exception("Can only activate");
        }
    }
}

class RestrictedState implements IState
{
    public function tryTransition(IState $newState): void
    {
        if (!($newState instanceof RestrictedState) && !($newState instanceof ActiveState)) {
            throw new \Exception("Can only restrict or activate");
        }
    }
}

class Tenant
{
    /** @var IState */
    private $state = 0;

    /**
     * Other private data
     */
    private $_accounts;
    private $_licenses;
    private $_applications;

    public function __construct()
    {
        $this->state = new NewState();
    }

    public function RegisterApplication(AppKey $key, UserAccount $admin)
    {
        $this->tryTransitionOrThrowException(new RegisteredState());

        $this->_applications[] = new Application($key, $admin);
    }

    /**
     * @param IState $nextState
     * @throws Exception
     */
    private function tryTransitionOrThrowException(IState $nextState)
    {
        $this->state->tryTransition($nextState);
        $this->state = $nextState;
    }

    public function Activate(LicenseKey $key)
    {
        $this->tryTransitionOrThrowException(new ActiveState());

        $this->_licenses[] = new License($key);
    }

    public function Lock()
    {
        $this->tryTransitionOrThrowException(new LockedState());
    }

    public function UnLock()
    {
        $this->tryTransitionOrThrowException(new ActiveState());
    }
    // etc
}
0