Come recuperare dalla rottura della macchina a stati finiti?


13

La mia domanda può sembrare molto scientifica, ma penso che sia un problema comune e speriamo che sviluppatori e programmatori esperti abbiano qualche consiglio per evitare il problema che menziono nel titolo. A proposito, quello che descrivo qui sotto è un vero problema che sto cercando di risolvere in modo proattivo nel mio progetto iOS, voglio evitarlo a tutti i costi.

Per macchina a stati finiti intendo questo> Ho un'interfaccia utente con alcuni pulsanti, diversi stati di sessione rilevanti per quell'interfaccia utente e ciò che rappresenta questa interfaccia utente, ho alcuni dati quali valori sono parzialmente visualizzati nell'interfaccia utente, ricevo e gestisco alcuni trigger esterni (rappresentato dai callback dei sensori). Ho creato diagrammi di stato per mappare meglio gli scenari rilevanti che sono desiderabili e consentiti nell'interfaccia utente e nell'applicazione. Mentre implemento lentamente il codice, l'app inizia a comportarsi sempre più come dovrebbe. Tuttavia, non sono molto sicuro che sia abbastanza robusto. I miei dubbi derivano dal guardare il mio modo di pensare e di attuazione mentre procede. Ero fiducioso di avere tutto coperto, ma era abbastanza per fare alcuni test brutali nell'interfaccia utente e ho capito rapidamente che ci sono ancora lacune nel comportamento .. Li ho riparati. Tuttavia, poiché ogni componente dipende e si comporta in base all'input di qualche altro componente, un determinato input dell'utente o una sorgente esterna innesca una catena di eventi, lo stato cambia ... ecc. Ho diversi componenti e ognuno si comporta come questo Trigger ricevuto in input -> trigger e il suo mittente analizzato -> output qualcosa (un messaggio, un cambio di stato) basato sull'analisi

Il problema è che questo non è completamente autonomo e i miei componenti (un elemento del database, uno stato della sessione, lo stato di alcuni pulsanti) ... POTREBBERO essere cambiati, influenzati, cancellati o altrimenti modificati, al di fuori dell'ambito della catena di eventi o scenario desiderabile. (il telefono si arresta in modo anomalo, la batteria si scarica improvvisamente) Ciò introdurrà una situazione non valida nel sistema, da cui il sistema NON POTREBBE POTREBBE essere IN GRADO di recuperare. Vedo questo (anche se le persone non si rendono conto che questo è il problema) in molte delle mie app della concorrenza che si trovano su Apple Store, i clienti scrivono cose come questa> "Ho aggiunto tre documenti e dopo essere andato lì e là, non riesco ad aprirli, anche se a vederli. " o "Ho registrato video ogni giorno, ma dopo aver registrato un video troppo log, non riesco a disattivare i sottotitoli .. e il pulsante per i sottotitoli non fa '

Questi sono solo esempi abbreviati, i clienti spesso lo descrivono in modo più dettagliato .. dalle descrizioni e dal comportamento descritti in essi, presumo che la particolare app abbia un guasto a FSM.

Quindi la domanda finale è: come posso evitarlo e come proteggere il sistema dal blocco stesso?

EDIT> Sto parlando nel contesto della vista di un viewcontroller sul telefono, intendo una parte dell'applicazione. Capisco il modello MVC, ho moduli separati per funzionalità distinte ... tutto ciò che descrivo è rilevante per un canvas sull'interfaccia utente.


2
Sembra un caso per i test unitari!
Michael K,

Risposte:


7

Sono sicuro che lo sai già, ma per ogni evenienza:

  1. Assicurarsi che ogni nodo nel diagramma di stato abbia un arco in uscita per OGNI tipo di input legale (o dividere gli input in classi, con un arco in uscita per ogni classe di input).

    Ogni esempio che ho visto di una macchina a stati usa solo un arco in uscita per QUALSIASI input errato.

    Se non esiste una risposta definita su cosa farà ogni volta un input, si tratta di una condizione di errore o di un altro input mancante (che dovrebbe risultare costantemente un arco per gli input che vanno su un nuovo nodo).

    Se un nodo non ha un arco per un tipo di input, allora è un presupposto che l'input non si verificherà mai nella vita reale (questa è una potenziale condizione di errore che non verrebbe gestita dalla macchina a stati).

  2. Assicurarsi che la macchina a stati possa accettare o seguire solo UN arco in risposta all'ingresso ricevuto (non più di un arco).

    Se vi sono diversi tipi di scenari di errore o tipi di input che non possono essere identificati in fase di progettazione della macchina a stati, gli scenari di errore e gli input sconosciuti dovrebbero passare a uno stato con un percorso completamente separato separato dagli "archi normali".

    Ad esempio, se si riceve un errore o uno sconosciuto in uno stato "noto", gli archi seguiti a seguito dell'input di gestione degli errori / sconosciuto non dovrebbero ritornare in uno stato in cui si troverebbe la macchina se ricevessero solo input noti.

  3. Una volta raggiunto uno stato terminale (fine) non dovresti essere in grado di tornare a uno stato non terminale solo allo stato iniziale (iniziale).

  4. Per una macchina a stati non dovrebbe esserci più di uno stato iniziale o iniziale (basato sugli esempi che ho visto).

  5. Sulla base di ciò che ho visto, una macchina a stati può rappresentare solo lo stato di un problema o di uno scenario.
    Non dovrebbero mai esserci più stati possibili contemporaneamente in un diagramma di stato.
    Se vedo il potenziale per più stati simultanei, questo mi dice che ho bisogno di dividere il diagramma di stato in 2 o più macchine a stati separati che hanno il potenziale di modificare indipendentemente ogni stato.


9

Il punto di una macchina a stati finiti è che ha regole esplicite per tutto ciò che può accadere in uno stato. Ecco perché è finito .

Per esempio:

if a:
  print a
elif b:
  print b

Non è finito, perché potremmo ottenere input c. Questo:

if a:
  print a
elif b:
  print b
else:
  print error

è finito, perché viene tenuto conto di tutti i possibili input. Ciò tiene conto dei possibili input per uno stato , che può essere separato dal controllo degli errori. Immagina una macchina a stati con i seguenti stati:

No money state. 
Not enough money state.
Pick soda state.

All'interno degli stati definiti, tutti i possibili input sono gestiti per denaro inserito e soda selezionata. L'interruzione dell'alimentazione è esterna alla macchina a stati e "non esiste". Una macchina a stati può gestire solo input per gli stati che ha, quindi hai due scelte.

  1. Garantire che ogni azione sia atomica. La macchina può avere una perdita di potenza totale e lasciare comunque tutto in uno stato stabile e corretto.
  2. Estendi i tuoi stati per includere il problema sconosciuto e fai in modo che gli errori ti portino a questo stato, in cui vengono gestiti i problemi.

Per riferimento, l' articolo wiki sulle macchine a stati è approfondito. Suggerisco anche Code Complete , per i capitoli sulla creazione di software stabile e affidabile.


"Potremmo ottenere input c" - ecco perché i linguaggi typesafe sono così importanti. Se il tuo tipo di input è un bool, puoi ottenere truee false, ma nient'altro. Anche allora è importante capire i tuoi tipi - ad es. Tipi nullable, virgola mobile NaN, ecc.
MSalters

5

Le espressioni regolari sono implementate come macchine a stati finiti. La tabella di transizione generata dal codice della libreria avrà uno stato di errore incorporato, per gestire cosa succede se l'input non corrisponde al modello. C'è almeno una transizione implicita allo stato di fallimento da quasi tutti gli altri stati.

Le grammatiche del linguaggio di programmazione non sono FSM, ma i generatori di parser (come Yacc o bison) generalmente hanno un modo per inserire uno o più stati di errore, in modo che input imprevisti possano far finire il codice generato in uno stato di errore.

Sembra che il tuo FSM abbia bisogno di uno stato di errore o di uno stato di fallimento o dell'equivalente morale, insieme a transizioni esplicite (per i casi che prevedi) e implicite (per i casi che non prevedi) a uno degli stati di errore o di errore.


Perdonatemi, se la mia domanda sembra sciocca, poiché non ho un'istruzione formale in CS e sto imparando la programmazione da qualche mese. Ciò significa che, quando ho lasciato dire un metodo gestore, per un evento push per un pulsante, e in quel metodo ho una struttura di condizionamento if-else-switch moderatamente complicata (20-30 righe di codice), che Dovrei sempre gestire gli input non desiderati in modo esplicito? O intendi a livello "globale"? Dovrei avere una classe separata a guardare questo FSM, e quando si verifica il problema, ripristinerebbe i valori e gli stati?
Earl Grey,

Spiegare i parser generati da Yacc o Bison è al di là di me, ma di solito si affrontano casi noti e quindi si ha un piccolo blocco di codice per "tutto il resto va allo stato di errore o di errore". Il codice per lo stato di errore / fallimento farebbe tutto il ripristino. Forse devi avere un valore aggiuntivo che dice perché sei arrivato allo stato di errore.
Bruce Ediger,

Il tuo FSM dovrebbe avere almeno uno stato per errori o più stati di errore per diversi tipi di errori.
whatsisname

3

Il modo migliore per evitarlo è il test automatico .

L'unico modo per avere una reale fiducia in ciò che il codice fa in base a determinati input è testarli. Puoi fare clic nella tua applicazione e provare a fare le cose in modo errato, ma ciò non si adatta bene per assicurarti di non avere regressioni. Invece, puoi creare un test che passa un input errato in un componente del tuo codice e garantisce che lo gestisca in modo sano.

Questo non sarà in grado di dimostrare che la macchina a stati non può mai essere rotta, ma ti dirà che molti dei casi comuni sono gestiti correttamente e non rompono altre cose.


2

quello che stai cercando è la gestione delle eccezioni. Una filosofia di progettazione per evitare di rimanere in uno stato incoerente è documentata come the way of the samurai: tornare vittorioso o non tornare. In altre parole: un componente dovrebbe controllare tutti i suoi input e assicurarsi che sarà in grado di elaborarli normalmente. In caso contrario, dovrebbe sollevare un'eccezione contenente informazioni utili.

Una volta sollevata un'eccezione, si crea una bolla. Dovresti definire un livello di gestione degli errori che saprà cosa fare. Se un file utente è danneggiato, spiegare al client che i dati vengono persi e ricreare un file vuoto e pulito.

La parte importante qui è tornare a uno stato funzionante ed evitare la propagazione degli errori. Al termine, è possibile lavorare su singoli componenti per renderli più robusti.

Non sono un esperto di obiettivi-c, ma questa pagina dovrebbe essere un buon punto di partenza:

http://developer.apple.com/library/ios/#documentation/cocoa/conceptual/objectivec/Chapters/ocExceptionHandling.html


1

Dimentica la tua macchina a stati finiti. Quello che hai qui è una grave situazione multi-threading . È possibile premere qualsiasi pulsante in qualsiasi momento e i trigger esterni potrebbero spegnersi in qualsiasi momento. I pulsanti sono probabilmente tutti su un thread, ma un trigger potrebbe attivarsi letteralmente nello stesso istante di uno dei pulsanti o uno, molti o tutti gli altri trigger.

Quello che devi fare è determinare il tuo stato nel momento in cui decidi di agire. Ottieni tutti i pulsanti e gli stati di attivazione. Salvali nelle variabili locali . I valori originali possono cambiare ogni volta che li guardi. Quindi agisci sulla situazione così come l'hai. È un'istantanea dell'aspetto del sistema in un punto. Un millisecondo dopo potrebbe apparire molto diverso, ma con il multi-threading non esiste una vera "ora" a cui puoi aggrapparti, solo un'immagine che hai salvato nelle variabili locali.

Quindi devi rispondere al tuo stato - storico - salvato. Tutto è risolto e dovresti avere un'azione per tutti i possibili stati. Non prenderà in considerazione le modifiche apportate tra il momento in cui hai realizzato l'istantanea e il momento in cui visualizzi i risultati, ma questa è la vita nel mondo multi-thread. E potrebbe essere necessario utilizzare la sincronizzazione per evitare che l'istantanea sia troppo sfocata. (Non puoi essere aggiornato, ma puoi avvicinarti a ottenere l'intero stato da un particolare istante nel tempo.)

Leggi su multi-threading. Hai molto da imparare. E a causa di questi trigger, non penso che tu possa usare molti dei trucchi spesso forniti per rendere semplice l'elaborazione parallela ("Discussioni dei lavoratori" e simili). Non stai facendo "elaborazione parallela"; non stai cercando di utilizzare il 75% di 8 core. Stai utilizzando l'1% dell'intera CPU, ma hai thread altamente indipendenti e altamente interagenti e ci vorrà molto pensiero per sincronizzarli e impedire che la sincronizzazione blocchi il sistema.

Test su macchine single core e multi-core; Ho scoperto che si comportano in modo piuttosto diverso con il multi-threading. Le macchine single-core presentano meno bug multi-threading, ma questi bug sono molto più strani. (Anche se le macchine multi-core ti faranno impazzire finché non ti abituerai.)

Un'ultima spiacevole idea: non è roba facile da testare. Dovrai generare trigger casuali e premere i pulsanti e lasciare che il sistema si apra per un po 'per vedere cosa succede. Il codice multi-thread non è deterministico. Qualcosa può fallire una volta su un miliardo di corse, solo perché il tempo è scaduto per un nanosecondo. Inserisci le dichiarazioni di debug (con attente dichiarazioni if ​​per evitare 999.999.999 messaggi non necessari) e devi fare un miliardo di corse solo per ottenere un messaggio utile. Fortunatamente, le macchine sono molto veloci in questi giorni.

Mi dispiace lasciarti tutto questo all'inizio della tua carriera. Eventualmente qualcuno troverà un'altra risposta con un modo per aggirare tutto questo (penso che ci siano cose là fuori che potrebbero domare i grilletti, ma hai ancora il conflitto grilletto / pulsante). In tal caso, questa risposta ti farà almeno sapere cosa ti stai perdendo. In bocca al lupo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.