Le astrazioni devono ridurre la leggibilità del codice?


19

Un buon sviluppatore con cui lavoro di recente mi ha parlato di alcune difficoltà che aveva nell'implementazione di una funzione in un codice che avevamo ereditato; ha detto che il problema era che il codice era difficile da seguire. Da quello, ho approfondito il prodotto e ho capito quanto fosse difficile vedere il percorso del codice.

Ha usato così tante interfacce e strati astratti, che cercare di capire dove sono iniziate e finite le cose è stato piuttosto difficile. Mi ha fatto pensare alle volte in cui avevo guardato i progetti passati (prima ero così consapevole dei principi del codice pulito) e trovavo estremamente difficile aggirare il progetto, principalmente perché i miei strumenti di navigazione del codice mi avrebbero sempre indirizzato su un'interfaccia. Ci vorrebbe uno sforzo extra per trovare l'implementazione concreta o dove qualcosa è stato cablato in un'architettura di tipo plug-in.

So che alcuni sviluppatori rifiutano rigorosamente i contenitori di iniezione di dipendenza proprio per questo motivo. Confonde il percorso del software a tal punto che la difficoltà della navigazione del codice aumenta in modo esponenziale.

La mia domanda è: quando un framework o pattern introduce così tanto overhead come questo, ne vale la pena? È un sintomo di un modello mal implementato?

Immagino che uno sviluppatore dovrebbe guardare al quadro più ampio di ciò che queste astrazioni portano al progetto per aiutarli a superare la frustrazione. Di solito, però, è difficile far loro vedere quel quadro generale. So di non essere riuscito a vendere le esigenze di IOC e DI con TDD. Per quegli sviluppatori, l'uso di questi strumenti limita troppo la leggibilità del codice.

Risposte:


17

Questo è davvero più di un lungo commento sulla risposta di @kevin cline.

Anche se le lingue stesse non causano o impediscono necessariamente questo, penso che ci sia qualcosa nella sua idea che sia in qualche modo correlato alle lingue (o almeno alle comunità linguistiche). In particolare, anche se puoi incontrare lo stesso problema in diverse lingue, spesso assumerà forme piuttosto diverse in diverse lingue.

Solo per esempio, quando ti imbatti in questo in C ++, è probabile che sia meno un risultato di troppa astrazione e più un risultato di troppa intelligenza. Solo per esempio, il programmatore ha nascosto la trasformazione cruciale che sta avvenendo (che non riesci a trovare) in un iteratore speciale, quindi quello che sembra solo copiare i dati da un posto a un altro ha davvero un numero di effetti collaterali che non hanno nulla da fare con quella copia dei dati. Solo per mantenere le cose interessanti, questo è intercalato con l'output creato come effetto collaterale della creazione di un oggetto temporaneo nel corso del cast di un tipo di oggetto a un altro.

Al contrario, quando ci si imbatte in Java, è molto più probabile che si veda una variante del noto "mondo dell'impresa", dove invece di una singola banale classe che fa qualcosa di semplice, si ottiene una classe base astratta e una classe di derivazione concreta che implementa l'interfaccia X, creata da una classe di fabbrica in un framework DI, ecc. Le 10 righe di codice che svolgono il vero lavoro sono sepolte sotto 5000 linee di infrastruttura.

In parte dipende dall'ambiente almeno quanto la lingua: lavorare direttamente con ambienti con finestre come X11 e MS Windows è noto per trasformare un banale programma "ciao mondo" in oltre 300 righe di immondizia quasi indecifrabile. Nel tempo, abbiamo sviluppato vari toolkit per isolarci anche da quello - ma 1) quei toolkit sono abbastanza non banali e 2) il risultato finale è ancora non solo più grande e più complesso, ma di solito meno flessibile rispetto a un equivalente in modalità testo (ad esempio, anche se sta solo stampando del testo, raramente è possibile / supportato reindirizzarlo su un file).

Per rispondere (almeno in parte) alla domanda originale: almeno quando l'ho vista, era meno una questione di una cattiva implementazione di un modello piuttosto che semplicemente applicare un modello che era inappropriato per il compito da svolgere - la maggior parte spesso di tentare di applicare un modello che potrebbe essere utile in un programma che è inevitabilmente enorme e complesso, ma quando applicato a un problema più piccolo finisce per renderlo anche enorme e complesso, anche se in questo caso le dimensioni e la complessità erano davvero evitabili .


7

Trovo che ciò sia spesso causato dal fatto di non adottare un approccio YAGNI . Tutto ciò che passa attraverso le interfacce, anche se esiste solo un'implementazione concreta e nessun piano attuale per introdurne altri, è un ottimo esempio di aggiunta di complessità di cui non avrete bisogno. Probabilmente è un'eresia, ma mi sento allo stesso modo riguardo al molto uso dell'iniezione di dipendenza.


+1 per menzionare YAGNI e astrazioni con punti di riferimento singoli. Il ruolo principale di fare un'astrazione è prendere in considerazione il punto comune di più cose. Se si fa riferimento a un'astrazione solo da un punto, non si può parlare di fattorizzare cose comuni, un'astrazione come questa contribuisce solo al problema yoyo. Vorrei estenderlo perché questo è vero per tutti i tipi di astrazioni: funzioni, generici, macro, qualunque cosa ...
Calmarius

3

Bene, non abbastanza astrazione e il tuo codice è difficile da capire perché non puoi isolare quali parti fanno cosa.

Troppa astrazione e vedi l'astrazione ma non il codice stesso, e quindi diventa difficile seguire il thread di esecuzione reale.

Per ottenere una buona astrazione, si dovrebbe BACI: vedere la mia risposta a queste domande per sapere cosa seguire per evitare questo tipo di problemi .

Penso che evitare la gerarchia e la denominazione profonde sia il punto più importante da considerare per il caso che descrivi. Se le astrazioni fossero ben definite, non dovresti andare troppo in profondità, solo al livello di astrazione in cui devi capire cosa succede. La denominazione consente di identificare dove si trova questo livello di astrazione.

Il problema sorge nel codice di basso livello, quando è davvero necessario comprendere tutto il processo. Quindi, l'incapsulamento tramite moduli chiaramente isolati è l'unico aiuto.


3
Bene, non abbastanza astrazione e il tuo codice è difficile da capire perché non puoi isolare quali parti fanno cosa. Questa è incapsulamento, non astrazione. È possibile isolare le parti in classi concrete senza molta astrazione.
Dichiarazione del

Le classi non sono le uniche astrazioni che stiamo usando: funzioni, moduli / librerie, servizi, ecc. Nelle tue classi di solito astraggi ciascuna funzionalità dietro una funzione / metodo, che può chiamare altri metodi che si astraggono l'un l'altro funzionalità.
Klaim,

1
@Statement: incapsulare i dati è ovviamente un'astrazione.
Ed S.

Le gerarchie degli spazi dei nomi sono davvero belle, però.
JAB

2

Per me è un problema di accoppiamento e legato alla granularità del design. Perfino la forma più libera di accoppiamento introduce dipendenze da una cosa all'altra. Se ciò viene fatto per centinaia o migliaia di oggetti, anche se sono tutti relativamente semplici, aderiscono a SRP e anche se tutte le dipendenze scorrono verso astrazioni stabili, ciò produce una base di codice che è molto difficile ragionare su come un tutto interrelato.

Ci sono cose pratiche che ti aiutano a valutare la complessità di una base di codice, non frequentemente discussa nella SE teorica, come solo quanto in profondità nello stack di chiamate puoi ottenere prima di raggiungere la fine e quanto in profondità devi andare prima che puoi, con molta fiducia, comprendere tutti i possibili effetti collaterali che potrebbero verificarsi a quel livello dello stack di chiamate, anche in caso di eccezione.

E ho scoperto, proprio nella mia esperienza, che i sistemi più piatti con stack di chiamate più superficiali tendono ad essere molto più facili da ragionare. Un esempio estremo sarebbe un sistema a componenti di entità in cui i componenti sono solo dati non elaborati. Solo i sistemi hanno funzionalità e, nel processo di implementazione e utilizzo di un ECS, l'ho trovato di gran lunga il sistema più semplice di sempre a ragionare su quando complesse basi di codice che si estendono su centinaia di migliaia di righe di codice praticamente si riducono a poche decine di sistemi che contiene tutte le funzionalità.

Troppe cose forniscono funzionalità

L'alternativa prima di quando lavoravo in codebase precedenti era un sistema con centinaia o migliaia di oggetti per lo più piccoli rispetto a qualche dozzina di sistemi ingombranti con alcuni oggetti usati solo per passare messaggi da un oggetto a un altro ( Messageoggetto, ad esempio, che aveva il suo propria interfaccia pubblica). Questo è fondamentalmente ciò che ottieni analogicamente quando ripristini l'ECS a un punto in cui i componenti hanno funzionalità e ogni combinazione unica di componenti in un'entità produce il proprio tipo di oggetto. E ciò tenderà a produrre funzioni più piccole e più semplici ereditate e fornite da infinite combinazioni di oggetti che modellano idee adolescenti ( Particleoggetto vs.Physics System, per esempio). Tuttavia, tende anche a produrre un grafico complesso di interdipendenze che rende difficile ragionare su ciò che accade a livello generale, semplicemente perché ci sono così tante cose nella base di codice che possono effettivamente fare qualcosa e quindi possono fare qualcosa di sbagliato - - tipi che non sono tipi di "dati", ma tipi di "oggetti" con funzionalità associate. I tipi che fungono da dati puri senza funzionalità associata non possono andare storti poiché non possono fare nulla da soli.

Le interfacce pure non aiutano molto questo problema di comprensibilità perché anche se ciò rende le "dipendenze in fase di compilazione" meno complicate e offre più spazio per il cambiamento e l'espansione, non rende le "dipendenze di runtime" e le interazioni meno complicate. L'oggetto client finisce comunque per invocare funzioni su un oggetto account concreto anche se vengono richiamati IAccount. Il polimorfismo e le interfacce astratte hanno i loro usi ma non disaccoppiano le cose nel modo che ti aiuta davvero a ragionare su tutti gli effetti collaterali che potrebbero verificarsi in un dato punto. Per ottenere questo tipo di disaccoppiamento efficace, è necessario un codebase che abbia molte meno cose che contengono funzionalità.

Più dati, meno funzionalità

Quindi ho trovato l'approccio ECS, anche se non lo si applica completamente, per essere estremamente utile, poiché trasforma quelli che sarebbero stati centinaia di oggetti in soli dati grezzi con sistemi ingombranti, progettati in modo più grossolano, che forniscono tutti i funzionalità. Massimizza il numero di tipi di "dati" e minimizza il numero di tipi di "oggetti", quindi minimizza assolutamente il numero di posizioni nel sistema che possono effettivamente andare storte. Il risultato finale è un sistema molto "piatto" senza un grafico complesso di dipendenze, solo sistemi per componenti, mai viceversa e mai componenti per altri componenti. Sono sostanzialmente molti più dati grezzi e molte meno astrazioni che hanno l'effetto di centralizzare e appiattire la funzionalità della base di codice in aree chiave, astrazioni chiave.

30 cose più semplici non sono necessariamente più semplici da ragionare su 1 cosa più complessa, se quelle 30 cose più semplici sono correlate mentre la cosa complessa si trova da sola. Quindi il mio suggerimento è in realtà di trasferire la complessità lontano dalle interazioni tra oggetti e altro verso oggetti più voluminosi che non devono interagire con nient'altro per ottenere il disaccoppiamento di massa, verso interi "sistemi" (non monoliti e oggetti divini, intendiamoci, e non classi con 200 metodi, ma qualcosa di molto più alto livello di a Messageo Particlea nonostante abbia un'interfaccia minimalista). E preferisci tipi di dati vecchi più semplici. Più dipendi da quelli, meno accoppiamento otterrai. Anche se questo contraddice alcune idee SE, ho scoperto che aiuta davvero molto.


0

La mia domanda è: quando un framework o pattern introduce così tanto overhead come questo, ne vale la pena? È un sintomo di un modello mal implementato?

Forse è un sintomo di scegliere il linguaggio di programmazione sbagliato.


1
Non vedo come questo abbia qualcosa a che fare con la lingua scelta. Le astrazioni sono un concetto indipendente di lingua di alto livello.
Ed S.

@Ed: alcune astrazioni sono più semplicemente realizzabili in alcune lingue rispetto ad altre.
Kevin Cline,

Sì, ma ciò non significa che non puoi scrivere un'astrazione perfettamente gestibile e facilmente comprensibile in quelle lingue. Il mio punto era che la tua risposta non risponde alla domanda né aiuta l'OP in alcun modo.
Ed S.

0

La scarsa comprensione dei modelli di progettazione tende ad essere una delle principali cause di questo problema. Uno dei peggiori che abbia mai visto per questo yo-yo e rimbalzare da un'interfaccia all'altra senza dati molto concreti nel mezzo è stata un'estensione per Oracle Grid Control.
Onestamente sembrava che qualcuno avesse avuto un metodo di fabbrica astratto e un orgasmo con motivi decorativi in ​​tutto il mio codice Java. E mi ha fatto sentire solo vuoto e solo.


-1

Vorrei anche mettere in guardia dall'utilizzare le funzionalità IDE che rendono facile astrarre cose.

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.