SOLIDO vs. Evitare l'astrazione prematura


27

Capisco che SOLID dovrebbe realizzare e utilizzarlo regolarmente in situazioni in cui la modularità è importante e i suoi obiettivi sono chiaramente utili. Tuttavia, due cose mi impediscono di applicarlo in modo coerente su tutta la mia base di codice:

  • Voglio evitare l'astrazione prematura. Nella mia esperienza, disegnare linee di astrazione senza casi d'uso concreti (il tipo che esiste ora o in un futuro prevedibile ) porta a disegnarli in posti sbagliati. Quando provo a modificare tale codice, le linee di astrazione si frappongono piuttosto che aiutare. Pertanto, tendo a sbagliare sul lato di non tracciare alcuna linea di astrazione fino a quando non ho una buona idea di dove sarebbero utili.

  • Trovo difficile giustificare la crescente modularità per se stessa se rende il mio codice più dettagliato, più difficile da capire, ecc. E non elimina alcuna duplicazione. Trovo che il codice oggetto procedurale semplice o strettamente accoppiato o Dio sia a volte più facile da capire rispetto al codice ravioli molto ben fatto perché il flusso è semplice e lineare. È anche molto più facile da scrivere.

D'altra parte, questa mentalità porta spesso agli oggetti di Dio. In genere li refactoring in modo conservativo, aggiungendo chiare linee di astrazione solo quando vedo emergere schemi chiari. Cosa c'è di sbagliato negli oggetti di Dio e nel codice strettamente accoppiato se non hai chiaramente bisogno di più modularità, non hai duplicazioni significative e il codice è leggibile?

EDIT: Per quanto riguarda i singoli principi SOLID, intendevo sottolineare che la sostituzione di Liskov è IMHO una formalizzazione del senso comune e dovrebbe essere applicata ovunque, poiché le astrazioni non hanno senso se non lo è. Inoltre, ogni classe dovrebbe avere una singola responsabilità a un certo livello di astrazione, sebbene possa essere un livello molto alto con i dettagli di implementazione stipati tutti in un'enorme classe di 2.000 righe. Fondamentalmente, le tue astrazioni dovrebbero avere senso dove scegli di astrarre. I principi che metto in dubbio nei casi in cui la modularità non è chiaramente utile sono la chiusura aperta, la segregazione delle interfacce e soprattutto l'inversione delle dipendenze, poiché si tratta di modularità, non solo avere astrazioni ha senso.


10
@ S.Lott: la parola chiave è "altro". Questo è esattamente il genere di cose per cui YAGNI è stato inventato. Aumentare la modularità o ridurre l'accoppiamento semplicemente per se stessa è solo una programmazione di culto.
Mason Wheeler,

3
@ S.Lott: dovrebbe essere ovvio dal contesto. Come ho letto, almeno, "più di quanto esiste attualmente nel codice in questione".
Mason Wheeler,

3
@ S.Lott: se insisti a essere pedante, "più" si riferisce a più di quanto esiste attualmente. L'implicazione è che qualcosa, sia esso un codice o un progetto approssimativo, esiste già e stai pensando a come riformattare.
dsimcha,

5
@ back2dos: Giusto, ma sono scettico sull'ingegnerizzare qualcosa per estensibilità senza una chiara idea di come è probabile che venga estesa. Senza queste informazioni, le tue linee di astrazione probabilmente finiranno in tutti i posti sbagliati. Se non sai dove saranno probabilmente utili le linee di astrazione, sto suggerendo che dovresti semplicemente scrivere il codice più conciso che funzioni ed è leggibile, anche se ci sono alcuni oggetti God gettati lì dentro.
dsimcha,

2
@dsimcha: sei fortunato se i requisiti cambiano dopo aver scritto un pezzo di codice, perché di solito cambiano mentre lo fai. Questo è il motivo per cui le implementazioni di snapshot non sono adatte a sopravvivere a un vero ciclo di vita del software. Inoltre, SOLID non richiede di astrarre alla cieca. Il contesto per un'astrazione è definito tramite inversione di dipendenza. Riduci la dipendenza di ogni modulo all'astrazione più semplice e chiara di altri servizi su cui si basa. Non è arbitrario, artificiale o complesso, ma ben definito, plausibile e semplice.
back2dos,

Risposte:


8

I seguenti sono semplici principi che puoi applicare per aiutarti a capire come bilanciare la progettazione del tuo sistema:

  • Principio unico di responsabilità: il SSOLID. Puoi avere un oggetto molto grande in termini di numero di metodi o quantità di dati e mantenere questo principio. Prendi ad esempio l'oggetto String Ruby. Questo oggetto ha più metodi di quanti tu possa agitare, ma ha ancora una sola responsabilità: tenere una stringa di testo per l'applicazione. Non appena il tuo oggetto inizia ad assumere una nuova responsabilità, inizia a pensarci intensamente. Il problema di manutenzione è chiedersi "dove mi aspetterei di trovare questo codice se avessi problemi con esso in seguito?"
  • La semplicità conta: Albert Einstein ha detto "Rendi tutto il più semplice possibile, ma non più semplice". Hai davvero bisogno di quel nuovo metodo? Riesci a realizzare ciò di cui hai bisogno con la funzionalità esistente? Se pensi davvero che ci debba essere un nuovo metodo, puoi cambiare un metodo esistente per soddisfare tutto ciò di cui hai bisogno? In sostanza refactor mentre costruisci nuove cose.

In sostanza, stai cercando di fare il possibile per non spararti ai piedi quando arriva il momento di mantenere il software. Se i tuoi oggetti di grandi dimensioni sono astrazioni ragionevoli, allora ci sono poche ragioni per dividerli solo perché qualcuno ha inventato una metrica che dice che una classe non dovrebbe essere più di X linee / metodi / proprietà / ecc. Semmai, queste sono linee guida e regole non rigide.


3
Buon commento Come discusso in altre risposte, un problema con "responsabilità singola" è che la responsabilità di una classe può essere descritta a vari livelli di astrazione.
dsimcha,

5

Penso che per la maggior parte tu abbia risposto alla tua domanda. SOLID è un insieme di linee guida su come riformattare il codice, quando i concetti devono essere aumentati di livello di astrazioni.

Come tutte le altre discipline creative non ci sono assoluti - solo compromessi. Più lo fai, più "facile" diventa decidere quando è sufficiente per il dominio o i requisiti del tuo problema attuale.

Detto questo, l'astrattismo è il cuore dello sviluppo del software, quindi se non c'è una buona ragione per non farlo, allora la pratica ti renderà migliore e avrai un istinto per i compromessi. Quindi favoriscilo a meno che non diventi dannoso.


5
I want to avoid premature abstraction.In my experience drawing abstraction lines without concrete use cases... 

Questo è parzialmente giusto e parzialmente sbagliato.

Sbagliato
Questo non è un motivo per impedire ad uno di applicare i principi OO / SOLID. Impedisce solo di applicarli troppo presto.

Che è...

Diritto
Non refactificare il codice fino a quando non è refactorable; quando è completo i requisiti. Oppure, quando i "casi d'uso" sono tutti lì come dici tu.

I find simple, tightly coupled procedural or God object code is sometimes easier to understand than very well-factored...

Quando si lavora da soli o in un silo
Il problema di non eseguire OO non è immediatamente evidente. Dopo aver scritto la prima versione di un programma:

Code is fresh in your head
Code is familiar
Code is easy to mentally compartmentalize
You wrote it

3/4 di queste cose buone muoiono rapidamente in breve tempo.

Infine, riconosci di avere oggetti God (Oggetti con molte funzioni), quindi riconosci le singole funzioni. Incapsulare. Mettili in cartelle. Usa le interfacce di tanto in tanto. Fatevi un favore per la manutenzione a lungo termine. Soprattutto perché i metodi non refactored tendono a gonfiarsi all'infinito e diventano metodi di Dio.

In una squadra
I problemi con il non eseguire OO sono immediatamente evidenti. Un buon codice OO è almeno in qualche modo auto-documentante e leggibile. Soprattutto se combinato con un buon codice verde. Inoltre, la mancanza di struttura rende difficile separare compiti e integrazione molto più complessi.


Destra. Riconosco vagamente che i miei oggetti fanno più di una cosa, ma non riconosco più concretamente come separarli utilmente in modo che le linee di astrazione siano nei punti giusti quando è necessario eseguire la manutenzione. Sembra una perdita di tempo per il refactoring se il programmatore della manutenzione dovrà probabilmente fare molto più refactoring per ottenere comunque le linee di astrazione nel posto giusto.
dsimcha,

Incapsulamento e astrazione sono due concetti diversi. L'incapsulamento è un concetto OO di base che sostanzialmente significa che hai delle classi. Le lezioni forniscono modularità e leggibilità in sé e per sé. Questo è utile
P.Brian.Mackey,

L'astrazione può ridurre la manutenzione se applicata correttamente. Se applicato in modo errato può aumentare la manutenzione. Sapere quando e dove tracciare le linee richiede una solida conoscenza del business, del framework, del cliente, oltre agli aspetti tecnici di base di come "applicare un modello di fabbrica" ​​di per sé.
P.Brian.Mackey,

3

Non c'è niente di sbagliato negli oggetti di grandi dimensioni e nel codice strettamente accoppiato quando sono appropriati e quando non è richiesto nulla di meglio progettato . Questa è solo un'altra manifestazione di una regola empirica che si trasforma in dogma.

L'accoppiamento lento e gli oggetti piccoli e semplici tendono a fornire vantaggi specifici in molti casi comuni, quindi è una buona idea usarli, in generale. Il problema sta nelle persone che non comprendono la logica alla base dei principi che cercano ciecamente di applicarli universalmente, anche dove non si applicano.


1
+1. Serve ancora una buona definizione del significato di "appropriato".
giovedì

2
@jprete: Perché? Cercare di applicare definizioni assolute fa parte della causa di problemi concettuali come questo. C'è molta abilità artigianale nella costruzione di un buon software. Ciò che è veramente necessario IMO è esperienza e buon senso.
Mason Wheeler,

4
Bene, sì, ma una definizione ampia di qualche tipo è ancora utile.
giovedì

1
Avere una definizione di appropriato sarebbe utile, ma è piuttosto questo il punto. È difficile stabilire un morso sonoro perché è applicabile solo caso per caso. Se fai la scelta appropriata per ogni caso, dipende in gran parte dall'esperienza. Se non riesci a capire perché sei ad alta coesione, la soluzione monolitica è migliore di una soluzione a bassa coesione e altamente modulare, "probabilmente" sarebbe meglio andare a bassa coesione e modulare. Rifattorizzare è sempre più facile.
Ian,

1
Preferisco qualcuno che scriva ciecamente il codice disaccoppiato invece di scrivere ciecamente il codice degli spaghetti :)
Boris Yankov,

2

Tendo ad avvicinarmi di più dal punto di vista You Ain't Gonna Need It. Questo post si concentrerà su un elemento in particolare: l'eredità.

Nel mio progetto iniziale creo una gerarchia di cose che so che ce ne saranno due o più. È probabile che avranno bisogno dello stesso codice, quindi vale la pena progettarlo dall'inizio. Dopo che il codice iniziale è stato inserito e devo aggiungere altre caratteristiche / funzionalità, guardo quello che ho e penso: "Qualcosa che ho già implementato la stessa funzione o simile?" In tal caso, è molto probabile che venga rilasciata una nuova astrazione.

Un buon esempio di ciò è un framework MVC. A partire da zero, crei una grande pagina con un codice dietro. Ma poi vuoi aggiungere un'altra pagina. Tuttavia, l'unico codice dietro implementa già molta della logica richiesta dalla nuova pagina. Quindi, estrai una Controllerclasse / interfaccia che implementerà la logica specifica per quella pagina, lasciando le cose comuni nel codice "god" originale.


1

Se sai come sviluppatore quando NON applicare i principi a causa del futuro del codice, allora buon per te.

Spero che SOLID resti nella parte posteriore della tua mente per sapere quando le cose devono essere astratte per evitare complessità e migliorare la qualità del codice se sta iniziando a degradare nei valori che hai dichiarato.

Ancora più importante, considera che la maggior parte degli sviluppatori sta facendo il lavoro diurno e non gli importa quanto te. Se inizialmente hai impostato metodi a esecuzione prolungata, cosa penseranno gli altri sviluppatori che entrano nel codice quando vengono per mantenerlo o estenderlo? ... Sì, hai indovinato, funzioni più GRANDI, classi più lunghe e ALTRO dio obietta e non ha in mente i principi per essere in grado di catturare il codice in crescita e incapsularlo correttamente. Il codice "leggibile" ora diventa un casino in decomposizione.

Quindi potresti sostenere che uno sviluppatore cattivo lo farà indipendentemente, ma almeno hai un vantaggio migliore se le cose vengono mantenute semplici e ben organizzate dall'inizio.

Non mi dispiace particolarmente una "Mappa del Registro" globale o un "Oggetto Dio" se sono semplici. Dipende molto dalla progettazione del sistema, a volte puoi cavartela e rimanere semplice, a volte non puoi.

Non dimentichiamo inoltre che le grandi funzioni e gli oggetti God possono rivelarsi estremamente difficili da testare. Se vuoi rimanere agile e sentirti "sicuro" durante il re-factoring e la modifica del codice, hai bisogno di test. I test sono difficili da scrivere quando funzioni o oggetti fanno molte cose.


1
Fondamentalmente una buona risposta. La mia unica lamentela è che, se qualche sviluppatore di manutenzione arriva e fa un pasticcio, è colpa sua per non refactoring, a meno che tali modifiche non sembrino abbastanza probabili da giustificare la pianificazione per loro o il codice è stato così scarsamente documentato / testato / ecc. . quel refactoring sarebbe stato una forma di follia.
dsimcha,

Questo è vero dsimcha. È solo che ho pensato in un sistema in cui uno sviluppatore crea una cartella "RequestHandlers" e ogni classe in quella cartella è un gestore di richieste, quindi il prossimo sviluppatore che arriva e pensa "Devo inserire un nuovo gestore di richieste", l'infrastruttura attuale quasi lo costringe a seguire la convenzione. Penso che sia quello che stavo cercando di dire. Impostare una convenzione ovvia fin dall'inizio, può guidare anche gli sviluppatori più inesperti a continuare il modello.
Martin Blore,

1

Il problema con un oggetto divino è che di solito puoi romperlo proficuamente in pezzi. Per definizione, fa più di una cosa. L'intero scopo di dividerlo in classi più piccole è quello di poter avere una classe che faccia bene una cosa (e non dovresti nemmeno allungare la definizione di "una cosa"). Ciò significa che, per ogni data lezione, una volta che conosci l'unica cosa che dovrebbe fare, dovresti essere in grado di leggerlo abbastanza facilmente e dire se sta facendo quella cosa correttamente.

Penso che ci sia una cosa a come avere troppo modularità, e troppa flessibilità, ma questo è più di un over-design e over-engineering problema, dove si sta contabilità per esigenze che non conoscono nemmeno il cliente vuole. Molte idee di design sono orientate a semplificare il controllo delle modifiche al modo in cui viene eseguito il codice, ma se nessuno si aspetta il cambiamento, è inutile includere la flessibilità.


"Fa più di una cosa" può essere difficile da definire, a seconda del livello di astrazione a cui stai lavorando. Si potrebbe sostenere che qualsiasi metodo con due o più righe di codice sta facendo più di una cosa. All'estremità opposta dello spettro, qualcosa di complesso come un motore di scripting avrà bisogno di un sacco di metodi che fanno tutti "cose" individuali che non sono direttamente correlate tra loro, ma ognuna comprende una parte importante della "meta- cosa "che sta facendo funzionare correttamente i tuoi script, e c'è così tanta suddivisione che può essere fatta senza rompere il motore di script.
Mason Wheeler,

Esiste sicuramente uno spettro di livelli concettuali, ma su questo possiamo scegliere un punto: la dimensione di una "classe che fa una cosa" dovrebbe essere tale da poter comprendere quasi immediatamente l'implementazione. Cioè i dettagli di implementazione si adattano alla tua testa. Se diventa più grande, allora non puoi capire l'intera classe in una volta. Se è più piccolo, stai astrattando troppo lontano. Ma come hai detto in un altro commento, c'è molto giudizio ed esperienza in gioco.
giovedì

IMHO il miglior livello di astrazione a cui pensare è il livello che ti aspetti che i consumatori dell'oggetto utilizzino. Supponi di avere un oggetto chiamato Queryerche ottimizza le query del database, interroga RDBMS e analizza i risultati in oggetti da restituire. Se c'è solo un modo per farlo nella tua app ed Queryerè ermeticamente sigillato e incapsula questo in un modo, allora fa solo una cosa. Se ci sono diversi modi per farlo e qualcuno potrebbe preoccuparsi dei dettagli di una parte di esso, allora fa più cose e potresti voler dividerlo.
dsimcha,

1

Uso una linea guida più semplice: se riesci a scrivere unit test per questo e non hai duplicazioni di codice, è abbastanza astratto. Inoltre, sei in una buona posizione per il refactoring successivo.

Oltre a ciò, dovresti tenere presente SOLID, ma più come linea guida che come regola.


0

Cosa c'è di sbagliato negli oggetti di Dio e nel codice strettamente accoppiato se non hai chiaramente bisogno di più modularità, non hai duplicazioni significative e il codice è leggibile?

Se l'applicazione è abbastanza piccola, tutto è gestibile. In applicazioni più grandi, gli oggetti di Dio diventano rapidamente un problema. Alla fine l'implementazione di una nuova funzionalità richiede la modifica di 17 oggetti God. Le persone non vanno molto bene seguendo le procedure in 17 passaggi. Gli oggetti God vengono costantemente modificati da più sviluppatori e questi cambiamenti devono essere uniti ripetutamente. Non vuoi andarci.


0

Condivido le tue preoccupazioni sull'astrazione eccessiva e inappropriata, ma non sono necessariamente così preoccupato per l'astrazione prematura.

Questo probabilmente suona come una contraddizione, ma è improbabile che l'uso di un'astrazione causi un problema fintanto che non ci si sovraccarica troppo presto, cioè finché si è disposti e in grado di rifattorizzare se necessario.

Ciò implica un certo grado di prototipazione, sperimentazione e backtracking nel codice.

Detto questo, non esiste una semplice regola deterministica da seguire che risolva tutti i problemi. L'esperienza conta molto, ma devi guadagnarla facendo errori. E ci sono sempre più errori da fare che gli errori precedenti non ti hanno preparato.

Tuttavia, considera i principi del libro di testo come un punto di partenza, quindi impara a programmare molto e a vedere come funzionano questi principi. Se fosse stato possibile fornire linee guida migliori, più precise e affidabili rispetto a cose come SOLID, qualcuno lo avrebbe probabilmente fatto ora - e anche se lo fossero, quei principi migliorati sarebbero comunque imperfetti e la gente si chiederebbe quali fossero i limiti .

Ottimo lavoro - se qualcuno potesse fornire un algoritmo chiaro e deterministico per la progettazione e la codifica dei programmi, ci sarebbe solo un ultimo programma da scrivere per gli umani - il programma che scriverebbe automaticamente tutti i programmi futuri senza intervento umano.


0

Tracciare le linee di astrazione appropriate è qualcosa che si trae dall'esperienza. In questo modo commetterai degli errori, questo è innegabile.

SOLID è una forma concentrata di quell'esperienza che ti viene consegnata da persone che hanno acquisito questa esperienza nel modo più duro. L'hai praticamente inchiodato quando dici che ti asterrai dal creare l'astrazione prima di vederne la necessità. Bene, l'esperienza fornita da SOLID ti aiuterà a risolvere problemi che non hai mai visto prima . Ma fidati di me ... ci sono molto reali.

SOLID riguarda la modularità, la modularità riguarda la manutenibilità e la manutenibilità consiste nel permetterti di mantenere un programma di lavoro umano una volta che il software ha raggiunto la produzione e il cliente inizia a notare bug che non hai.

Il più grande vantaggio della modularità è la testabilità. Più modulare è il sistema, più facile sarà testarlo e più velocemente sarà possibile costruire solide basi per affrontare gli aspetti più difficili del problema. Quindi il tuo problema potrebbe non richiedere questa modularità, ma solo il fatto che ti consentirà di creare codice di prova migliore più velocemente non è un gioco da ragazzi cercare la modularità.

Essere agili significa tentare di trovare il delicato equilibrio tra la spedizione veloce e la fornitura di buona qualità. Agile non significa che dovremmo tagliare l'angolo, infatti, i progetti agili di maggior successo con cui sono stato coinvolto sono quelli che hanno prestato molta attenzione a tali linee guida.


0

Un punto importante che sembra essere stato trascurato è che il SOLID viene praticato insieme al TDD. TDD tende a evidenziare ciò che è "appropriato" nel tuo particolare contesto e aiuta ad alleviare gran parte dell'equivoco che sembra essere nel consiglio.


1
Ti preghiamo di considerare di espandere la tua risposta. Allo stato attuale, stai riunendo due concetti ortogonali e non è chiaro in che modo il risultato risolva i problemi del PO.
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.