Come implementare lo schema della macchina dello stato sulla radice aggregata

6

Sto modellando una radice aggregata, che ha diverse azioni che eseguono operazioni contro altre entità, come ci si aspetterebbe. L'aggregato, tuttavia, ha uno stato e molte di queste operazioni possono essere eseguite solo quando l'aggregato si trova in uno stato particolare.

Ho creato un'implementazione del modello di stato, in modo che l'aggregato delegasse semplicemente l'azione all'oggetto concreto di stato. Tuttavia, ora che l'ho implementato, mi sono trovato con le seguenti preoccupazioni:

  • Ci sono operazioni che possono essere invocate in più di uno stato, quindi ho finito per ripetere le implementazioni.
  • Ci sono operazioni che generano eventi di dominio, quindi ho dovuto passare la raccolta di eventi della root in modo che possano aggiungere gli eventi correttamente.
  • Alcune operazioni richiedono l'accesso ai membri privati della radice aggregata, quindi ho finito per dichiararle come interne (C #) o per creare metodi interni che modificano i membri privati.

Quindi ora mi chiedo se l'implementazione è valsa la pena o se l'oggetto stato deve avere solo proprietà CanPerformOperation1 e lasciare che la radice aggregata controlli questa proprietà e, se false, lancia una InvalidOperationException.

Il seguente codice è un riassunto di ciò che sto tentando di provare.

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
}

Ora, ciò aumenterà solo di complessità, poiché il cliente mi richiede di aggiungere altri metodi che dipendono dallo stato. Quindi mi stavo chiedendo se dovrei semplicemente aggiungere proprietà come CanRegisterApplication, CanRegisterLicense, ecc., E quindi gli stati agiranno solo come flag switch.

Quale sarebbe il modo corretto di implementare ciò che sto cercando di ottenere? O forse sto ricevendo il modello di stato sbagliato?

    
posta Fernando Gómez 30.01.2018 - 01:04
fonte

4 risposte

2

IMHO, "il cliente mi richiede di aggiungere più metodi" è la ragione esatta per cui abbiamo bisogno del modello di stato in primo luogo. Senza pattern di stato, è probabile che tu debba ripetere il switch o if / else nei nuovi metodi aggiunti.

Dalla vista del punto di implementazione, la classe astratta è molto più semplice dell'interfaccia nel modello di stato.

Per quanto riguarda il tuo modificatore di accesso, puoi rendere ogni singolo stato come classe interna del client (classe Tenant nel tuo caso, vedi tipi annidati ). In questo modo, hai effettivamente un incapsulamento migliore in quanto il mondo esterno non deve conoscere i dettagli dello stato effettivo.

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
}

P.S: In java, le persone di solito usano enum per implementare pattern di stato. Tuttavia, .net non consente il polimorfismo in enum.

    
risposta data 30.01.2018 - 02:40
fonte
4

Spesso incontro lo stesso problema: ho alcuni processi aziendali che richiedono il mio aggregato per passare attraverso diversi stati, con qualche comportamento specifico per ogni stato:

Quello che faccio spesso è dividere questo aggregato con tutte le sue classi di stato in più classi separate, ognuna rappresentante un aggregato nel suo stato specifico. Ad esempio, anziché un FinancialTransaction aggregato con stati Registered , CheckedForFraud , Authorized , ecc., Ho RegisteredTransaction , CheckedForFraudTransaction , AuthorizedTransaction , ecc.

Solo per renderlo cristallino, è così che funziona. Innanzitutto, il comando viene emesso dal client. Potrebbe essere smth come Register transaction . Il comando viene convalidato, RegisteredTransaction viene creato e salvato in db. Oltre all'evento Transaction is registered viene pubblicato il transaction_id appena creato. Viene utilizzato da un ascoltatore che vuole verificare se questa transazione è fraudolenta. Supponiamo che richieda qualche servizio di terze parti e, se è ok, viene creato CheckedForFraudTransaction . Sotto il cofano funziona sulla stessa riga del db table (anche se non è necessario) con lo stesso transaction_id .

Mi piace questo approccio, non mi fa modificare l'interfaccia di State ogni volta che viene aggiunto un nuovo comportamento. Inoltre, ogni fase è ben isolata, potrebbe essere derisa durante i test di integrazione e l'intero processo aziendale è più chiaro.

    
risposta data 30.01.2018 - 08:20
fonte
0

La tua implementazione rompe il principio Aperto / chiuso perché ogni nuova implementazione dello stato ti obbliga a modificare tutte le altre implementazioni . Sto proponendo un approccio alternativo, basato sul modello di strategia .

L'idea è che il tuo Tenant in un dato momento sia in State e ogni State conosca solo gli stati a cui può passare. Questo libera il tuo Tenant dell'algoritmo di transizione dello stato. Tuttavia, Tenant è quello che comanda la transizione di stato, è quello in controllo.

D'altra parte, States non dovrebbe conoscere Tenant in quanto ha una sola responsabilità: autorizzare o rifiutare la transizione a un altro State .

Seguendo questo modello, Tenant diventa molto pulito.

Ecco un esempio di implementazione (in 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
}
    
risposta data 31.01.2018 - 19:38
fonte
0

Quello che stai cercando di costruire è State Machine e akka .Net Framework fornisce questa funzionalità in modo molto accurato.

Leggi questo link e i pattern lì risponderanno alle tue domande

    
risposta data 01.02.2018 - 08:47
fonte

Leggi altre domande sui tag