Computer finestre Internet

Stato. Classi statali per ogni stato

Modello di progettazione comportamentale. Viene utilizzato nei casi in cui, durante l'esecuzione del programma, un oggetto deve cambiare il suo comportamento a seconda del suo stato. L'implementazione classica prevede la creazione di una classe o interfaccia astratta di base contenente tutti i metodi e una classe per ogni possibile stato. Il modello è un caso speciale della raccomandazione "sostituisci le istruzioni condizionali con il polimorfismo".

Sembrerebbe che tutto sia secondo il libro, ma c'è una sfumatura. Come implementare correttamente metodi che non sono rilevanti per un determinato stato? Ad esempio, come rimuovere un articolo da un carrello vuoto o pagare un carrello vuoto? In genere, ogni classe di stato implementa solo i metodi rilevanti e genera InvalidOperationException negli altri casi.

Violazione del principio di sostituzione di Liskov con una persona. Yaron Minsky ha suggerito un approccio alternativo: rendere irrappresentabili gli stati illegali. Ciò rende possibile spostare il controllo degli errori dal runtime al tempo di compilazione. Tuttavia, il flusso di controllo in questo caso sarà organizzato in base alla corrispondenza dei modelli e non all'utilizzo del polimorfismo. Fortunatamente, .

Maggiori dettagli sull'esempio dell'argomento F# rendere irrappresentabili gli stati illegali rivelato sul sito web di Scott Vlashin.

Consideriamo l'implementazione dello “stato” usando l'esempio di un paniere. C# non ha un tipo di unione incorporato. Separiamo dati e comportamenti. Codificheremo lo stato stesso utilizzando enum e il comportamento come una classe separata. Per comodità, dichiariamo un attributo che collega l'enumerazione e la classe di comportamento corrispondente, la classe "stato" di base, e aggiungiamo un metodo di estensione per passare dall'enumerazione alla classe di comportamento.

Infrastruttura

public class StateAttribute: Attribute ( public Type StateType ( get; ) public StateAttribute(Type stateType) ( StateType = stateType ?? lancia new ArgumentNullException(nameof(stateType)); ) ) public abstract class State dove T: classe ( Stato protetto (entità T) ( Entità = entità ?? lancia nuova ArgumentNullException (nome di (entità)); ) Entità T protetta ( get; ) ) classe statica pubblica StateCodeExtensions ( Stato statico pubblico AlloStato (questo Enum stateCode, entità oggetto) dove T: class // sì, sì la riflessione è lenta. Sostituisci con l'albero delle espressioni compilato // o IL Emit e sarà veloce => (State ) Activator.CreateInstance(stateCode .GetType() .GetCustomAttribute ().StateType, entità); )

Argomento

Dichiariamo l'entità "carrello":

Interfaccia pubblica IHasState dove TEntity: classe ( TStateCode StateCode ( get; ) Stato State ( get; ) ) classe parziale pubblica Carrello: IHasState ( public Utente Utente ( get; protected set; ) public CartStateCode StateCode ( get; protected set; ) public Stato Stato => StateCode.ToState (Questo); public decimal Total ( get; protected set; ) ICollection virtuale protetta Prodotti (get; set; ) = nuova Lista (); // Solo ORM protetto Cart() ( ) public Cart(Utente utente) ( Utente = utente ?? lancia new ArgumentNullException(nome(utente)); StateCode = StateCode = CartStateCode.Empty; ) public Cart(Utente utente, IEnumerable Prodotti): this(utente) ( StateCode = StateCode = CartStateCode.Empty; foreach (var prodotto in prodotti) ( Products.Add(prodotto); ) ) public Cart(Utente utente, IEnumerable Prodotti, totale decimale): this(utente, prodotti) ( if (totale<= 0) { throw new ArgumentException(nameof(total)); } Total = total; } }
Implementeremo una classe per ogni stato del carrello: vuoto, attivo e pagato, ma non dichiareremo un'interfaccia comune. Lascia che ogni stato implementi solo il comportamento rilevante. Ciò non significa che le classi VacuumCartState, ActiveCartState e PaidCartState non possano implementare tutte la stessa interfaccia. Possono, ma tale interfaccia deve contenere solo metodi disponibili in ciascuno stato. Nel nostro caso, il metodo Add è disponibile in VacuumCartState e ActiveCartState, quindi possiamo ereditarli dall'astratto AddableCartStateBase. Tuttavia, puoi aggiungere articoli solo a un carrello non pagato, quindi non ci sarà un'interfaccia comune per tutti gli stati. In questo modo garantiamo che non ci sia InvalidOperationException nel nostro codice in fase di compilazione.

Classe parziale pubblica Cart ( enum pubblica CartStateCode: byte ( Vuoto, Attivo, Pagato ) interfaccia pubblica IAddableCartState ( ActiveCartState Add(Prodotto prodotto); IEnumerable Prodotti ( get; ) ) interfaccia pubblica INotEmptyCartState ( IEnumerable Prodotti ( get; ) decimal Total ( get; ) ) classe astratta pubblica AddableCartState: Stato , IAddableCartState ( protected AddableCartState(entità carrello): base(entità) ( ) public ActiveCartState Add(prodotto prodotto) ( Entity.Products.Add(prodotto); Entity.StateCode = CartStateCode.Active; return (ActiveCartState)Entity.State; ) public IEnumerable Prodotti => Entità.Prodotti; ( Entity.Total = totale; Entity.StateCode = CartStateCode.Paid; return (PaidCartState)Entity.State; ) Stato pubblico Rimuovi(Prodotto prodotto) ( Entity.Products.Remove(prodotto); if(!Entity.Products.Any()) ( Entity.StateCode = CartStateCode.Empty; ) return Entity.State; ) public VuotoCartState Clear() ( Entity. Prodotti.Clear(); Entity.StateCode = CartStateCode.Empty; return (EmptyCartState)Entity.State; ) decimale pubblico Totale => Prodotti.Sum(x => x.Prezzo); ) classe pubblica PaidCartState: Stato , INotEmptyCartState ( public IEnumerable Prodotti => Entità.Prodotti; pubblico decimale Totale => Entità.Totale; public PaidCartState(entità Carrello): base(entità) ( ) ) )
Gli stati sono dichiarati nidificati ( nidificato) le classi non sono casuali. Le classi nidificate hanno accesso ai membri protetti della classe Cart, il che significa che non dobbiamo sacrificare l'incapsulamento delle entità per implementare il comportamento. Per evitare confusione nel file della classe di entità, ho diviso la dichiarazione in due: Cart.cs e CartStates.cs utilizzando la parola chiave partial.

Risultato azione pubblica GetViewResult(State cartState) ( switch (cartState) ( case Cart.ActiveCartState activeState: return View("Active", activeState); case Cart.EmptyCartState emptyState: return View("Empty", emptyState); case Cart.PaidCartStatepaidCartState: return View(" Paid",paidCartState); impostazione predefinita: lancia new InvalidOperationException(); ) )
A seconda dello stato del carrello, utilizzeremo visualizzazioni diverse. Per un carrello vuoto, verrà visualizzato il messaggio "il tuo carrello è vuoto". Il carrello attivo conterrà un elenco di prodotti, la possibilità di modificare il numero di prodotti e rimuoverne alcuni, un pulsante “effettua un ordine” e l'importo totale dell'acquisto.

Il carrello a pagamento avrà lo stesso aspetto del carrello attivo, ma senza la possibilità di modificare nulla. Questo fatto può essere notato evidenziando l'interfaccia INotEmptyCartState. Pertanto, non solo ci siamo sbarazzati della violazione del principio di sostituzione di Liskov, ma abbiamo anche applicato il principio di separazione delle interfacce.

Conclusione

Nel codice dell'applicazione, possiamo utilizzare i collegamenti dell'interfaccia IAddableCartState e INotEmptyCartState per riutilizzare il codice responsabile dell'aggiunta di articoli al carrello e della visualizzazione degli articoli nel carrello. Credo che la corrispondenza dei modelli sia adatta solo per il flusso di controllo in C# quando non c'è nulla in comune tra i tipi. In altri casi, è più conveniente lavorare con il collegamento di base. Una tecnica simile può essere utilizzata non solo per codificare il comportamento di un'entità, ma anche per i file .

È tempo di una confessione: con questo principale ho esagerato un po'. Doveva riguardare il modello di progettazione GoF State. Ma non posso parlare del suo utilizzo nei giochi senza toccare il concetto macchine a stati finiti(o "FSM"). Ma una volta che ci sono entrato, ho capito che avrei dovuto ricordare macchina statale gerarchica O automa gerarchico E macchina automatica con memoria del magazzino (automi pushdown).

Si tratta di un argomento molto ampio, quindi per mantenere questo capitolo il più breve possibile, tralascerò alcuni esempi di codice ovvi e dovrai riempire tu stesso alcune lacune. Spero che questo non li renda meno comprensibili.

Non c'è bisogno di arrabbiarsi se non hai mai sentito parlare di macchine a stati finiti. Sono ben noti agli sviluppatori di intelligenza artificiale e agli hacker informatici, ma poco conosciuti in altri campi. Secondo me meritano più riconoscimento, quindi voglio mostrarti alcuni dei problemi che risolvono.

Questi sono tutti echi dei vecchi tempi dell’intelligenza artificiale. Negli anni ’50 e ’60 l’intelligenza artificiale si concentrava principalmente sull’elaborazione delle strutture linguistiche. Molte delle tecnologie utilizzate nei compilatori moderni sono state inventate per analizzare i linguaggi umani.

Siamo stati tutti lì

Diciamo che stiamo lavorando su un piccolo platform a scorrimento laterale. Il nostro compito è modellare un'eroina che sarà l'avatar del giocatore nel mondo di gioco. Ciò significa che deve rispondere all'input dell'utente. Premi B e lei salterà. Abbastanza semplice:

void Heroine::handleInput(Input input) ( if (input == PRESS_B) ( yVelocity_ = JUMP_VELOCITY; setGraphics(IMAGE_JUMP); ) )

Hai notato un bug?

Non esiste un codice qui per impedire di "saltare in aria"; continua a premere B mentre è in aria e volerà in alto ancora e ancora. Il modo più semplice per risolvere questo problema è aggiungere un flag booleano isJumping_ a Heroine, che terrà traccia di quando l'eroina ha saltato:

void Heroine::handleInput(Input input) ( if (input == PRESS_B) ( if (!isJumping_) ( isJumping_ = true ; // Salta... ) ) )

Abbiamo anche bisogno di un codice che reimposti isJumping_ su false quando l'eroina tocca di nuovo il suolo. Per semplicità, ometto questo codice.

void Heroine::handleInput(Input input) ( if (input == PRESS_B) ( // Facciamo un salto se non l'abbiamo già fatto...) else if (input == PRESS_DOWN) ( if (!isJumping_) ( setGraphics(IMAGE_DUCK); ) ) else if (input == RELEASE_DOWN) ( setGraphics(IMAGE_STAND); ) )

Hai notato un bug qui?

Con questo codice il giocatore può:

  1. Premi verso il basso per accovacciarti.
  2. Premi B per saltare da una posizione seduta.
  3. Rilascia mentre sei in aria.

Allo stesso tempo, l'eroina passerà alla grafica in cui si trova in aria. Dovremo aggiungere un'altra bandiera...

void Heroine::handleInput(Input input) ( if (input == PREMI_B) ( if (!isJumping_ && !isDucking_) ( // Salta... ) ) else if (input == PREMI_GIÙ) ( if (!isJumping_) ( isDucking_ = true ; setGraphics(IMAGE_DUCK); ) ) else if (input == RELEASE_DOWN) ( if (isDucking_) ( isDucking_ = false ; setGraphics(IMAGE_STAND); ) ) )

Ora sarebbe fantastico aggiungere la capacità dell'eroina di attaccare con un contrasto quando il giocatore preme mentre è in aria:

void Heroine::handleInput(Input input) ( if (input == PREMI_B) ( if (!isJumping_ && !isDucking_) ( // Salta... ) ) else if (input == PREMI_GIÙ) ( if (!isJumping_) ( isDucking_ = true ; setGraphics(IMAGE_DUCK); ) else ( isJumping_ = false ; setGraphics(IMAGE_DIVE); ) ) else if (input == RELEASE_DOWN) ( if (isDucking_) ( // In piedi... ) ) )

Alla ricerca di bug di nuovo. Trovato?

Abbiamo un controllo per rendere impossibile saltare in aria, ma non durante un contrasto. Aggiunta di un'altra bandiera...

C'è qualcosa di sbagliato in questo approccio. Ogni volta che tocchiamo il codice, qualcosa si rompe. Dovremo aggiungere un sacco di movimento in più, non ne abbiamo nemmeno a piedi no, ma con questo approccio dovremo superare un sacco di bug in più.

I programmatori che tutti idealizziamo e che creano ottimo codice non sono affatto dei superuomini. Hanno semplicemente sviluppato un istinto per il codice che minaccia di introdurre errori e cercano di evitarlo quando possibile.

Ramificazioni complesse e cambiamenti di stato sono esattamente i tipi di codice che dovresti evitare.

Le macchine a stati finiti sono la nostra salvezza

In un impeto di frustrazione, rimuovi tutto dalla scrivania tranne carta e matita e inizi a disegnare un diagramma di flusso. Disegniamo un rettangolo per ogni azione che l'eroina può eseguire: stare in piedi, saltare, accovacciarsi e rotolare. In modo che possa rispondere alla pressione dei tasti in qualsiasi stato, disegniamo delle frecce tra questi rettangoli, etichettiamo i pulsanti sopra di essi e colleghiamo gli stati insieme.

Congratulazioni, hai appena creato macchina statale (macchina a stati finiti). Provengono da un campo dell'informatica chiamato teoria degli automi (teoria degli automi), alla cui famiglia di strutture appartiene anche la celebre macchina di Turing. FSM è il membro più semplice di questa famiglia.

La conclusione è questa:

    Abbiamo un set fisso stati, che può contenere una mitragliatrice. Nel nostro esempio si tratta di stare in piedi, saltare, accovacciarsi e rotolare.

    La macchina può solo essere dentro uno stato in qualsiasi momento. La nostra eroina non può saltare e stare in piedi allo stesso tempo. In realtà, l’FSM viene utilizzato in primo luogo per prevenire ciò.

    Sotto sequenza ingresso O eventi, trasmesso alla macchina. Nel nostro esempio, si tratta di premere e rilasciare i pulsanti.

    Ogni stato ha insieme di transizione, ognuno dei quali è associato ad un ingresso e indica uno stato. Quando si verifica l'input dell'utente, se corrisponde allo stato corrente, la macchina cambia il suo stato nel punto in cui punta la freccia.

    Ad esempio, se premi mentre sei in piedi, passerai allo stato accovacciato. Premendo verso il basso mentre si salta si cambia lo stato in placcaggio. Se nello stato corrente non è prevista alcuna transizione per l'input, non accade nulla.

Nella sua forma più pura, questa è l'intera banana: stati, input e transizioni. Puoi rappresentarli sotto forma di un diagramma a blocchi. Sfortunatamente, il compilatore non capirà tali scarabocchi. E allora? strumento macchina a stati finiti? The Gang of Four offre la sua versione, ma inizieremo con una ancora più semplice.

La mia analogia FSM preferita è la vecchia ricerca testuale Zork. Hai un mondo composto da stanze collegate da passaggi. E puoi esplorarli inserendo comandi come "vai a nord".

Tale mappa corrisponde pienamente alla definizione di macchina a stati finiti. La stanza in cui ti trovi è lo stato attuale. Ogni uscita da una stanza è una transizione. Comandi di navigazione - input.

Enumerazioni e commutazioni

Uno dei problemi con la nostra vecchia classe Heroine è che consente una combinazione errata di chiavi booleane: isJumping_ e isDucking_ , non possono essere vere allo stesso tempo. E se hai diversi flag booleani, solo uno dei quali può essere true , non sarebbe meglio sostituirli tutti con enum .

Nel nostro caso, utilizzando enum possiamo descrivere completamente tutti gli stati della nostra FSM in questo modo:

enum Stato ( STATE_STANDING, STATE_JUMPING, STATE_DUCKING, STATE_DIVING );

Invece di un mucchio di flag, Heroine ha solo un campo state_. Dovremo anche cambiare l'ordine di ramificazione. Nell'esempio di codice precedente, abbiamo effettuato ramificazioni prima in base all'input e poi allo stato. Così facendo, abbiamo raggruppato il codice in base al pulsante premuto, ma abbiamo offuscato il codice associato agli stati. Ora faremo il contrario e cambieremo l'ingresso a seconda dello stato. Questo è ciò che otteniamo:

void Heroine::handleInput(Input input) ( switch (state_) ( case STATE_STANDING: if (input == PRESS_B) ( state_ = STATE_JUMPING; yVelocity_ = JUMP_VELOCITY; setGraphics(IMAGE_JUMP); ) else if (input == PRESS_DOWN) ( state_ = STATE_DUCKING; setGraphics(IMAGE_DUCK); ) break ; case STATE_JUMPING: if (input == PRESS_DOWN) ( state_ = STATE_DIVING; setGraphics(IMAGE_DIVE); ) break ; case STATE_DUCKING: if (input == RELEASE_DOWN) ( state_ = STATE_STANDING; setGraphics (IMAGE_STAND); ) pausa ; ) )

Sembra abbastanza banale, ma nonostante ciò questo codice è già molto migliore del precedente. Abbiamo ancora alcune ramificazioni condizionali, ma abbiamo semplificato lo stato mutabile in un singolo campo. Tutto il codice che gestisce un singolo stato è raccolto in un unico posto. Questo è il modo più semplice per implementare una macchina a stati finiti e talvolta è abbastanza sufficiente.

Adesso l'eroina non potrà più esserci incerto condizione. Quando si utilizzavano i flag booleani, alcune combinazioni erano possibili, ma non avevano senso. Quando si utilizza enum tutti i valori sono corretti.

Sfortunatamente, il tuo problema potrebbe superare questa soluzione. Diciamo che abbiamo voluto aggiungere un attacco speciale alla nostra eroina, per il quale l'eroina ha bisogno di sedersi per ricaricarsi e poi scaricare l'energia accumulata. E mentre siamo seduti dobbiamo tenere d’occhio il tempo di ricarica.

Aggiungi un campo chargeTime_ a Heroine per memorizzare il tempo di ricarica. Diciamo che abbiamo già un metodo update() chiamato su ogni frame. Aggiungiamo ad esso il seguente codice:

void Heroine::update() ( if (state_ == STATE_DUCKING) ( chargeTime_++; if (chargeTime_ > MAX_CHARGE) ( superBomb(); ) ) )

Se hai indovinato il modello del metodo di aggiornamento, hai vinto un premio!

Ogni volta che eseguiamo nuovamente lo squat, dobbiamo reimpostare questo timer. Per fare questo dobbiamo cambiare handleInput() :

void Heroine::handleInput(Input input) ( switch (state_) ( case STATE_STANDING: if (input == PRESS_DOWN) ( state_ = STATE_DUCKING; chargeTime_ = 0 ; setGraphics(IMAGE_DUCK); ) // Elabora l'input rimanente... rottura ; // Altri stati... } }

Alla fine, per aggiungere questo attacco caricato, abbiamo dovuto modificare due metodi e aggiungere un campo chargeTime_ a Heroine, anche se viene utilizzato solo nello stato accovacciato. Mi piacerebbe avere tutto questo codice e dati in un unico posto. La Banda dei Quattro può aiutarci in questo.

Stato del modello

Per le persone esperte nel paradigma orientato agli oggetti, ogni ramo condizionale è un'opportunità per utilizzare l'invio dinamico (in altre parole, chiamare un metodo virtuale in C++). Penso che dobbiamo andare ancora più a fondo in questa tana del coniglio. A volte è tutto ciò di cui abbiamo bisogno.

C’è una base storica per questo. Molti dei vecchi apostoli del paradigma orientato agli oggetti, come la Gang of Four e il loro Modelli di programmazione e Martin Fuller con il suo Refactoring proveniva da Smalltalk. E ifThen è solo un metodo che usi per elaborare la condizione e che è implementato in modo diverso per oggetti true e false.

Nel nostro esempio abbiamo già raggiunto il punto critico in cui dovremmo prestare attenzione a qualcosa di orientato agli oggetti. Questo ci porta al modello dello Stato. Per citare la Banda dei Quattro:

Consente agli oggetti di modificare il proprio comportamento in base ai cambiamenti dello stato interno. In questo caso, l'oggetto si comporterà come un'altra classe.

Non è molto chiaro. Alla fine, Switch affronta anche questo. In relazione al nostro esempio con l'eroina, il modello sarebbe simile a questo:

Interfaccia di stato

Innanzitutto, definiamo un'interfaccia per lo stato. Ogni bit di comportamento dipendente dallo stato, ad es. tutto ciò che abbiamo precedentemente implementato utilizzando switch si trasforma in un metodo virtuale di questa interfaccia. Nel nostro caso, questi sono handleInput() e update() .

classe HeroineState ( public : virtuale ~HeroineState() () handleInput del vuoto virtuale{} {} };

Classi per ogni stato

Per ogni stato definiamo una classe che implementa l'interfaccia. I suoi metodi determinano il comportamento dell'eroina in questo stato. In altre parole, prendiamo tutte le opzioni dello switch nell'esempio precedente e le trasformiamo in una classe di stato. Per esempio:

classe DuckingState: public HeroineState ( public : DuckingState() : chargeTime_(0 ) () handleInput del vuoto virtuale (Eroina&eroina, Ingresso input)( if (input == RELEASE_DOWN) ( // Transizione allo stato permanente... heroine.setGraphics(IMAGE_STAND); ) ) aggiornamento del vuoto virtuale (eroina ed eroina)( chargeTime_++; if (chargeTime_ > MAX_CHARGE) ( heroine.superBomb(); ) ) privato: int chargeTime_; );

Tieni presente che abbiamo spostato chargeTime_ dalla classe dell'eroina alla classe DuckingState. E questo è molto positivo, perché questo dato ha significato solo in questo stato e il nostro modello di dati lo indica chiaramente.

Delega allo Stato

classe Heroine (pubblico: handle del vuoto virtualeInput(Input input)( state_->handleInput(*this , input); ) aggiornamento del vuoto virtuale()( stato_->aggiornamento(*questo); ) // Altri metodi... privato: HeroineState* stato_; );

Per "cambiare stato" dobbiamo solo fare in modo che state_ punti a un diverso oggetto HeroineState. In questo consiste effettivamente il modello statale.

Sembra abbastanza simile ai modelli di strategia e tipo di oggetto di GoF. In tutti e tre abbiamo un oggetto master che delega a uno slave. La differenza è scopo.

  • Lo scopo della strategia è diminuzione della connettività(disaccoppiare) tra la classe principale e il suo comportamento.
  • Lo scopo di un oggetto tipo è creare un numero di oggetti che si comportano allo stesso modo condividendo tra loro un oggetto tipo comune.
  • Lo scopo di uno Stato è modificare il comportamento dell'oggetto principale modificando l'oggetto a cui delega.

Dove sono questi oggetti statali?

C'è qualcosa che non ti ho detto. Per cambiare lo stato dobbiamo assegnare a state_ un nuovo valore che punta al nuovo stato, ma da dove viene questo oggetto? Nel nostro esempio di enumerazione non c'è nulla a cui pensare: i valori di enumerazione sono semplicemente primitivi come i numeri. Ma ora i nostri stati sono rappresentati da classi, il che significa che abbiamo bisogno di puntatori a istanze reali. Ci sono due risposte più comuni:

Stati statici

Se un oggetto di stato non ha altri campi, l'unica cosa che memorizza è un puntatore a una tabella virtuale interna di metodi in modo che tali metodi possano essere chiamati. In questo caso non è necessario avere più di un'istanza della classe: ogni istanza sarà sempre la stessa.

Se il tuo stato non ha campi e ha un solo metodo virtuale, puoi semplificare ulteriormente il modello. Sostituiremo ciascuno Classe stato funzione stato: una normale funzione di livello superiore. E di conseguenza il campo stato_ nella nostra classe principale si trasformerà in un semplice puntatore a funzione.

È del tutto possibile cavarsela con uno solo statico copia. Anche se hai un sacco di FSM tutti nello stesso stato contemporaneamente, possono tutti puntare alla stessa istanza statica perché non c'è nulla di specifico della macchina a stati al riguardo.

Dipende da te dove posizionare l'istanza statica. Trova un posto dove sarà appropriato. Inseriamo la nostra istanza in una classe base. Per nessuna ragione.

class HeroineState ( public : statico StandingState in piedi; statico DuckingState ducking; statico JumpingState saltando; statico DivingState immersione; // Il resto del codice... };

Ciascuno di questi campi statici è un'istanza di uno stato utilizzato dal gioco. Per far saltare l'eroina, lo stato in piedi farà qualcosa del tipo:

if (input == PRESS_B) ( heroine.state_ = &HeroineState::jumping; heroine.setGraphics(IMAGE_JUMP); )

Istanze statali

A volte l'opzione precedente non decolla. Lo stato statico non è adatto allo stato accovacciato. Ha un campo chargeTime_ ed è specifico per l'eroina che sarà accovacciata. Funzionerà ancora meglio nel nostro caso, perché abbiamo solo un'eroina, ma se vogliamo aggiungere la modalità cooperativa per due giocatori, avremo grossi problemi.

In questo caso, dovremmo creare un oggetto di stato quando ci spostiamo al suo interno. Ciò consentirà a ogni FSM di avere la propria istanza di stato. Naturalmente, se allochiamo memoria per nuovo condizione, questo significa che dovremmo pubblicazione memoria occupata di quello corrente. Dobbiamo fare attenzione perché il codice che causa le modifiche è nello stato corrente del metodo. Non vogliamo rimuoverlo da sotto di noi.

Invece, lasceremo che handleInput() su HeroineState restituisca facoltativamente un nuovo stato. Quando ciò accade, Heroine rimuoverà il vecchio stato e lo sostituirà con quello nuovo, in questo modo:

void Heroine::handleInput(Input input) ( HeroineState* state = state_->handleInput(*this , input); if (state!= NULL ) ( delete state_; state_ = state; ) )

In questo modo non rimuoviamo lo stato precedente finché non siamo tornati dal nostro metodo. Ora, lo stato in piedi può passare allo stato di immersione creando una nuova istanza:

HeroineState* StandingState::handleInput(Heroine& heroine, Input input) ( if (input == PRESS_DOWN) ( // Altro codice... return new DuckingState(); ) // Rimani in questo stato. return NULL ; )

Quando posso, preferisco utilizzare gli stati statici perché non occupano memoria e cicli della CPU allocando oggetti ogni volta che lo stato cambia. Per condizioni che non sono altro che giuste stato- questo è esattamente ciò di cui hai bisogno.

Naturalmente, quando si alloca dinamicamente la memoria per lo stato, è necessario pensare alla possibile frammentazione della memoria. Il modello Pool di oggetti può aiutare.

Passaggi di accesso e disconnessione

Il modello State è progettato per incapsulare tutto il comportamento e i dati associati all'interno di un'unica classe. Stiamo andando abbastanza bene, ma ci sono ancora alcuni dettagli poco chiari.

Quando l'eroina cambia stato, cambiamo anche il suo sprite. In questo momento, questo codice appartiene allo stato, con chi lei cambia. Quando lo stato passa da tuffo a fermo, il tuffo stabilisce la sua immagine:

HeroineState* DuckingState::handleInput(Heroine& heroine, Input input) ( if (input == RELEASE_DOWN) ( heroine.setGraphics(IMAGE_STAND); return new StandingState(); ) // Altro codice... )

Ciò che vogliamo veramente è che ogni stato controlli la propria grafica. Possiamo raggiungere questo obiettivo aggiungendo allo Stato azione di ingresso (azione di ingresso):

class StandingState: public HeroineState ( public : ingresso nel vuoto virtuale (eroina ed eroina)( heroine.setGraphics(IMAGE_STAND); ) // Altro codice... );

Tornando a Heroine, modifichiamo il codice per garantire che il cambiamento di stato sia accompagnato da una chiamata alla funzione di azione di input del nuovo stato:

void Heroine::handleInput(Input input) ( HeroineState* state = state_->handleInput(*this , input); if (state != NULL ) ( delete state_; state_ = state; // Richiama l'azione di input del nuovo stato. stato_->enter(*questo); ) )

Ciò semplificherà il codice DuckingState:

HeroineState* DuckingState::handleInput(Heroine& heroine, Input input) ( if (input == RELEASE_DOWN) ( return new StandingState(); ) // Altro codice... )

Tutto ciò che fa è passare allo stato in piedi e lo stato in piedi si prende cura della grafica. Ora i nostri stati sono veramente incapsulati. Un'altra caratteristica interessante di tale azione di input è che viene attivata quando si entra in uno stato, indipendentemente dallo stato in cui si trova Quale eravamo là.

La maggior parte dei grafici di stato nella vita reale presentano più transizioni allo stesso stato. Ad esempio, la nostra eroina può sparare con un'arma stando in piedi, seduta o saltando. Ciò significa che potremmo avere duplicazione del codice ovunque ciò accada. L'azione di input ti consente di raccoglierlo in un unico posto.

Puoi farlo per analogia azione di uscita (azione di uscita). Questo sarà semplicemente un metodo a cui faremo appello prima allo Stato in partenza e passare a un nuovo stato.

E cosa abbiamo ottenuto?

Ho passato così tanto tempo a venderti FSM, e ora sto per toglierti il ​​terreno sotto i piedi. Tutto quello che ho detto finora è vero e rappresenta un ottimo risolutore di problemi. Ma si dà il caso che i vantaggi più importanti delle macchine a stati finiti siano anche i loro maggiori svantaggi.

La macchina a stati ti aiuta a districare seriamente il tuo codice organizzandolo in una struttura molto rigorosa. Tutto ciò che abbiamo è un insieme fisso di stati, un singolo stato corrente e transizioni codificate.

Un automa finito non è Turing completo. La teoria degli automi descrive la completezza attraverso una serie di modelli astratti, ciascuno più complesso del precedente. La macchina di Turing è una delle più espressive.

"Turing completo" indica un sistema (di solito un linguaggio di programmazione) sufficientemente espressivo per implementare una macchina di Turing. Ciò a sua volta significa che tutti i linguaggi completi di Turing sono approssimativamente ugualmente espressivi. Gli FSM non sono abbastanza espressivi per entrare in questo club.

Se provi a utilizzare una macchina a stati per qualcosa di più complesso, come l’intelligenza artificiale di un gioco, ti imbatterai immediatamente nei limiti di questo modello. Fortunatamente, i nostri predecessori hanno imparato ad aggirare alcuni ostacoli. Concluderò questo capitolo con alcuni esempi di questo tipo.

Macchina statale competitiva

Abbiamo deciso di aggiungere la possibilità per la nostra eroina di portare un'arma. Anche se ora è armata, può ancora fare tutto ciò che faceva prima: correre, saltare, accovacciarsi, ecc. Ma ora, mentre fa tutto questo, può anche sparare con un'arma.

Se vogliamo adattare questo comportamento al quadro FSM, dovremo raddoppiare il numero di Stati. Per ciascuno degli stati dovremo crearne un altro uguale, ma per l'eroina con un'arma: in piedi, in piedi con un'arma, saltando, saltando con un'arma... Beh, hai capito.

Se aggiungi qualche altra arma, il numero di stati aumenterà in modo combinatorio. E non si tratta solo di un insieme di stati, ma anche di un insieme di ripetizioni: gli stati armati e quelli disarmati sono quasi identici, tranne che per la parte del codice responsabile della sparatoria.

Il problema qui è che stiamo confondendo due parti dello Stato: quello fa E allora tiene tra le mani- in una macchina. Per modellare tutte le possibili combinazioni, dobbiamo creare uno stato per ciascuna coppie. La soluzione è ovvia: è necessario creare due macchine a stati separate.

Se vogliamo unirci N stati d'azione e M stati di ciò che abbiamo tra le mani in una macchina a stati finiti: di cui abbiamo bisogno n×m stati. Se abbiamo due mitragliatrici, ne avremo bisogno n+m stati.

Lasceremo la nostra prima macchina statale con azioni invariate. E oltre a ciò, creeremo un'altra macchina per descrivere ciò che tiene in mano l'eroina. Ora Heroine avrà due riferimenti "stato", uno per ogni macchina.

classe eroina ( // Il resto del codice... privato: HeroineState* stato_; HeroineState* equipaggiamento_; );

A scopo illustrativo, utilizziamo l'implementazione completa del modello State per la seconda macchina a stati, anche se in pratica in questo caso sarebbe sufficiente un semplice flag booleano.

Quando l'eroina delega l'input agli stati, passa la traduzione a entrambe le macchine statali:

void Heroine::handleInput(Input input) ( state_->handleInput(*this , input); Equipment_->handleInput(*this , input); )

Sistemi più complessi possono includere macchine a stati finiti che possono assorbire parte dell'input in modo che altre macchine non lo ricevano più. Ciò ci consentirà di prevenire una situazione in cui più macchine rispondono allo stesso input.

Ogni macchina a stati può reagire agli input, produrre comportamenti e cambiare il proprio stato indipendentemente dalle altre macchine a stati. E quando entrambi gli stati sono praticamente indipendenti, funziona alla grande.

In pratica, potresti incontrare una situazione in cui gli stati interagiscono tra loro. Ad esempio, non può sparare mentre salta o, ad esempio, eseguire un attacco in scivolata quando è armato. Per garantire questo comportamento e il coordinamento degli automi nel codice, dovrai tornare allo stesso controllo di forza bruta tramite if un altro macchina a stati finiti. Non è la soluzione più elegante, ma almeno funziona.

Macchina statale gerarchica

Dopo un'ulteriore rivitalizzazione del comportamento dell'eroina, probabilmente avrà un sacco di stati simili. Ad esempio, non è possibile stare in piedi, camminare, correre e scivolare lungo i pendii. In uno qualsiasi di questi stati, premendo B la fa saltare, mentre premendo verso il basso la fa accovacciare.

Nell'implementazione più semplice di una macchina a stati, abbiamo duplicato questo codice per tutti gli stati. Ma ovviamente sarebbe molto meglio se dovessimo scrivere il codice solo una volta e poi potessimo riutilizzarlo per tutti gli stati.

Se questo fosse solo codice orientato agli oggetti e non una macchina a stati, potremmo usare una tecnica per separare il codice tra stati chiamata ereditarietà. Puoi definire una classe per lo stato fondamentale che gestirà il salto e l'accovacciamento. Stare in piedi, camminare, correre e rotolare vengono ereditati da esso e aggiungono il proprio comportamento aggiuntivo.

Questa decisione ha conseguenze sia positive che negative. L'ereditarietà è uno strumento potente per riutilizzare il codice, ma allo stesso tempo fornisce una coesione molto forte tra due parti di codice. Il martello è troppo pesante per colpire senza pensarci.

In questa forma verrà chiamata la struttura risultante macchina statale gerarchica(O automa gerarchico). E ogni condizione può avere la sua superstato(lo stato stesso si chiama sottostato). Quando si verifica un evento e il sottostato non lo elabora, viene passato alla catena dei superstati. In altre parole, sembra una sostituzione di un metodo ereditato.

Infatti, se utilizziamo il modello State originale per implementare FSM, possiamo già utilizzare l'ereditarietà delle classi per implementare la gerarchia. Definiamo una classe base per la superclasse:

class OnGroundState: public HeroineState ( public : handleInput del vuoto virtuale (Eroina&eroina, Ingresso input)( if (input == PRESS_B) ( // Salta... ) else if (input == PRESS_DOWN) ( // Squat... ) ) );

E ora ogni sottoclasse lo erediterà:

class DuckingState: public OnGroundState ( public : handleInput del vuoto virtuale (Eroina&eroina, Ingresso input)( if (input == RELEASE_DOWN) ( // Alzati... ) else ( // L'input non viene elaborato. Pertanto, lo passiamo più in alto nella gerarchia. OnGroundState::handleInput(eroina, input); )) );

Naturalmente, questo non è l’unico modo per implementare la gerarchia. Ma se non usi il modello Gang of Four State, non funzionerà. Invece, puoi modellare una chiara gerarchia degli stati attuali e dei superstati con pila stati invece di un singolo stato nella classe principale.

Lo stato attuale sarà in cima allo stack, sotto c'è il suo superstato, quindi il superstato per Questo superstati, ecc. E quando è necessario implementare un comportamento specifico dello stato, si inizia dalla cima dello stack e si procede verso il basso finché lo stato non lo gestisce. (E se non lo elabora, semplicemente lo ignori).

Macchina automatica con memoria magazzino

Esiste un'altra estensione comune alle macchine a stati che utilizza anch'essa uno stack di stati. Solo che qui lo stack rappresenta un concetto completamente diverso e viene utilizzato per risolvere problemi diversi.

Il problema è che una macchina statale non ha concetto storie. Sai in che stato ti trovi? sei, ma non hai informazioni sullo stato in cui ti trovi erano. E di conseguenza, non esiste un modo semplice per tornare allo stato precedente.

Ecco un semplice esempio: in precedenza, permettevamo alla nostra impavida eroina di armarsi fino ai denti. Quando spara con la sua arma, abbiamo bisogno di un nuovo stato per riprodurre l'animazione dello sparo, generare il proiettile e gli effetti visivi di accompagnamento. Per fare ciò, creiamo un nuovo FiringState ed effettuiamo transizioni verso di esso da tutti gli stati in cui l'eroina può sparare premendo il pulsante di fuoco.

Poiché questo comportamento è duplicato tra più stati, è qui che è possibile utilizzare una macchina a stati gerarchica per riutilizzare il codice.

La difficoltà qui è che devi in ​​qualche modo capire in quale stato devi andare. Dopo tiro. L'eroina può sparare l'intera clip mentre è ferma, corre, salta o accovacciata. Una volta terminata la sequenza delle riprese, deve tornare allo stato in cui si trovava prima delle riprese.

Se ci affezioniamo alla FSM pura, dimentichiamo immediatamente in quale stato ci trovavamo. Per tenerne traccia, dobbiamo definire molti stati quasi identici: tiro da fermo, tiro in corsa, tiro in salto, ecc. Pertanto, abbiamo transizioni codificate che passano allo stato corretto una volta completate.

Ciò di cui abbiamo veramente bisogno è la capacità di memorizzare lo stato in cui ci trovavamo prima della sparatoria e di ricordarlo nuovamente dopo la sparatoria. Anche in questo caso la teoria degli automi ci viene in aiuto. La struttura dati corrispondente si chiama Pushdown Automaton.

Laddove in un automa finito abbiamo un singolo puntatore allo stato, in un automa con memoria di deposito c'è il loro pila. Nella FSM, la transizione verso un nuovo stato sostituisce quello precedente. Anche una macchina dotata di memoria magazzino consente di farlo, ma aggiunge altre due operazioni:

    Puoi posto (spingere) nuovo stato nello stack. Lo stato corrente sarà sempre in cima allo stack, quindi questa è l'operazione di passaggio a un nuovo stato. Ma allo stesso tempo, il vecchio stato rimane direttamente sotto quello attuale nello stack e non scompare senza lasciare traccia.

    Puoi estratto (pop) stato in cima allo stack. Lo Stato scompare e ciò che c'era sotto diventa attuale.

Questo è tutto ciò di cui abbiamo bisogno per le riprese. Noi creiamo l'unica cosa condizione di ripresa. Quando premiamo il pulsante di fuoco mentre ci troviamo in uno stato diverso, noi posto (spingere) stato di ripresa dello stack. Al termine dell'animazione delle riprese, noi estratto (pop) e una macchina con memoria magazzino ci riporta automaticamente allo stato precedente.

Quanto sono davvero utili?

Anche con questa espansione delle macchine statali, le loro capacità sono ancora piuttosto limitate. Nell'intelligenza artificiale odierna, la tendenza prevalente è quella di utilizzare cose come alberi comportamentali(alberi comportamentali) e sistemi di pianificazione(sistemi di pianificazione). E se sei particolarmente interessato al campo dell'intelligenza artificiale, l'intero capitolo dovrebbe stuzzicare il tuo appetito. Per soddisfarlo, dovrai rivolgerti ad altri libri.

Ciò non significa affatto che le macchine a stati finiti, le macchine con memoria di magazzino e altri sistemi simili siano completamente inutili. Per alcune cose questi sono buoni strumenti di modellazione. Le macchine a stati sono utili quando:

  • Hai un'entità il cui comportamento cambia a seconda del suo stato interno.
  • Questa condizione è rigorosamente suddivisa in un numero relativamente piccolo di opzioni specifiche.
  • L'entità risponde continuamente a una serie di comandi o eventi di input.

Nei giochi, le macchine a stati vengono generalmente utilizzate per modellare l'intelligenza artificiale, ma possono anche essere utilizzate per implementare l'input dell'utente, la navigazione nei menu, l'analisi del testo, i protocolli di rete e altri comportamenti asincroni.

"ModelloStato" source.ru

Lo stato è un modello di comportamento dell'oggetto che specifica funzionalità diverse a seconda dello stato interno dell'oggetto. fonte del sito web originale

Condizioni, compito, scopo

Consente a un oggetto di variare il proprio comportamento a seconda del suo stato interno. Poiché il comportamento può cambiare in modo del tutto arbitrario e senza alcuna restrizione, dall'esterno sembra che la classe dell'oggetto sia cambiata.

Motivazione

Considera la classe Connessione TCP, che rappresenta una connessione di rete. Un oggetto di questa classe può trovarsi in uno dei diversi stati: Stabilito(installato), Ascoltando(ascoltando), Chiuso(Chiuso). Quando un oggetto Connessione TCP riceve richieste da altri oggetti, risponde in modo diverso a seconda dello stato corrente. Ad esempio, la risposta a una richiesta Aprire(aperto) dipende dallo stato in cui si trova la connessione Chiuso O Stabilito. Il modello di stato descrive come funziona un oggetto Connessione TCP possono comportarsi diversamente quando si trovano in stati diversi. sito sorgente sito originale

L'idea principale di questo modello è introdurre una classe astratta TCPState per rappresentare diversi stati di connessione. Questa classe dichiara un'interfaccia comune a tutte le classi che descrivono diversi lavoratori. fonte originale.ru

condizione. In queste sottoclassi TCPState viene implementato il comportamento specifico dello stato. Ad esempio, nelle classi TCPstabilito E TCPChiuso comportamento specifico dello stato implementato Stabilito E Chiuso rispettivamente. fonte originale del sito web del sito web

originale.ru

Classe Connessione TCP memorizza un oggetto di stato (un'istanza di una sottoclasse TCPState) che rappresenta lo stato corrente della connessione e delega tutte le richieste dipendenti dallo stato a questo oggetto. Connessione TCP utilizza la propria istanza della sottoclasse TCPState abbastanza semplice: chiamare metodi di una singola interfaccia TCPState, solo a seconda della sottoclasse specifica attualmente memorizzata TCPState-a - il risultato è diverso, cioè in realtà vengono eseguite operazioni specifiche solo per questo stato di connessione. fonte originale.ru

E ogni volta che cambia lo stato della connessioneConnessione TCP cambia il suo oggetto di stato. Ad esempio, quando una connessione stabilita viene chiusa, Connessione TCP sostituisce un'istanza di una classe TCPstabilito copia TCPChiuso. sito sito di origine originale

Segni di applicazione, uso del modello di Stato

Utilizzare il modello di stato nei seguenti casi: source.ru
  1. Quando il comportamento di un oggetto dipende dal suo stato e deve cambiare in fase di esecuzione. fonte originale.ru
  2. Quando il codice dell'operazione contiene istruzioni condizionali costituite da molti rami, in cui la scelta del ramo dipende dallo stato. Tipicamente in questo caso lo stato è rappresentato da costanti enumerate. Spesso la stessa struttura dell'istruzione condizionale viene ripetuta in diverse operazioni. Lo schema dello stato suggerisce di collocare ciascun ramo in una classe separata. Ciò consente di trattare lo stato di un oggetto come un oggetto indipendente che può cambiare indipendentemente dagli altri. sito sorgente sito originale

Soluzione

fonte originale del sito web del sito web

source.ru

Partecipanti al modello statale

source.ru
  1. Contesto(Connessione TCP) - contesto.
    Definisce un'unica interfaccia per i client.
    Memorizza un'istanza di una sottoclasse StatoConcreto, che determina lo stato attuale. codelab.
  2. Stato(TCPState) - stato.
    Definisce un'interfaccia per incapsulare il comportamento associato a un particolare stato del contesto. sito di origine sito originale
  3. Sottoclassi StatoConcreto(TCPEstablished, TCPListen, TCPClosed) - stato specifico.
    Ogni sottoclasse implementa il comportamento associato ad alcuni stati del contesto Contesto. sito sito di origine originale

Schema per l'utilizzo del modello di Stato

Classe Contesto delega le richieste all'oggetto corrente StatoConcreto. sito sito di origine originale

Un contesto può passare se stesso come argomento a un oggetto Stato, che elaborerà la richiesta. Ciò consente all'oggetto stato ( StatoConcreto) accedere al contesto se necessario. codelab.

Contesto- Questa è l'interfaccia principale per i client. I client possono configurare il contesto con oggetti di stato Stato(più precisamente StatoConcreto). Una volta configurato il contesto, i client non hanno più bisogno di comunicare direttamente con gli oggetti di stato (solo attraverso l'interfaccia comune Stato). source.ru

Anche in questo caso Contesto o le sottoclassi stesse StatoConcreto può decidere in quali condizioni e in quale ordine avviene il cambiamento di stato. fonte del sito web originale

Domande riguardanti l'attuazione del modello Stato

Domande riguardanti l’attuazione del modello statale: fonte originale.ru
  1. Cosa determina le transizioni tra stati.
    Il modello statale non dice nulla su quale partecipante determini le condizioni (criteri) per la transizione tra stati. Se i criteri sono fissi, possono essere implementati direttamente nella classe Contesto. Tuttavia, in generale, un approccio più flessibile e corretto è quello di consentire le sottoclassi della classe stessa Stato determinare lo stato successivo e il momento di transizione. Per farlo in classe Contesto dobbiamo aggiungere un'interfaccia che consenta dagli oggetti Stato impostarne lo stato.
    Questa logica di transizione decentralizzata è più facile da modificare ed estendere: devi solo definire nuove sottoclassi Stato. Lo svantaggio del decentramento è che ogni sottoclasse Stato deve “conoscere” almeno una sottoclasse di un altro stato (al quale può effettivamente passare allo stato corrente), il che introduce dipendenze di implementazione tra le sottoclassi. sito di origine sito originale

    fonte originale.ru
  2. Alternativa tabellare.
    Esiste un altro modo per strutturare il codice guidato dallo stato. Questo è il principio di una macchina a stati finiti. Utilizza una tabella per mappare gli input alle transizioni di stato. Con il suo aiuto, puoi determinare in quale stato devi andare quando arrivano determinati dati di input. In sostanza, stiamo sostituendo il codice condizionale con una ricerca nella tabella.
    Il vantaggio principale della macchina è la sua regolarità: per cambiare i criteri di transizione è sufficiente modificare solo i dati, non il codice. Ma ci sono anche degli svantaggi:
    - cercare una tabella è spesso meno efficiente che chiamare una funzione,
    - presentare la logica di transizione in un formato tabellare uniforme rende i criteri meno espliciti e, quindi, più difficili da comprendere,
    - solitamente è difficile aggiungere azioni che accompagnino le transizioni tra Stati. Il metodo tabellare tiene conto degli stati e delle transizioni tra di essi, ma deve essere integrato in modo che possano essere eseguiti calcoli arbitrari ad ogni cambiamento di stato.
    La differenza principale tra le macchine a stati basate su tabelle e Pattern State può essere formulata come segue: Pattern State modella il comportamento dipendente dallo stato e il metodo della tabella si concentra sulla definizione delle transizioni tra gli stati. originale.ru

    sito sito di origine originale
  3. Creazione e distruzione di oggetti statali.
    Durante il processo di sviluppo solitamente devi scegliere tra:
    - creare oggetti statali quando sono necessari e distruggerli immediatamente dopo l'uso,
    -creandoli in anticipo e per sempre.

    La prima opzione è preferibile quando non si sa in anticipo in quali stati cadrà il sistema e il contesto cambia lo stato relativamente raramente. Allo stesso tempo, non creiamo oggetti che non verranno mai utilizzati, il che è importante se negli oggetti di stato vengono archiviate molte informazioni. Quando i cambiamenti di stato si verificano frequentemente, quindi non vuoi distruggere gli oggetti che li rappresentano (perché potrebbero essere nuovamente necessari molto presto), dovresti usare il secondo approccio. Il tempo per la creazione degli oggetti viene speso solo una volta, all'inizio, e il tempo per la distruzione non viene speso affatto. È vero, questo approccio può rivelarsi scomodo, poiché il contesto deve memorizzare riferimenti a tutti gli stati in cui teoricamente il sistema potrebbe cadere. fonte originale.ru

    source.ru originale
  4. Utilizzando il cambiamento dinamico.
    È possibile variare il comportamento su richiesta modificando la classe dell'oggetto in fase di esecuzione, ma la maggior parte dei linguaggi orientati agli oggetti non lo supporta. L'eccezione è rappresentata da Perl, JavaScript e altri linguaggi basati su motori di scripting che forniscono tale meccanismo e quindi supportano direttamente Pattern State. Ciò consente agli oggetti di variare il proprio comportamento modificando il codice della classe. source.ru

    .ru fonte originale

risultati

Risultati dell'uso stato del modello: fonte originale.ru
  1. Localizza il comportamento dipendente dallo stato.
    E lo divide in parti corrispondenti agli stati. Il modello di stato inserisce tutto il comportamento associato a un particolare stato in un oggetto separato. Perché il codice dipendente dallo stato è interamente contenuto in una delle sottoclassi della classe Stato, quindi puoi aggiungere nuovi stati e transizioni semplicemente generando nuove sottoclassi.
    Invece, si potrebbero usare i membri dati per definire gli stati interni, quindi le operazioni dell'oggetto Contesto controllerei questi dati. Ma in questo caso, istruzioni condizionali o istruzioni di ramo simili sarebbero sparse in tutto il codice della classe Contesto. Tuttavia, l'aggiunta di un nuovo stato richiederebbe la modifica di diverse operazioni, il che renderebbe difficile la manutenzione. Il modello di stato risolve questo problema, ma ne crea anche un altro, poiché il comportamento per i diversi stati finisce per essere distribuito tra diverse sottoclassi Stato. Ciò aumenta il numero di classi. Naturalmente, una classe è più compatta, ma se ci sono molti stati, allora tale distribuzione è più efficiente, poiché altrimenti bisognerebbe fare i conti con istruzioni condizionali ingombranti.
    Avere istruzioni condizionali complicate non è auspicabile, così come avere procedure lunghe. Sono troppo monolitici, motivo per cui modificare ed estendere il codice diventa un problema. Il modello di stato offre un modo migliore per strutturare il codice dipendente dallo stato. La logica che descrive le transizioni di stato non è più racchiusa in istruzioni monolitiche Se O interruttore, ma distribuiti tra sottoclassi Stato. Incapsulando ogni transizione e azione in una classe, lo stato diventa un oggetto a tutti gli effetti. Ciò migliora la struttura del codice e ne rende più chiaro lo scopo. originale.ru
  2. Rende esplicite le transizioni tra gli stati.
    Se un oggetto definisce il suo stato corrente esclusivamente in termini di dati interni, le transizioni tra stati non hanno una rappresentazione esplicita; appaiono solo come assegnazioni a determinate variabili. L'introduzione di oggetti separati per stati diversi rende le transizioni più esplicite. Inoltre, oggetti Stato può proteggere il contesto Contesto dalla mancata corrispondenza delle variabili interne, poiché le transizioni dal punto di vista del contesto sono azioni atomiche. Per effettuare la transizione è necessario modificare il valore di una sola variabile (variabile oggetto Stato in classe Contesto), piuttosto che diversi. source.ru
  3. Gli oggetti di stato possono essere condivisi.
    Se in uno stato oggetto Stato non ci sono variabili di istanza, il che significa che lo stato che rappresenta è codificato esclusivamente dal tipo stesso, quindi contesti diversi possono condividere lo stesso oggetto Stato. Quando gli stati sono separati in questo modo, sono essenzialmente opportunisti (vedi modello opportunista) che non hanno uno stato interno, ma solo un comportamento. sito sorgente sito originale

Esempio

Diamo un'occhiata all'implementazione dell'esempio dalla sezione "", ad es. costruire una semplice architettura di connessione TCP. Questa è una versione semplificata del protocollo TCP; ovviamente non rappresenta l'intero protocollo e nemmeno tutti gli stati delle connessioni TCP. source.ru originale

Prima di tutto definiamo la classe Connessione TCP, che fornisce un'interfaccia per il trasferimento dei dati e gestisce le richieste di modifica dello stato: TCPConnection. source.ru originale

In una variabile membro stato classe Connessione TCP viene memorizzata un'istanza della classe TCPState. Questa classe duplica l'interfaccia di modifica dello stato definita nella classe Connessione TCP. fonte originale.ru

sito sito di origine originale

Connessione TCP delega tutte le richieste dipendenti dallo stato all'istanza archiviata in state TCPState. Anche in classe Connessione TCP c'è un'operazione Cambia stato, con il quale puoi scrivere un puntatore a un altro oggetto in questa variabile TCPState. Costruttore di classi Connessione TCP inizializza stato puntatore allo stato chiuso TCPChiuso(lo definiremo di seguito). fonte originale.ru

source.ru originale

Ogni operazione TCPState accetta un'istanza Connessione TCP come parametro, consentendo così l'oggetto TCPState accedere ai dati dell'oggetto Connessione TCP e modificare lo stato della connessione. .ru

In classe TCPState ha implementato il comportamento predefinito per tutte le richieste ad esso delegate. Può anche cambiare lo stato di un oggetto Connessione TCP attraverso un'operazione Cambia stato. TCPState si trova nello stesso pacchetto di Connessione TCP, quindi ha accesso anche a questa operazione: TCPState . sito sito di origine originale

originale.ru

Nelle sottoclassi TCPState comportamento dipendente dallo stato implementato. Una connessione TCP può trovarsi in molti stati: Stabilito(installato), Ascoltando(ascoltando), Chiuso(chiuso), ecc., e ognuno di essi ha la propria sottoclasse TCPState. Per semplicità, considereremo in dettaglio solo 3 sottoclassi: TCPstabilito, TCPAscolta E TCPChiuso. fonte originale del sito web del sito web

Fonte Ru

Nelle sottoclassi TCPState implementa il comportamento dipendente dallo stato per le richieste valide in quello stato. .ru

sito sito di origine originale

Dopo aver eseguito azioni specifiche dello stato, queste operazioni fonte originale.ru

causa Cambia stato per modificare lo stato di un oggetto Connessione TCP. Lui stesso non ha informazioni sul protocollo TCP. Sono sottoclassi TCPState definire le transizioni tra stati e azioni dettate dal protocollo. originale.ru

fonte originale.ru

Applicazioni note del modello di stato

Ralph Johnson e Jonathan Zweig caratterizzano il modello di stato e lo descrivono in relazione al protocollo TCP.
I programmi di disegno interattivo più diffusi forniscono "strumenti" per eseguire operazioni di manipolazione diretta. Ad esempio, uno strumento di disegno di linee consente all'utente di fare clic su un punto arbitrario con il mouse e quindi spostare il mouse per tracciare una linea da quel punto. Lo strumento di selezione consente di selezionare alcune forme. In genere, tutti gli strumenti disponibili vengono posizionati nella tavolozza. Il compito dell'utente è selezionare e applicare uno strumento, ma in realtà il comportamento dell'editor varia al variare dello strumento: con lo strumento disegno creiamo forme, con lo strumento selezione le selezioniamo e così via. fonte originale del sito web del sito web

Per riflettere la dipendenza del comportamento dell'editor dallo strumento corrente, puoi utilizzare il modello di stato. fonte originale.ru

È possibile definire una classe astratta Attrezzo, le cui sottoclassi implementano il comportamento specifico dello strumento. L'editor grafico memorizza un collegamento all'oggetto corrente Purel e gli delega le richieste in arrivo. Quando selezioni uno strumento, l'editor utilizza un oggetto diverso, che fa sì che il comportamento cambi. .ru

Questa tecnica viene utilizzata nei framework degli editor grafici HotDraw e Unidraw. Consente ai clienti di definire facilmente nuovi tipi di strumenti. IN HotDraw Classe DrawingController inoltra le richieste all'oggetto corrente Attrezzo. IN Unidraw vengono chiamate le classi corrispondenti Spettatore E Attrezzo. Il diagramma delle classi riportato di seguito fornisce una rappresentazione schematica delle interfacce delle classi Attrezzo

originale.ru

Scopo del modello statale

  • Il modello State consente a un oggetto di modificare il proprio comportamento a seconda del suo stato interno. Sembra che l'oggetto abbia cambiato classe.
  • Il modello State è un'implementazione orientata agli oggetti di una macchina a stati.

Problema da risolvere

Il comportamento di un oggetto dipende dal suo stato e deve cambiare durante l'esecuzione del programma. Tale schema può essere implementato utilizzando molti operatori condizionali: in base all'analisi dello stato attuale dell'oggetto, vengono intraprese determinate azioni. Tuttavia, con un gran numero di stati, le istruzioni condizionali saranno sparse in tutto il codice e un programma del genere sarà difficile da mantenere.

Discussione sul modello statale

Il modello State risolve questo problema come segue:

  • Introduce una classe Context che definisce un'interfaccia verso il mondo esterno.
  • Introduce la classe astratta State.
  • Rappresenta i vari "stati" di una macchina a stati come sottoclassi State.
  • La classe Context ha un puntatore allo stato corrente che cambia quando cambia lo stato della macchina a stati.

Il modello di stato non definisce dove viene determinata esattamente la condizione per la transizione a un nuovo stato. Ci sono due opzioni: la classe Context o le sottoclassi State. Il vantaggio di quest'ultima opzione è che è facile aggiungere nuove classi derivate. Lo svantaggio è che ogni sottoclasse di stato deve conoscere i suoi vicini per effettuare una transizione verso un nuovo stato, il che introduce dipendenze tra le sottoclassi.

Esiste anche un approccio alternativo basato su tabelle per progettare macchine a stati finiti, basato sull'uso di una tabella che mappa in modo univoco i dati di input alle transizioni tra stati. Tuttavia, questo approccio presenta degli svantaggi: è difficile aggiungere l'esecuzione di azioni durante l'esecuzione delle transizioni. L'approccio del modello State utilizza il codice (invece delle strutture dati) per effettuare transizioni tra stati, quindi queste azioni sono facili da aggiungere.

Struttura del modello statale

La classe Context definisce l'interfaccia esterna per i client e memorizza un riferimento allo stato corrente dell'oggetto State. L'interfaccia della classe base astratta State è la stessa dell'interfaccia Context con l'eccezione di un parametro aggiuntivo: un puntatore a un'istanza Context. Le classi derivate dallo stato definiscono il comportamento specifico dello stato. La classe wrapper Context delega tutte le richieste ricevute a un oggetto "stato corrente", che può utilizzare il parametro aggiuntivo ricevuto per accedere all'istanza Context.

Il modello State consente a un oggetto di modificare il proprio comportamento a seconda del suo stato interno. Un'immagine simile può essere osservata nel funzionamento di un distributore automatico. Le macchine possono avere stati diversi a seconda della disponibilità delle merci, della quantità di monete ricevute, della capacità di scambiare denaro, ecc. Dopo che l'acquirente ha selezionato e pagato il prodotto, sono possibili le seguenti situazioni (stati):

  • Consegnare la merce all'acquirente; non è necessario il resto.
  • Dai la merce all'acquirente e cambiala.
  • L'acquirente non riceverà la merce per mancanza di denaro sufficiente.
  • L'acquirente non riceverà la merce a causa della sua assenza.

Utilizzando il modello di stato

  • Definire una classe wrapper di contesto esistente o crearne una nuova da utilizzare dal client come "macchina a stati".
  • Crea una classe State di base che replichi l'interfaccia della classe Context. Ogni metodo accetta un parametro aggiuntivo: un'istanza della classe Context. La classe State può definire qualsiasi comportamento "predefinito" utile.
  • Crea classi derivate dallo stato per tutti gli stati possibili.
  • La classe wrapper Context ha un riferimento all'oggetto dello stato corrente.
  • La classe Context delega semplicemente tutte le richieste ricevute dal client all'oggetto “stato corrente”, con l'indirizzo dell'oggetto Context passato come parametro aggiuntivo.
  • Utilizzando questo indirizzo, i metodi della classe State possono modificare, se necessario, lo "stato corrente" della classe Context.

Caratteristiche del modello statale

  • Gli oggetti stato sono spesso singleton.
  • Flyweight mostra come e quando gli oggetti Stato possono essere divisi.
  • Il modello Interpreter può utilizzare State per definire i contesti di analisi.
  • I modelli State e Bridge hanno strutture simili, tranne per il fatto che Bridge consente una gerarchia di classi di inviluppo (analoghi delle classi “wrapper”), mentre State no. Questi pattern hanno strutture simili, ma risolvono problemi diversi: State consente a un oggetto di cambiare il suo comportamento a seconda del suo stato interno, mentre Bridge separa l'astrazione dalla sua implementazione in modo che possano essere modificati indipendentemente l'uno dall'altro.
  • L’attuazione del modello Stato si basa sul modello Strategia. Le differenze risiedono nel loro scopo.

Attuazione del modello statale

Consideriamo un esempio di macchina a stati finiti con due possibili stati e due eventi.

#includere utilizzando lo spazio dei nomi std; class Macchina ( class State *current; public: Machine(); void setCurrent(State *s) ( current = s; ) void on(); void off(); ); class State ( public: virtual void on(Machine *m) ( cout<< " already ON\n"; } virtual void off(Machine *m) { cout << " already OFF\n"; } }; void Machine::on() { current->su questo); ) void Macchina::off() ( corrente->off(questo); ) classe ON: public Stato ( public: ON() ( cout<< " ON-ctor "; }; ~ON() { cout << " dtor-ON\n"; }; void off(Machine *m); }; class OFF: public State { public: OFF() { cout << " OFF-ctor "; }; ~OFF() { cout << " dtor-OFF\n"; }; void on(Machine *m) { cout << " going from OFF to ON"; m->setCurrent(nuovo ON()); cancella questo; ) ); void ON::off(Macchina *m) ( cout<< " going from ON to OFF"; m->setCurrent(nuovo OFF()); cancella questo; ) Macchina::Macchina() ( corrente = nuovo OFF(); cout<< "\n"; } int main() { void(Machine:: *ptrs)() = { Machine::off, Machine::on }; Machine fsm; int num; while (1) { cout << "Enter 0/1: "; cin >>numero; (fsm. *ptrs)(); ) )

15.02.2016
21:30

Il modello State è destinato alla progettazione di classi con più stati logici indipendenti. Passiamo direttamente a un esempio.

Diciamo che stiamo sviluppando una classe di controllo della webcam. La fotocamera può trovarsi in tre stati:

  1. Non inizializzato. Chiamiamolo NotConnectedState ;
  2. Inizializzato e pronto all'uso, ma non è stato ancora acquisito alcun fotogramma. Lascia che sia ReadyState;
  3. Modalità di acquisizione fotogrammi attivi. Indichiamo ActiveState .

Poiché stiamo lavorando con il modello di stato, è meglio iniziare con l'immagine del diagramma di stato:

Ora trasformiamo questo diagramma in codice. Per non complicare l'implementazione, omettiamo il codice per lavorare con le webcam. Se necessario, puoi aggiungere tu stesso le chiamate di funzione di libreria appropriate.

Fornirò immediatamente l'elenco completo con commenti minimi. Successivamente discuteremo i dettagli chiave di questa implementazione in modo più dettagliato.

#includere #define DECLARE_GET_INSTANCE(ClassName) \ static ClassName* getInstance() (\ static ClassName istanza;\ return \ ) class WebCamera ( public: typedef std::string Frame; public: // *********** *************************************** // Eccezioni // ****** ******************************************** classe NotSupported: pubblico std: :eccezione ( ); pubblico: // ***************************************** ******* ********* // Stati // ***************************** ******* ************** classe NotConnectedState; classe ReadyState; classe ActiveState; classe State ( public: virtual ~State() ( ) virtual void connect(WebCamera*) ( lanciare NotSupported(); ) virtual void Disconnect(WebCamera* cam) ( std::cout<< "Деинициализируем камеру..." << std::endl; // ... cam->changeState(NotConnectedState::getInstance()); ) virtual void start(WebCamera*) ( lancia NotSupported(); ) virtual void stop(WebCamera*) ( lancia NotSupported(); ) virtual Frame getFrame(WebCamera*) ( lancia NotSupported(); ) protetto: State() ( ) ); // ************************************************ ** classe NotConnectedState: public State ( public: DECLARE_GET_INSTANCE(NotConnectedState) void connect(WebCamera* cam) ( std::cout<< "Инициализируем камеру..." << std::endl; // ... cam->changeState(ReadyState::getInstance()); ) void disconnette(WebCamera*) ( lancia NotSupported(); ) privato: NotConnectedState() ( ) ); // ************************************************ ** class ReadyState: public State ( public: DECLARE_GET_INSTANCE(ReadyState) void start(WebCamera* cam) ( std::cout<< "Запускаем видео-поток..." << std::endl; // ... cam->changeState(ActiveState::getInstance()); ) privato: ReadyState() ( ) ); // ************************************************ ** classe ActiveState: public State ( public: DECLARE_GET_INSTANCE(ActiveState) void stop(WebCamera* cam) ( std::cout<< "Останавливаем видео-поток..." << std::endl; // ... cam-> << "Получаем текущий кадр..." << std::endl; // ... return "Current frame"; } private: ActiveState() { } }; public: explicit WebCamera(int camID) : m_camID(camID), m_state(NotConnectedState::getInstance()) { } ~WebCamera() { try { disconnect(); } catch(const NotSupported& e) { // Обрабатываем исключение } catch(...) { // Обрабатываем исключение } } void connect() { m_state->connetti(questo); ) void disconnessione() ( m_state->disconnect(questo); ) void start() ( m_state->start(questo); ) void stop() ( m_state->stop(questo); ) Frame getFrame() ( return m_state ->getFrame(this); ) privato: void changeState(State* newState) ( m_state = newState; ) privato: int m_camID; Stato* m_stato; );

Attiro la tua attenzione sulla macro DECLARE_GET_INSTANCE. Naturalmente l'uso delle macro in C++ è sconsigliato. Tuttavia, questo vale nei casi in cui la macro agisce come un analogo di una funzione modello. In questo caso, dare sempre la preferenza a quest'ultimo.

Nel nostro caso, la macro ha lo scopo di definire una funzione statica necessaria per implementare . Pertanto, il suo utilizzo può essere considerato giustificato. Dopotutto, consente di ridurre la duplicazione del codice e non rappresenta una minaccia seria.

Dichiariamo le classi State nella classe principale: WebCamera. Per brevità, ho utilizzato le definizioni in linea delle funzioni membro di tutte le classi. Tuttavia, nelle applicazioni reali è meglio seguire le raccomandazioni sulla separazione della dichiarazione e dell'implementazione nei file h e cpp.

Le classi di stato vengono dichiarate all'interno di WebCamera in modo che abbiano accesso ai campi privati ​​di quella classe. Naturalmente, questo crea una connessione estremamente stretta tra tutte queste classi. Ma gli Stati risultano così specifici che il loro riutilizzo in altri contesti è fuori discussione.

La base della gerarchia delle classi di stato è la classe astratta WebCamera::State:

Stato della classe ( public: virtual ~State() ( ) virtual void connect(WebCamera*) ( Throw NotSupported(); ) virtual void Disconnect(WebCamera* cam) ( std::cout<< "Деинициализируем камеру..." << std::endl; // ... cam->changeState(NotConnectedState::getInstance()); ) virtual void start(WebCamera*) ( lancia NotSupported(); ) virtual void stop(WebCamera*) ( lancia NotSupported(); ) virtual Frame getFrame(WebCamera*) ( lancia NotSupported(); ) protetto: State() ( ) );

Tutte le sue funzioni membro corrispondono alle funzioni della classe WebCamera stessa. La delega diretta avviene:

Classe WebCamera ( // ... void connect() ( m_state->connect(this); ) void disconnessione() ( m_state->disconnect(this); ) void start() ( m_state->start(this); ) void stop() ( m_state->stop(this); ) Frame getFrame() ( return m_state->getFrame(this); ) // ... State* m_state; )

La caratteristica fondamentale è che l'oggetto State accetta un puntatore all'istanza WebCamera che lo chiama. Ciò consente di avere solo tre oggetti State per un numero arbitrariamente elevato di telecamere. Questa possibilità è ottenuta attraverso l'uso del pattern Singleton. Naturalmente, nel contesto dell’esempio, non otterrai un guadagno significativo da questo. Ma conoscere questa tecnica è comunque utile.

Di per sé, la classe WebCamera non fa praticamente nulla. Dipende completamente dai suoi Stati. E questi Stati, a loro volta, determinano le condizioni per lo svolgimento delle operazioni e forniscono il contesto necessario.

La maggior parte delle funzioni membro di WebCamera::State generano la nostra WebCamera::NotSupported . Questo è un comportamento predefinito del tutto appropriato. Ad esempio, se qualcuno tenta di inizializzare una telecamera quando è già stata inizializzata, riceverà naturalmente un'eccezione.

Allo stesso tempo, forniamo un'implementazione predefinita per WebCamera::State::disconnect(). Questo comportamento è adatto a due dei tre stati. Di conseguenza, evitiamo la duplicazione del codice.

Per modificare lo stato, utilizzare la funzione membro privata WebCamera::changeState() :

Void changeState(State* newState) ( m_state = newState; )

Passiamo ora all'attuazione di Stati specifici. Per WebCamera::NotConnectedState è sufficiente sovrascrivere le operazioni connect() e disconnessione():

Classe NotConnectedState: public Stato ( public: DECLARE_GET_INSTANCE(NotConnectedState) void connect(WebCamera* cam) ( std::cout<< "Инициализируем камеру..." << std::endl; // ... cam->changeState(ReadyState::getInstance()); ) void disconnette(WebCamera*) ( lancia NotSupported(); ) privato: NotConnectedState() ( ) );

Per ogni Stato è possibile creare una singola istanza. Questo ci viene garantito dichiarando un costruttore privato.

Un altro elemento importante dell'implementazione presentata è che ci spostiamo in un nuovo Stato solo in caso di successo. Ad esempio, se si verifica un errore durante l'inizializzazione della fotocamera, è troppo presto per entrare in ReadyState. L'idea principale è la completa corrispondenza tra lo stato attuale della fotocamera (nel nostro caso) e l'oggetto State.

Quindi, la fotocamera è pronta per l'uso. Creiamo la classe WebCamera::ReadyState State corrispondente:

Classe ReadyState: public Stato ( public: DECLARE_GET_INSTANCE(ReadyState) void start(WebCamera* cam) ( std::cout<< "Запускаем видео-поток..." << std::endl; // ... cam->changeState(ActiveState::getInstance()); ) privato: ReadyState() ( ) );

Dallo stato Ready possiamo entrare nello stato attivo di Frame Capture. A questo scopo viene fornita l'operazione start(), che abbiamo implementato.

Finalmente abbiamo raggiunto l'ultimo stato logico della fotocamera, WebCamera::ActiveState:

Classe ActiveState: public State ( public: DECLARE_GET_INSTANCE(ActiveState) void stop(WebCamera* cam) ( std::cout<< "Останавливаем видео-поток..." << std::endl; // ... cam->changeState(ReadyState::getInstance()); ) Frame getFrame(WebCamera*) ( std::cout<< "Получаем текущий кадр..." << std::endl; // ... return "Current frame"; } private: ActiveState() { } };

In questo stato, puoi interrompere l'acquisizione di fotogrammi utilizzando stop() . Di conseguenza, verremo riportati alla WebCamera::ReadyState. Inoltre, possiamo ricevere fotogrammi che si accumulano nel buffer della fotocamera. Per semplicità, per “frame” intendiamo una stringa regolare. In realtà, questo sarà una sorta di array di byte.

Ora possiamo scrivere un tipico esempio di lavoro con la nostra classe WebCamera:

Int main() ( WebCamera cam(0); try ( // cam in NotConnectedState cam.connect(); // cam in ReadyState cam.start(); // cam in ActiveState std::cout<< cam.getFrame() << std::endl; cam.stop(); // Можно было сразу вызвать disconnect() // cam в Состоянии ReadyState cam.disconnect(); // cam в Состоянии NotConnectedState } catch(const WebCamera::NotSupported& e) { // Обрабатываем исключение } catch(...) { // Обрабатываем исключение } return 0; }

Questo è ciò che verrà visualizzato come risultato sulla console:

Inizializza la telecamera... Avvia il flusso video... Ottieni il fotogramma corrente... Fotogramma corrente Interrompi il flusso video... Deinizializza la telecamera...

Ora proviamo a provocare un errore. Chiamiamo connect() due volte di seguito:

Int main() ( WebCamera cam(0); try ( // cam nel NotConnectedState cam.connect(); // cam nel ReadyState // Ma per questo stato non è prevista l'operazione connect()! cam.connect( ); // Genera un'eccezione NotSupported ) catch(const WebCamera::NotSupported& e) ( std::cout<< "Произошло исключение!!!" << std::endl; // ... } catch(...) { // Обрабатываем исключение } return 0; }

Ecco cosa ne viene fuori:

Inizializzazione della fotocamera... Si è verificata un'eccezione!!! Deinizializziamo la fotocamera...

Tieni presente che la fotocamera era ancora deinizializzata. La chiamata disconnessione() si è verificata nel distruttore della WebCamera. Quelli. lo stato interno dell'oggetto rimane assolutamente corretto.

conclusioni

Utilizzando il modello State, puoi trasformare in modo univoco un diagramma di stato in codice. A prima vista, l'implementazione si è rivelata dettagliata. Tuttavia, siamo arrivati ​​​​a una chiara divisione in possibili contesti per lavorare con la classe principale WebCamera. Di conseguenza, durante la scrittura di ogni singolo Stato, abbiamo potuto concentrarci su un compito ristretto. E questo è il modo migliore per scrivere codice chiaro, comprensibile e affidabile.