I sei comandamenti del buon codice: scrivi un codice che resista alla prova del tempo
Pubblicato: 2022-03-11Gli esseri umani sono alle prese con l'arte e la scienza della programmazione informatica solo da circa mezzo secolo. Rispetto alla maggior parte delle arti e delle scienze, l'informatica è per molti versi ancora solo un bambino, che cammina contro i muri, inciampa sui propri piedi e occasionalmente getta il cibo sul tavolo.
Come conseguenza della sua relativa giovinezza, non credo che abbiamo ancora un consenso su quale sia una definizione corretta di "buon codice", poiché tale definizione continua ad evolversi. Alcuni diranno che "buon codice" è un codice con una copertura del test del 100%. Altri diranno che è super veloce e ha prestazioni killer e funzionerà in modo accettabile su hardware vecchio di 10 anni.
Sebbene questi siano tutti obiettivi lodevoli per gli sviluppatori di software, tuttavia mi azzardo a inserire un altro obiettivo nel mix: la manutenibilità. In particolare, il "buon codice" è un codice che è facilmente e prontamente gestibile da un'organizzazione (non solo dal suo autore!) e vivrà più a lungo del semplice sprint in cui è stato scritto. Di seguito sono riportate alcune cose che ho scoperto nel mio carriera come ingegnere presso grandi e piccole aziende, negli USA e all'estero, che sembrano correlare con software manutenibili e “buoni”.
Comandamento n. 1: tratta il tuo codice nel modo in cui desideri che il codice degli altri ti tratti
Sono tutt'altro che la prima persona a scrivere che il pubblico principale del tuo codice non è il compilatore/computer, ma chiunque debba leggere, comprendere, mantenere e migliorare il codice (che non sarai necessariamente tu tra sei mesi ). Qualsiasi ingegnere che valga la sua paga può produrre codice che "funziona"; ciò che distingue un ingegnere eccezionale è che può scrivere codice manutenibile in modo efficiente che supporta un business a lungo termine e ha la capacità di risolvere i problemi in modo semplice, in modo chiaro e manutenibile.
In qualsiasi linguaggio di programmazione è possibile scrivere codice buono o pessimo. Supponendo di giudicare un linguaggio di programmazione da quanto bene facilita la scrittura di un buon codice (dovrebbe essere almeno uno dei criteri principali, comunque), qualsiasi linguaggio di programmazione può essere "buono" o "cattivo" a seconda di come viene utilizzato (o abusato ).
Un esempio di linguaggio che da molti è considerato “pulito” e leggibile è Python. Il linguaggio stesso impone un certo livello di disciplina degli spazi bianchi e le API integrate sono abbondanti e abbastanza coerenti. Detto questo, è possibile creare mostri indicibili. Ad esempio, è possibile definire una classe e definire/ridefinire/annullare qualsiasi metodo su quella classe durante il runtime (spesso indicato come patch di scimmia). Questa tecnica porta naturalmente nel migliore dei casi a un'API incoerente e nel peggiore a un mostro impossibile da eseguire il debug. Si potrebbe ingenuamente pensare "sicuro, ma nessuno lo fa!" Sfortunatamente lo fanno, e non ci vuole molto a sfogliare pypi prima di imbattersi in librerie sostanziali (e popolari!) che (ab) utilizzano ampiamente le patch delle scimmie come nucleo delle loro API. Di recente ho utilizzato una libreria di rete la cui intera API cambia a seconda dello stato di rete di un oggetto. Immagina, ad esempio, di chiamare client.connect()
e talvolta di ricevere un errore MethodDoesNotExist
invece di HostNotFound
o NetworkUnavailable
.
Comandamento n. 2: un buon codice si legge e si comprende facilmente, in parte e per intero
Un buon codice è facilmente leggibile e compreso, in parte e per intero, da altri (così come dall'autore in futuro, cercando di evitare la sindrome "L'ho davvero scritto?" ).
Con "in parte" intendo che, se apro qualche modulo o funzione nel codice, dovrei essere in grado di capire cosa fa senza dover leggere anche l'intero resto della codebase. Dovrebbe essere il più intuitivo e auto-documentante possibile.
Il codice che fa costantemente riferimento a dettagli minuti che influiscono sul comportamento di altre parti (apparentemente irrilevanti) della base di codice è come leggere un libro in cui devi fare riferimento alle note a piè di pagina o a un'appendice alla fine di ogni frase. Non riusciresti mai a superare la prima pagina!
Alcuni altri pensieri sulla leggibilità "locale":
Il codice ben incapsulato tende ad essere più leggibile, separando le preoccupazioni a ogni livello.
I nomi contano. Attiva il sistema 2 di Thinking Fast and Slow in cui il cervello forma i pensieri e metti alcuni pensieri reali e attenti nei nomi di variabili e metodi. I pochi secondi in più possono pagare dividendi significativi. Una variabile con un nome corretto può rendere il codice molto più intuitivo, mentre una variabile con un nome errato può portare a falsi e confusione.
L'astuzia è il nemico. Quando usi tecniche, paradigmi o operazioni fantasiose (come la comprensione di elenchi o gli operatori ternari), fai attenzione a usarli in un modo che renda il tuo codice più leggibile, non solo più breve.
La coerenza è una buona cosa. La coerenza nello stile, sia in termini di come si posizionano le parentesi graffe, sia in termini di operazioni, migliora notevolmente la leggibilità.
Separazione degli interessi. Un determinato progetto gestisce un numero innumerevole di presupposti importanti a livello locale in vari punti della base di codice. Esponi ogni parte della base di codice al minor numero possibile di queste preoccupazioni. Supponiamo che tu abbia un sistema di gestione delle persone in cui un oggetto persona a volte può avere un cognome nullo. Per qualcuno che scrive codice in una pagina che mostra oggetti persona, potrebbe essere davvero imbarazzante! E a meno che tu non mantenga un manuale di "ipotesi imbarazzanti e non ovvie che la nostra base di codice ha" (so di no) il tuo programmatore della pagina di visualizzazione non saprà che i cognomi possono essere nulli e probabilmente scriverà il codice con un puntatore nullo eccezione al suo interno quando viene visualizzato il caso nullo del cognome. Gestisci invece questi casi con API e contratti ben congegnati che diversi pezzi della tua base di codice usano per interagire tra loro.
Comandamento n. 3: un buon codice ha un layout e un'architettura ben congegnati per rendere ovvio lo stato di gestione
Lo Stato è il nemico. Come mai? Perché è la parte più complessa di qualsiasi applicazione e deve essere gestita in modo molto deliberato e ponderato. I problemi comuni includono incoerenze del database, aggiornamenti parziali dell'interfaccia utente in cui i nuovi dati non si riflettono ovunque, operazioni fuori servizio o semplicemente un codice incredibilmente complesso con istruzioni if e rami ovunque che portano a codice difficile da leggere e ancora più difficile da mantenere. Mettere lo stato su un piedistallo da trattare con grande cura ed essere estremamente coerenti e deliberati riguardo al modo in cui si accede e si modifica lo stato, semplifica notevolmente la base di codice. Alcuni linguaggi (ad esempio Haskell) lo applicano a livello programmatico e sintattico. Saresti sorpreso di quanto la chiarezza della tua base di codice possa migliorare se hai librerie di funzioni pure che non accedono a nessuno stato esterno e quindi una piccola superficie di codice con stato che fa riferimento alla pura funzionalità esterna.

Comandamento n. 4: il buon codice non reinventa la ruota, sta sulle spalle dei giganti
Prima di reinventare potenzialmente una ruota, pensa a quanto è comune il problema che stai cercando di risolvere o la funzione che stai cercando di svolgere. Qualcuno potrebbe aver già implementato una soluzione che puoi sfruttare. Prenditi il tempo per pensare e ricercare tali opzioni, se appropriate e disponibili.
Detto questo, una controargomentazione del tutto ragionevole è che le dipendenze non vengono "gratuite" senza alcun aspetto negativo. Utilizzando una libreria di terze parti o open source che aggiunge alcune funzionalità interessanti, ti impegni e diventi dipendente da quella libreria. Questo è un grande impegno; se è una libreria gigante e hai solo bisogno di un po 'di funzionalità, vuoi davvero l'onere di aggiornare l'intera libreria se aggiorni, ad esempio, a Python 3.x? Inoltre, se riscontri un bug o desideri migliorare la funzionalità, dipendi dall'autore (o dal fornitore) per fornire la correzione o il miglioramento oppure, se è open source, ti trovi nella posizione di esplorare un ( potenzialmente sostanziale) base di codice con cui non hai familiarità con il tentativo di correggere o modificare un oscuro bit di funzionalità.
Sicuramente più il codice da cui dipendi è ben utilizzato, meno è probabile che dovrai investire tempo nella manutenzione. La linea di fondo è che vale la pena per te fare le tue ricerche e fare la tua valutazione se includere o meno la tecnologia esterna e quanta manutenzione quella particolare tecnologia aggiungerà al tuo stack.
Di seguito sono riportati alcuni degli esempi più comuni di cose che probabilmente non dovresti reinventare nell'era moderna nel tuo progetto (a meno che questi NON SIANO i tuoi progetti).
Banche dati
Scopri quale CAP ti serve per il tuo progetto, quindi scegli il database con le giuste proprietà. Database non significa più solo MySQL, puoi scegliere tra:
- SQL "tradizionale" con schema: Postgres / MySQL / MariaDB / MemSQL / Amazon RDS, ecc.
- Punti vendita chiave: Redis / Memcache / Riak
- NoSQL: MongoDB/Cassandra
- DB ospitati: AWS RDS / DynamoDB / AppEngine Datastore
- Sollevamento pesante: Amazon MR / Hadoop (Hive/Pig) / Cloudera / Google Big Query
- Roba pazzesca: Mnesia di Erlang, Core Data di iOS
Strati di astrazione dei dati
Nella maggior parte dei casi, non dovresti scrivere query non elaborate su qualsiasi database tu abbia scelto di utilizzare. Molto probabilmente, esiste una libreria da inserire tra il DB e il codice dell'applicazione, separando le preoccupazioni relative alla gestione di sessioni di database simultanee e i dettagli dello schema dal codice principale. Per lo meno, non dovresti mai avere query non elaborate o SQL inline nel mezzo del codice dell'applicazione. Piuttosto, avvolgilo in una funzione e centralizza tutte le funzioni in un file chiamato qualcosa di veramente ovvio (ad esempio, "queries.py"). Una riga come users = load_users()
, ad esempio, è infinitamente più facile da leggere rispetto a users = db.query(“SELECT username, foo, bar from users LIMIT 10 ORDER BY ID”)
. Questo tipo di centralizzazione rende inoltre molto più semplice avere uno stile coerente nelle query e limita il numero di posizioni in cui andare per modificare le query in caso di modifica dello schema.
Altre librerie e strumenti comuni da considerare come sfruttare
- Accodamento o servizi Pub/Sub. Scegli tra fornitori AMQP, ZeroMQ, RabbitMQ, Amazon SQS
- Conservazione. Amazon S3, Google Cloud Storage
- Monitoraggio: Grafite/Grafite ospitata, AWS Cloud Watch, New Relic
- Raccolta/aggregazione log. Loggly, Splunk
Ridimensionamento automatico
- Ridimensionamento automatico. Heroku, AWS Beanstalk, AppEngine, AWS Opsworks, Digital Ocean
Comandamento #5: Non oltrepassare i corsi d'acqua!
Ci sono molti buoni modelli per la progettazione della programmazione, pub/sub, attori, MVC ecc. Scegli quello che ti piace di più e attieniti ad esso. Diversi tipi di logica che si occupano di diversi tipi di dati dovrebbero essere fisicamente isolati nella base di codice (di nuovo, questo concetto di separazione delle preoccupazioni e la riduzione del carico cognitivo sul futuro lettore). Il codice che aggiorna l'interfaccia utente dovrebbe essere fisicamente distinto dal codice che calcola ciò che va nell'interfaccia utente, ad esempio.
Comandamento n. 6: Quando possibile, lascia che il computer faccia il lavoro
Se il compilatore è in grado di rilevare errori logici nel codice e prevenire comportamenti scorretti, bug o arresti anomali, dovremmo assolutamente trarne vantaggio. Naturalmente, alcuni linguaggi hanno compilatori che lo rendono più semplice di altri. Haskell, ad esempio, ha un famoso compilatore rigoroso che fa sì che i programmatori spendano la maggior parte dei loro sforzi solo per compilare il codice. Una volta compilato, però, "funziona e basta". Per quelli di voi che non hanno mai scritto in un linguaggio funzionale fortemente tipizzato, questo può sembrare ridicolo o impossibile, ma non credetemi sulla parola. Seriamente, fai clic su alcuni di questi collegamenti, è assolutamente possibile vivere in un mondo senza errori di runtime. Ed è davvero così magico.
Certo, non tutti i linguaggi hanno un compilatore o una sintassi che si presta a molti (o in alcuni casi a nessuno!) controlli in fase di compilazione. Per coloro che non lo fanno, dedica qualche minuto alla ricerca di quali controlli di rigore facoltativi puoi abilitare nel tuo progetto e valuta se hanno senso per te. Un breve elenco non completo di alcuni comuni che ho usato ultimamente per le lingue con tempi di esecuzione indulgenti include:
- Python: pylint, pyflakes, warnings, warning in emacs
- Rubino: avvertenze
- JavaScript: jslint
Conclusione
Questo non è affatto un elenco esaustivo o perfetto di comandamenti per produrre codice “buono” (cioè facilmente manutenibile). Detto questo, se ogni codebase che ho dovuto raccogliere in futuro seguisse anche solo la metà dei concetti in questo elenco, avrò molti meno capelli grigi e potrei anche essere in grado di aggiungere altri cinque anni alla fine della mia vita. E sicuramente troverò il lavoro più piacevole e meno stressante.