Con (fuori) l'app si arresta in modo anomalo, per favore!

Pubblicato: 2020-02-12

I programmatori cercano di evitare arresti anomali nel loro codice. Se qualcuno usa la sua applicazione, non dovrebbe interrompersi o chiudersi inaspettatamente. Questa è una delle misurazioni più semplici della qualità: se un'app si arresta spesso in modo anomalo, probabilmente non è fatta bene.

Gli arresti anomali dell'app si verificano quando un programma sta per eseguire qualcosa di non definito o errato come dividere un valore per zero o accedere a risorse limitate su una macchina. Potrebbe anche essere fatto esplicitamente dal programmatore che ha scritto l'applicazione. "Non accadrà mai, quindi lo salterò" - è un pensiero abbastanza comune e non del tutto irragionevole. Ci sono alcuni casi che semplicemente non possono verificarsi, mai, finché... non succede.

Promesse non mantenute

Uno dei casi più comuni in cui sappiamo che qualcosa non può accadere sono le API. Abbiamo concordato tra back-end e front-end: questa è l'unica risposta del server che puoi ottenere per questa richiesta. I manutentori di questa libreria hanno documentato questo comportamento della funzione. La funzione non può fare altro. Entrambi i modi di pensare sono corretti, ma entrambi possono causare problemi.

Quando utilizzi una libreria puoi fare affidamento su strumenti linguistici per aiutarti a gestire tutti i casi possibili. Se il linguaggio che usi è privo di qualsiasi forma di controllo del tipo o analisi statica, devi occupartene da solo. Tuttavia, puoi verificarlo prima della spedizione nell'ambiente di produzione, quindi non è un grosso problema. Può essere difficile, ma leggi i log delle modifiche prima di aggiornare le dipendenze e scrivere unit test, giusto? O usi o crei una libreria, più la digitazione rigorosa puoi fornire, meglio è per il tuo codice e altri programmatori.

La comunicazione backend-frontend è un po' più difficile. Spesso è accoppiato in modo lasco, quindi il cambio da un lato può essere fatto facilmente senza essere consapevoli di come influenzerà l'altro lato. I cambiamenti sul back-end spesso possono infrangere le tue supposizioni sul front-end ed entrambi sono spesso distribuiti separatamente. Deve finire male. Siamo solo umani e a volte capita che non abbiamo capito l'altra parte o ci siamo dimenticati di raccontare loro quel piccolo cambiamento. Ancora una volta, non è un grosso problema con una corretta gestione della rete: la risposta di decodifica fallirà e sappiamo come gestirla. Anche il miglior codice di decodifica può essere influenzato da una cattiva progettazione però...

Funzioni parziali. Cattivo design.

"Qui avremo due variabili booleane: 'isActive' e 'canTransfer', ovviamente non puoi trasferire quando non è attivo, ma questo è solo un dettaglio." Qui inizia, il nostro cattivo design che può colpire duramente. Ora qualcuno creerà una funzione con questi due argomenti ed elaborerà alcuni dati basati su di esso. La soluzione più semplice è... semplicemente andare in crash su uno stato non valido, non dovrebbe mai accadere, quindi non dovrebbe interessarci. A volte ci preoccupiamo anche e lasciamo qualche commento per risolverlo in seguito o per chiedere cosa dovrebbe accadere, ma alla fine può essere spedito senza completare quell'attività.

 // pseudocodice
function doTransfer(Bool isActive, Bool canTransfer) {
  If ( isActive e canTransfer ) {
    // fai qualcosa per il trasferimento disponibile
  } else if ( not isActive e not canTransfer ) {
    // fai qualcosa per il trasferimento non disponibile
  } else if ( isActive e non canTransfer ) {
    // fai qualcosa per il trasferimento non disponibile
  } else { // alias (non isActive e canTransfer)
    // ci sono quattro possibili stati
    // questo non dovrebbe accadere, il trasferimento non dovrebbe essere disponibile quando non è attivo
    incidente()
  }
}

Questo esempio potrebbe sembrare sciocco, ma a volte potresti trovarti in quel tipo di trappola che è un po' più difficile da individuare e risolvere rispetto a questo. Finirai con qualcosa chiamato funzione parziale. Questa è una funzione definita solo per alcuni dei suoi possibili input che ignorano o si bloccano con altri. Dovresti sempre evitare le funzioni parziali (tieni presente che nei linguaggi tipizzati dinamicamente la maggior parte delle funzioni può essere trattata come parziale). Se la tua lingua non è in grado di garantire un comportamento corretto con il controllo del tipo e l'analisi statica, potrebbe bloccarsi dopo qualche tempo in modo imprevisto. Il codice è in continua evoluzione e le ipotesi di ieri potrebbero non essere valide oggi.

Fallisci velocemente. Fallisci spesso.

Come puoi proteggerti? La miglior difesa è l'attacco! C'è questo simpatico detto: “Falli velocemente. Fallisci spesso". Ma non eravamo semplicemente d'accordo sul fatto che dovremmo evitare arresti anomali delle app, funzioni parziali e cattiva progettazione? Erlang OTP offre ai programmatori un vantaggio mitico che guarirà se stesso dopo stati imprevisti e si aggiornerà durante l'esecuzione. Possono permetterselo, ma non tutti hanno questo tipo di lusso. Allora perché dovremmo fallire velocemente e spesso?

Prima di tutto, per trovare quegli stati e comportamenti inaspettati . Se non controlli se lo stato della tua app è corretto, potrebbe portare a risultati ancora peggiori dell'arresto anomalo!

In secondo luogo, per aiutare altri programmatori a collaborare sulla stessa base di codice . Se sei solo in un progetto in questo momento, potrebbe esserci qualcun altro dopo di te. Potresti dimenticare alcuni presupposti e requisiti. È piuttosto comune non leggere la documentazione fornita fino a quando tutto non funziona o non documentare affatto metodi e tipi interni. In quello stato, qualcuno chiama una delle funzioni disponibili con un valore inaspettato ma valido. Ad esempio, supponiamo di avere una funzione "wait" che accetta qualsiasi valore intero e attende quella quantità di secondi. E se qualcuno gli passa "-17"? Se non si arresta in modo anomalo immediatamente dopo averlo fatto, potrebbero verificarsi errori gravi e stati non validi. Aspetta per sempre o per niente?

La parte più importante di un crash intenzionale è farlo con grazia . Se si arresta in modo anomalo l'applicazione è necessario fornire alcune informazioni per consentire una diagnosi. È abbastanza facile quando utilizzi un debugger, ma dovresti avere un modo per segnalare arresti anomali dell'app senza di esso. È possibile utilizzare i sistemi di registrazione per mantenere tali informazioni tra l'avvio dell'applicazione o per esaminarle esternamente.

La seconda parte più importante dell'arresto anomalo intenzionale è evitare che nell'ambiente di produzione...

Non fallire. Mai.

Alla fine spedirai il tuo codice. Non puoi renderlo perfetto, spesso è troppo costoso anche solo pensare di fare garanzie di correttezza. Tuttavia, dovresti assicurarti che non si comporti male o si arresti in modo anomalo. Come puoi ottenerlo dal momento che abbiamo già deciso di andare in crash velocemente e spesso?

Una parte importante dell'arresto anomalo intenzionale è farlo solo in ambienti non di produzione . Dovresti usare le asserzioni che vengono eliminate nelle build di produzione della tua applicazione. Ciò aiuterà durante lo sviluppo e consentirà di individuare i problemi senza influire sugli utenti finali. Tuttavia, è comunque meglio arrestarsi in modo anomalo a volte per evitare stati delle applicazioni non validi. Come possiamo ottenerlo se abbiamo già creato funzioni parziali?

Rendi gli stati non definiti e non validi impossibili da rappresentare e fallo altrimenti a quelli validi. Potrebbe sembrare facile, ma richiede molta riflessione e lavoro. Non importa quanto sia, è sempre meno che cercare bug, apportare correzioni temporanee e... utenti fastidiosi. Renderà automaticamente meno probabile che si verifichino alcune delle funzioni parziali.

 // pseudocodice
funzione doTransfer(Stato stato) {
  interruttore (stato) {
    caso State.canTransfer {
      // fai qualcosa per il trasferimento disponibile
    }
    case State.cannotTransfer {
      // fai qualcosa per il trasferimento non disponibile
    }
    case State.notActive {
      // fai qualcosa per il trasferimento non disponibile
    }
    // È impossibile rappresentare il trasferimento disponibile senza essere attivo
    // ci sono solo tre stati possibili
  }
}

Come puoi rendere impossibili gli stati non validi? Scegliamo due degli esempi precedenti. Nel caso delle nostre due variabili booleane 'isActive' e 'canTransfer' possiamo cambiarle in un'unica enumerazione. Rappresenterà in modo esaustivo tutti gli stati possibili e validi. Anche allora qualcuno può inviare variabili non definite, ma è molto più facile da gestire. Sarà un valore non valido che non verrà importato nel nostro programma invece di passare uno stato non valido rendendo tutto più difficile.

La nostra funzione di attesa può anche essere migliorata bene in linguaggi fortemente tipizzati. Possiamo fare in modo che utilizzi solo numeri interi senza segno in input. Questo da solo risolverà tutti i nostri problemi poiché gli argomenti non validi verranno eliminati dal compilatore. Ma cosa succede se la tua lingua non ha tipi? Abbiamo alcune possibili soluzioni. Primo: basta arrestare in modo anomalo, questa funzione non è definita per i numeri negativi e non faremo cose non valide o non definite. Dovremo trovarne un uso non valido durante i test. Gli unit test (che comunque dovremmo fare) saranno davvero importanti qui. Secondo: potrebbe essere rischioso, ma a seconda del contesto potrebbe essere utile. Possiamo ricorrere a valori validi mantenendo l'asserzione nelle build non di produzione per correggere gli stati non validi quando possibile. Potrebbe non essere una buona soluzione per funzioni come questa, ma se mettiamo il valore assoluto di intero invece eviteremo arresti anomali dell'app. A seconda del linguaggio concreto, potrebbe anche essere una buona idea lanciare/sollevare qualche errore/eccezione. Potrebbe valere la pena di eseguire il fallback, se possibile, anche quando l'utente vede un errore è un'esperienza molto migliore rispetto all'arresto anomalo.

Facciamo un altro esempio qui. Se lo stato dei dati utente nella tua applicazione frontend sta per essere non valido in alcuni casi, potrebbe essere meglio forzare un logout e ottenere nuovamente dati validi dal server invece di andare in crash. L'utente potrebbe essere costretto a farlo comunque o può essere catturato all'interno di un ciclo di arresto anomalo infinito. Ancora una volta: dovremmo affermare e arrestare in modo anomalo tali situazioni in ambienti non di produzione, ma non lasciare che i tuoi utenti siano tester esterni.

Riepilogo

A nessuno piacciono le applicazioni che si bloccano e instabili. Non ci piace crearli o usarli. Non riuscendo velocemente con asserzioni che forniscono diagnosi utili durante lo sviluppo e i test, si individueranno presto molti problemi. Il fallback su stati validi in produzione renderà la tua app molto più stabile. Rendere irrappresentabili gli stati non validi eliminerà un'intera classe di problemi. Concediti un po' più di tempo per pensare prima dello sviluppo su come eliminare e ripiegare sugli stati non validi e un po' di più durante la scrittura per includere alcune affermazioni. Puoi iniziare a migliorare le tue applicazioni oggi stesso!

Leggi di più:

  • Progettazione per contratto
  • Tipo di dati algebrici