Motivo di progettazione per "operazione su oggetto consentito, solo se l'oggetto si trova in un determinato stato"

8

Ad esempio:

Only job applications that are not yet in review or approved, can be updated. In other words, a person can update his job appliance form until HR starts reviewing it, or it's already acepted.

Quindi un'applicazione di lavoro può essere in 4 stati:

APPLIED(initial state), IN_REVIEW, APPROVED, DECLINED

Come posso ottenere un simile comportamento?

Surely I can write an update() method in Application class, check Application state, and do nothing or throw exception if Application is not in required state

Ma questo tipo di codice non rende evidente una regola del genere, consente a chiunque di chiamare il metodo update (), e solo dopo aver fallito un client sa che tale operazione non è consentita. Pertanto, il cliente deve essere consapevole che un tale tentativo potrebbe fallire, quindi fare attenzione. Il fatto che il cliente sia consapevole di queste cose significa anche che la logica sta fuoriuscendo all'esterno.

Ho provato a creare classi diverse per ogni stato (ApprovedApplication, ecc.) e mettere le operazioni consentite solo sulle classi consentite, ma questo tipo di approccio sembra sbagliato.

Esiste un modello di progettazione ufficiale o una semplice parte di codice per implementare un simile comportamento?

    
posta Reek 25.12.2015 - 11:25
fonte

7 risposte

4

Questo tipo di situazione si presenta abbastanza spesso. Ad esempio, i file possono essere manipolati solo mentre sono aperti, e se provi a fare qualcosa con un file dopo che è stato chiuso, ottieni un'eccezione di runtime.

Il tuo desiderio ( espresso nella domanda precedente ) di utilizzare il sistema di tipi della lingua in modo da garantire che la cosa sbagliata non può nemmeno accadere è nobile, dal momento che gli errori in fase di compilazione sono sempre preferibili rispetto agli errori di runtime. Tuttavia, non esiste un modello di progettazione che io conosca per questo tipo di situazione, probabilmente perché finirebbe per causare più problemi di quanti ne risolverebbe. (Sarebbe poco pratico.)

La cosa più vicina alla tua situazione che conosco è quella di modellare diversi stati di un oggetto che corrispondono a capacità diverse tramite interfacce aggiuntive, ma in questo modo si riduce solo il numero di posti nel codice in cui può verificarsi un errore di runtime, non si sta eliminando la possibilità di un errore di runtime.

Quindi, nella tua situazione, dichiareresti un numero di interfacce che descrivono cosa può essere fatto con il tuo oggetto nei suoi vari stati, e il tuo oggetto restituirebbe un riferimento all'interfaccia giusta su una transizione di stato.

Quindi, ad esempio, il metodo approve() della tua classe restituirebbe un'interfaccia ApprovedApplication . L'interfaccia dovrebbe essere implementata privatamente (tramite una classe nidificata), quindi il codice che ha solo un riferimento a Application non può richiamare nessuno dei metodi ApprovedApplication . Quindi, il codice che manipola un'applicazione approvata dichiara esplicitamente la sua intenzione di farlo al momento della compilazione, richiedendo un ApprovedApplication con cui lavorare. Ma naturalmente, se si memorizza questa interfaccia da qualche parte, e quindi si utilizza questa interfaccia dopo aver invocato il metodo decline() , si otterrà comunque un errore di runtime. Non penso che ci sia una soluzione perfetta al tuo problema.

    
risposta data 25.12.2015 - 15:07
fonte
3

Sto annuendo con la testa su diversi bit delle varie risposte, ma l'OP sembra avere ancora la preoccupazione del controllo di flusso. C'è troppo da provare per fondere le parole. Sto solo andando a destra del codice - The State Pattern.

Nomi di stato come passato

"In_Review" non è forse uno stato ma una transizione o un processo. In caso contrario, i nomi dei tuoi stati dovrebbero essere coerenti: "Applicare", "Approvare", "Rifiutare", ecc. O avere anche "Revisionato". Oppure no.

Lo stato applicato esegue una transizione di revisione e imposta lo stato su Rivisto. Lo stato revisionato esegue una transizione di approvazione e imposta lo stato su Approvato (o Rifiutato).

// Application class encapsulates state transition,
// the client is unable to directly set state.
public class Application {
    State currentState = null;

    State AppliedState    = new Applied(this);
    State DeclinedState   = new Declined(this);
    State ApprovedState   = new Approved(this);
    State ReviewedState   = new Reviewed(this);

    public class Application (ApplicationDocument myApplication) {
        if(myApplication != null && isComplete()) {
            currentState = AppliedState;
        } else {            
            throw new ArgumentNullException ("Your application is incomplete");
            // some kind of error communication would probably be better
        }
    }

    public apply()    { currentState.apply(); }
    public review()   { currentState.review(); }
    public approve()  { currentState.approve(); }
    public decline()  { currentState.decline(); }


    //These could be done via an enum. I like enums!
    protected void setSubmittingState() {}
    protected void setApproveState() {}
    // etc. ...
}

// could be an interface if we don't have any default or base behavior.
public abstract class State {   
    protected Application theApp;
    // maybe these return an object communicating errors / error state.
    public abstract void apply();
    public abstract void review();
    public abstract void accept();
    public abstract void decline();
}

public class Applied implements State {
    public Applied (Application newApp) {
        if(newApp != null)
            theApp = newApp;
        else
            throw new ArgumentNullException ("null application argument");
     }

    public override void apply() {
        // whatever is appropriate when already in "applied" state
        // do not do any work on behalf of other states!
        // throwing exceptions here is not appropriate, as others
        // have said.
      }

    public override void review() {
        if(recursiveBureaucracyBuckPassing())
            theApp.setReviewedState();
    }

    public override void decline() { // ditto  }
}

public class Reviewed implements State {}
public class Approved implements State {}
public class Declined implements State {}

Modifica - Commenti sulla gestione degli errori

Un commento recente:

... if you are attempting to loan a book which is already issued to somebody else the Book model will contain the logic to prevent it's state from changing. This might be via a return value (e.g. a boolean successful yay/nay, or status code) or an exception (e.g. IllegalStateChangeException) or some other means. Irrespective of the means chosen, this aspect is not covered as part of this (or any) answer.

E dalla domanda originale:

But this kind of code does not make it obvious such a rule exists, it allows anyone to call update() method, and only after failing a client knows such operation was not permitted.

C'è più lavoro di progettazione da fare. Non c'è Unified Field Theory Pattern . La confusione deriva dal presupposto che il framework di transizione dello stato esegua le funzioni generali dell'applicazione e la gestione degli errori. Questo sembra sbagliato perché lo è. La risposta mostrata è progettata per controllare il cambiamento di stato.

Surely I can write an update() method in Application class, check Application state, and do nothing or throw exception if Application is not in required state

Questo suggerisce che ci sono tre funzionalità che funzionano qui: lo stato, l'aggiornamento e l'interazione dei due. In questo caso Application non è il codice che ho scritto. Può usarlo per determinare lo stato corrente. Application non è neanche applicationPaperwork . Application non è l'interazione dei due, ma potrebbe essere una classe generale StateContextEvaluator . Ora Application orchestrerà queste interazioni tra componenti e quindi agirà di conseguenza, come se emettesse un messaggio di errore.

Termina modifica

    
risposta data 26.12.2015 - 03:26
fonte
1

In generale, ciò che stai descrivendo è un flusso di lavoro . Più specificamente, le funzioni aziendali che sono incorporate da stati come APPROVATI RECENTI o DECLINATI rientrano nella categoria "regole aziendali" o "logica aziendale" ".

Ma per essere chiari, le regole aziendali non dovrebbero essere codificate in eccezioni. Fare ciò significherebbe usare le eccezioni per il controllo del flusso del programma, e ci sono molte buone ragioni per le quali non dovresti farlo. Le eccezioni dovrebbero essere utilizzate per condizioni eccezionali e lo stato NON VALIDO di un'applicazione è del tutto non eccezionale dal punto di vista aziendale.

Utilizza le eccezioni nei casi in cui il programma non può ripristinare da una condizione di errore senza l'intervento dell'utente ("file non trovato", ad esempio).

Non esiste uno schema specifico per la scrittura della logica aziendale, se non le solite tecniche per organizzare sistemi di elaborazione dati aziendali e scrivere codice per implementare i tuoi processi. Se le regole aziendali e il flusso di lavoro sono elaborati, considera l'utilizzo di una sorta di server del flusso di lavoro o di un motore di regole aziendali.

In ogni caso, gli stati REVISIONE, APPROVATO, DECLINATO, ecc. possono essere rappresentati da una variabile di tipo enum nella tua classe. Se si utilizzano metodi getter / setter, è possibile controllare se i setter consentiranno o meno le modifiche esaminando prima il valore della variabile enum. Se qualcuno prova a scrivere su un setter quando il valore enum è sbagliato, allora puoi lanciare un'eccezione.

    
risposta data 25.12.2015 - 13:44
fonte
1

Application potrebbe essere un'interfaccia e potresti avere un'implementazione per ciascuno degli stati. L'interfaccia potrebbe avere un metodo moveToNextState() , e ciò nasconderebbe tutta la logica del flusso di lavoro.

Per le esigenze del cliente, potrebbe esserci anche un metodo che restituisce direttamente ciò che è possibile fare e non (cioè un insieme di booleani), anziché solo lo stato, in modo che non sia necessario un "elenco di controllo" nel client (Presumo che il client sia comunque un controller MVC o un'interfaccia utente).

Tuttavia, anziché lanciare un'eccezione, non puoi fare nulla e registrare il tentativo. Questo è sicuro in fase di esecuzione, le regole sono state applicate e il client ha avuto modo di nascondere i controlli di "aggiornamento".

    
risposta data 25.12.2015 - 16:01
fonte
1

Un approccio a questo problema che ha avuto un enorme successo in natura è ipermediale: la rappresentazione dello stato dell'entità è accompagnata da controlli ipermediali che descrivono i tipi di transizioni attualmente consentiti. Il consumatore interroga i controlli per scoprire cosa può essere fatto.

È una macchina a stati, con un'interfaccia nell'interfaccia che ti consente di scoprire quali eventi è possibile attivare.

In altre parole: stiamo descrivendo il web (REST).

Un altro approccio consiste nel prendere la tua idea di diverse interfacce per stati diversi e fornire una query che ti permetta di rilevare quali interfacce sono attualmente disponibili. Think IUnknown :: QueryInterface o down casting. Il codice client riproduce Mother May I con lo stato per scoprire cosa è consentito.

È essenzialmente lo stesso schema: basta usare un'interfaccia per rappresentare i controlli ipermediali.

    
risposta data 19.05.2016 - 19:08
fonte
1

Ecco un esempio di come potresti affrontarlo da una prospettiva funzionale e in che modo aiuta ad evitare le potenziali insidie. Sto lavorando in Haskell, che presumo tu non sappia, quindi lo spiegherò nei dettagli mentre procedo.

data Application = Applied ApplicationDetails |
                   InReview ApplicationDetails |
                   Approved ApplicationDetails |
                   Declined ApplicationDetails

Definisce un tipo di dati che può trovarsi in uno dei quattro stati corrispondenti agli stati dell'applicazione. Si presume che ApplicationDetails sia un tipo esistente che contiene le informazioni dettagliate.

newtype UpdatableApplication = UpdatableApplication Application

Un alias di tipo che richiede conversione esplicita da e verso Application . Ciò significa che se definiamo la seguente funzione che accetta e scartina un UpdatableApplication e fa qualcosa di utile con esso,

updateApplication :: UpdatableApplication -> ApplicationDetails -> Application
updateApplication (UpdatableApplication app) details = ...

quindi dobbiamo convertire esplicitamente l'applicazione in una UpdatableApplication prima che possiamo usarla. Questo viene fatto usando questa funzione:

findUpdatableApplication :: Application -> Maybe UpdatableApplication
findUpdatableApplication app@(Applied _) = Just (UpdatableApplication app)
findUpdatableApplication _               = Nothing

Qui facciamo tre cose interessanti:

  • Controlliamo lo stato dell'applicazione (usando la corrispondenza dei pattern, che è veramente utile per questo tipo di codice), e
  • se può essere aggiornato, lo avvolgiamo in un UpdatableApplication (che riguarda solo una nota tipo compilazione del cambiamento di tipo che viene aggiunto, poiché Haskell ha una caratteristica specifica per fare questo tipo di trucco a livello di tipo, non costa nulla in fase di esecuzione) e
  • restituiamo il risultato in un "Forse" (simile a Option in C # o Optional in Java: è un oggetto che racchiude un risultato che potrebbe mancare).

Ora, per metterlo insieme, dobbiamo chiamare questa funzione e, se il risultato ha successo, passarlo alla funzione di aggiornamento ...

case findUpdatableApplication application of
    Just updatableApplication -> do
        storeApplicationInDatabase (updateApplication updatableApplication)
        showConfirmationPage
    Nothing -> do
        showErrorPage

Poiché la funzione updateApplication ha bisogno dell'oggetto avvolto, non possiamo dimenticare di controllare le precondizioni. E poiché la funzione di controllo delle precondizioni restituisce l'oggetto avvolto all'interno di un oggetto Maybe , non possiamo dimenticare di controllare il risultato e rispondere di conseguenza se non ha funzionato.

Ora ... tu potresti fare questo in un linguaggio orientato agli oggetti. Ma è meno conveniente:

  • Nessuno dei linguaggi OO che ho provato ha una sintassi semplice per creare un tipo di wrapper sicuro dal tipo di codice, quindi è semplice.
  • Sarà anche meno efficiente, perché almeno per la maggior parte delle lingue non sarà in grado di eliminare il tipo di wrapper, poiché sarà necessario esistere ed essere rilevabile in fase di esecuzione (Haskell non ha il controllo del tipo di runtime, tutto i controlli di tipo vengono eseguiti in fase di compilazione).
  • Mentre alcuni linguaggi OO hanno tipi equivalenti a Maybe , di solito non hanno un modo conveniente di estrarre i dati e scegliere il percorso da intraprendere allo stesso tempo. Anche la corrispondenza dei pattern è molto utile qui.
risposta data 20.05.2016 - 02:28
fonte
1

Potresti usare il pattern «comando» e poi chiedere a Invoker di fornire un elenco di funzioni valide in base allo stato della classe ricevente.

Ho usato lo stesso per fornire funzionalità a diverse interfacce che dovevano chiamare il mio codice, alcune delle opzioni non erano disponibili a seconda dello stato attuale del record, quindi il mio invoker ha aggiornato l'elenco e in quel modo ogni GUI ha chiesto all'Invoker quali opzioni erano disponibili e si sono dipinte di conseguenza.

    
risposta data 19.05.2016 - 20:30
fonte