予想どおり、他のエンティティに対して操作を実行するいくつかのアクションを持つ集約ルートをモデリングしています。ただし、アグリゲートには状態があり、これらの操作のいくつかは、アグリゲートが特定の状態にある場合にのみ実行できます。
状態パターンの実装を作成したので、集約は単にアクションを状態具象オブジェクトに委任します。しかし、それを実装した今、私は次の懸念に気づきました:
そのため、実装に価値があるかどうか、または状態オブジェクトに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などのプロパティを追加するだけでよいのかと思ったところ、状態はフラグスイッチとしてのみ機能します。
私が達成しようとしていることを実装する適切な方法は何でしょうか?それとも、状態パターンが間違っているのでしょうか?
私見、「お客様がメソッドを追加する必要がある」が、そもそも状態パターンが必要な理由です。状態パターンがないと、新しく追加されたメソッドで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では列挙型でのポリモーフィズムは許可されていません。
私はしばしば同じ問題に遭遇します:私は、いくつかのビジネスプロセスを持っています。これらのビジネスプロセスでは、集計がいくつかの状態を通過する必要があり、いくつかの動作は各状態に固有です。
私がよく行うのは、この集合体とそのすべての状態クラスを複数の個別のクラスに分割することです。各クラスは特定の状態の集合体を表します。たとえば、FinancialTransaction
、Registered
、CheckedForFraud
などの状態を持つ1つのAuthorized
集合体の代わりに、RegisteredTransaction
、CheckedForFraudTransaction
、AuthorizedTransaction
など.
それを非常に明確にするために、それはそれがどのように機能するかです。最初に、コマンドはクライアントによって発行されます。 Register transaction
のようになります。コマンドが検証され、RegisteredTransaction
が作成されてdbに保存されます。 Transaction is registered
と共に、新しく作成されたtransaction_id
とともにイベントが公開されます。このトランザクションが不正であるかどうかを確認したいリスナーによって消費されます。たとえば、サードパーティのサービスをリクエストする必要があり、問題がなければ、CheckedForFraudTransaction
が作成されます。内部的には、同じtransaction_id
を使用して(必要ではありませんが)同じdbテーブル行を操作します。
私はこのアプローチが好きです。新しい動作が追加されるたびにState
のインターフェイスを変更する必要はありません。さらに、各ステップは十分に分離されており、統合テスト中にモックされる可能性があり、ビジネスプロセス全体がより明確になります。
ビルドしようとしているのはステートマシンであり、akka .Net Frameworkはこの機能を非常にきれいに提供します。
これを読んでください link とそこにあるパターンがあなたの質問に答えます
すべての新しい状態の実装では、他のすべての実装を変更する必要があるため、実装は オープン/クローズの原則 に違反します。 戦略パターン に基づく別のアプローチを提案しています。
つまり、あなたのTenant
はいつでもState
にあり、すべてのState
はそれが遷移する可能性のある状態のみを知っています。これにより、Tenant
の状態遷移アルゴリズムが解放されます。ただし、Tenant
は状態遷移を制御するものであり、制御下にあるものです。
一方、States
はTenant
について知っているべきではありません。別の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
}