Qualcosa che muta alla fine non manipola lo stato?
Sì, ma se è dietro una funzione membro di una piccola classe che è l'unica entità nell'intero sistema in grado di manipolare il suo stato privato, tale stato ha un ambito molto ristretto.
Che cosa dovresti avere a che fare con il minor stato possibile?
Dal punto di vista della variabile: il minor numero di righe di codice dovrebbe essere in grado di accedervi il più possibile. Restringere al minimo l'ambito della variabile.
Dal punto di vista della linea di codice: il minor numero possibile di variabili dovrebbe essere accessibile da quella linea di codice. Restringere il numero di variabili a cui la riga di codice può eventualmente accedere (non importa nemmeno molto se vi accede, tutto ciò che conta è se può ).
Le variabili globali sono così dannose perché hanno il massimo scopo. Anche se sono accessibili da 2 righe di codice in una base di codice, dalla riga del POV del codice, una variabile globale è sempre accessibile. Dal POV della variabile, una variabile globale con collegamento esterno è accessibile a ogni singola riga di codice nell'intera base di codice (o a ogni singola riga di codice che include comunque l'intestazione). Nonostante sia effettivamente accessibile solo da 2 righe di codice, se la variabile globale è visibile a 400.000 righe di codice, il tuo elenco immediato di sospetti quando scopri che è stato impostato su uno stato non valido avrà quindi 400.000 voci (forse rapidamente ridotto a 2 voci con strumenti, ma l'elenco immediato avrà 400.000 sospetti e questo non è un punto di partenza incoraggiante).
Allo stesso modo, anche se una variabile globale inizia a essere modificata solo da 2 righe di codice nell'intera base di codice, la sfortunata tendenza delle basi di codice a evolversi all'indietro tenderà ad aumentare drasticamente quel numero, semplicemente perché può aumentare il numero di gli sviluppatori, frenetici nel rispettare le scadenze, vedono questa variabile globale e si rendono conto che possono prendere scorciatoie attraverso di essa.
In un linguaggio impuro come C ++, la gestione dello stato non è davvero ciò che stai facendo?
In gran parte sì, a meno che tu non stia usando C ++ in un modo molto esotico che ti ha a che fare con strutture di dati immutabili su misura e pura programmazione funzionale in tutto - è anche spesso la fonte della maggior parte dei bug quando la gestione dello stato diventa complessa e la complessità è spesso in funzione della visibilità / esposizione di quello stato.
E quali altri modi per gestire il minor stato possibile oltre a limitare la durata variabile?
Tutti questi sono nel regno della limitazione dell'ambito di una variabile, ma ci sono molti modi per farlo:
- Evita le variabili globali grezze come la peste. Perfino qualche stupida funzione setter / getter globale restringe drasticamente la visibilità di quella variabile, e almeno consente un modo per mantenere invarianti (es: se alla variabile globale non dovrebbe mai essere permesso di avere un valore negativo, il setter può mantenere quell'invariante). Naturalmente, anche un design setter / getter in aggiunta a quella che altrimenti sarebbe una variabile globale è un design piuttosto scadente, il mio punto è che è ancora molto meglio.
- Rendi le tue lezioni più piccole quando possibile. Una classe con centinaia di funzioni membro, 20 variabili membro e 30.000 righe di codice che la implementano avrebbe variabili private piuttosto "globali", poiché tutte quelle variabili sarebbero accessibili alle sue funzioni membro che consistono in 30k righe di codice. Si potrebbe dire che la "complessità dello stato" in quel caso, pur scontando le variabili locali in ciascuna funzione membro, lo è
30,000*20=600,000
. Se ci fossero 10 variabili globali accessibili oltre a ciò, la complessità dello stato potrebbe essere simile 30,000*(20+10)=900,000
. Una sana "complessità di stato" (il mio tipo personale di metrica inventata) dovrebbe essere nelle migliaia o sotto per le classi, non decine di migliaia, e sicuramente non centinaia di migliaia. Per le funzioni gratuite, diciamo centinaia o meno prima di iniziare a ottenere seri mal di testa nella manutenzione.
- Allo stesso modo di cui sopra, non implementare qualcosa come una funzione membro o funzione amico che può essere altrimenti non membro, non amico usando solo l'interfaccia pubblica della classe. Tali funzioni non possono accedere alle variabili private della classe e quindi ridurre il potenziale di errore riducendo l'ambito di tali variabili private.
- Evita di dichiarare le variabili molto prima che siano effettivamente necessarie in una funzione (ad esempio, evita lo stile C legacy che dichiara tutte le variabili all'inizio di una funzione anche se sono necessarie solo molte righe di seguito). Se usi questo stile comunque, cerca almeno funzioni più brevi.
Oltre le variabili: effetti collaterali
Molte di queste linee guida che ho elencato sopra stanno affrontando l'accesso diretto allo stato grezzo (variabili). Tuttavia, in una base di codice sufficientemente complessa, restringere l'ambito delle variabili non sarà sufficiente per ragionare facilmente sulla correttezza.
Si potrebbe avere, per esempio, una struttura di dati centrale, dietro un'interfaccia totalmente SOLIDA, astratta, pienamente in grado di mantenere perfettamente gli invarianti e finire ancora per affrontare un sacco di dolore a causa della vasta esposizione di questo stato centrale. Un esempio di stato centrale che non è necessariamente accessibile a livello globale ma semplicemente accessibile è il grafico della scena centrale di un motore di gioco o la struttura dei dati del livello centrale di Photoshop.
In tali casi, l'idea di "stato" va oltre le variabili grezze, e solo alle strutture di dati e cose del genere. Allo stesso modo aiuta a ridurne l'ambito (ridurre il numero di linee che possono chiamare funzioni che le mutano indirettamente).
Notate come ho deliberatamente contrassegnato anche l'interfaccia come rossa qui, poiché dal livello architettonico ampio e ingrandito, l'accesso a quell'interfaccia è ancora stato mutante, anche se indirettamente. La classe può mantenere invarianti come risultato dell'interfaccia, ma ciò va solo così lontano in termini della nostra capacità di ragionare sulla correttezza.
In questo caso, la struttura centrale dei dati si trova dietro un'interfaccia astratta che potrebbe non essere nemmeno accessibile a livello globale. Potrebbe semplicemente essere iniettato e quindi indirettamente mutato (attraverso le funzioni membro) da un carico di funzioni nella tua complessa base di codice.
In tal caso, anche se la struttura dei dati mantiene perfettamente i propri invarianti, possono accadere cose strane a un livello più ampio (es: un lettore audio può mantenere tutti i tipi di invarianti come il livello del volume che non va mai al di fuori dell'intervallo dello 0% per 100%, ma ciò non lo protegge dall'utente che preme il pulsante di riproduzione e che ha una clip audio casuale diversa da quella che ha caricato più di recente e inizia a riprodurre quando viene attivato un evento che provoca il rimpasto della playlist in modo valido ma comportamento indesiderato, ancora glitch dal punto di vista dell'utente).
Il modo per proteggersi in questi scenari complessi è quello di "strozzare" i posti nella base di codice che possono chiamare funzioni che alla fine causano effetti collaterali esterni anche da questo tipo di visione più ampia del sistema che va oltre lo stato grezzo e oltre le interfacce.
Per quanto strano possa sembrare, puoi vedere che nessuno "stato" (mostrato in rosso, e questo non significa "variabile grezza", significa solo un "oggetto" e forse anche dietro un'interfaccia astratta) è accessibile da numerosi luoghi . Ciascuna delle funzioni ha accesso a uno stato locale che è anche accessibile da un programma di aggiornamento centrale e lo stato centrale è accessibile solo al programma di aggiornamento centrale (rendendolo non più centrale ma piuttosto di natura locale).
Questo è solo per basi di codice davvero complesse, come un gioco che si estende su 10 milioni di righe di codice, ma può aiutare enormemente nel ragionamento sulla correttezza del tuo software e scoprire che le tue modifiche producono risultati prevedibili, quando limiti / colli di bottiglia significativamente il numero di luoghi che possono mutare stati critici su cui l'intera architettura ruota per funzionare correttamente.
Oltre alle variabili non elaborate vi sono effetti collaterali esterni e gli effetti collaterali esterni sono una fonte di errore anche se limitati a una manciata di funzioni membro. Se un carico di funzioni può richiamare direttamente quelle poche funzioni membro, allora c'è un carico di funzioni nel sistema che può indirettamente causare effetti collaterali esterni e che aumenta la complessità. Se c'è un solo posto nella base di codice che ha accesso a quelle funzioni membro e che un percorso di esecuzione non è innescato da eventi sporadici dappertutto, ma viene invece eseguito in modo molto controllato e prevedibile, allora riduce la complessità.
Complessità statale
Anche la complessità dello stato è un fattore piuttosto importante da tenere in considerazione. Una struttura semplice, ampiamente accessibile dietro un'interfaccia astratta, non è così difficile da confondere.
Una struttura dati grafica complessa che rappresenta la rappresentazione logica di base di un'architettura complessa è piuttosto semplice da confondere e in un modo che non viola nemmeno gli invarianti del grafico. Un grafico è molte volte più complesso di una struttura semplice, e quindi diventa ancora più cruciale in tal caso ridurre la complessità percepita della base di codice per ridurre il numero di posti che hanno accesso a tale struttura grafica al minimo assoluto, e dove quel tipo di strategia di "aggiornamento centrale" che si inverte in un paradigma pull per evitare sporadici, spinte dirette alla struttura dei dati del grafico da ogni parte può davvero ripagare.