Seguire SOLID porta a scrivere un framework in cima allo stack tecnologico?


70

Mi piace SOLID e faccio del mio meglio per usarlo e applicarlo durante lo sviluppo. Ma non posso fare a meno di pensare che l'approccio SOLID trasformi il tuo codice in codice "framework", ovvero il codice che progetteresti se stessi creando un framework o una libreria che altri sviluppatori possano usare.

In genere ho praticato 2 modalità di programmazione: creare più o meno esattamente ciò che viene richiesto tramite requisiti e KISS (programmazione tipica), oppure creare logiche, servizi, ecc. Molto generici e riutilizzabili che forniscano la flessibilità di cui altri sviluppatori potrebbero aver bisogno (programmazione quadro) .

Se l'utente vuole davvero che un'applicazione faccia cose xey, ha senso seguire SOLID e aggiungere un sacco di punti di ingresso di astrazione, quando non sai nemmeno se questo è anche un problema valido per iniziare con? Se aggiungi questi punti di ingresso di astrazione, stai davvero soddisfacendo i requisiti degli utenti o stai creando un framework in aggiunta al tuo framework esistente e stack tecnologico per rendere più facili le aggiunte future? In quale caso stai servendo gli interessi del cliente o dello sviluppatore?

Questo è qualcosa che sembra comune nel mondo Java Enterprise, in cui sembra che tu stia progettando il tuo framework sopra J2EE o Spring in modo che sia una UX migliore per lo sviluppatore, invece di concentrarsi su UX per l'utente?


12
Il problema con la maggior parte delle brevi regole di programmazione è che sono soggetti a interpretazione, casi limite e talvolta le definizioni di parole in tali regole non sono chiare a un esame più attento. Possono essenzialmente significare una grande varietà di cose per persone diverse. Avere un certo pragmatismo non ideologico di solito consente di prendere decisioni più sagge.
Mark Rogers,

1
Fai sembrare che seguire i principi SOLID implichi in qualche modo un grande investimento, un sacco di lavoro extra. Non lo è, è praticamente gratuito. E probabilmente risparmierà a te o a qualcun altro un grande investimento in futuro perché semplifica la manutenzione e l'estensione del codice. Continui a porre domande come "dovremmo fare i compiti o rendere felice il cliente?" Questi non sono compromessi.
Martin Maat,

1
@MartinMaat Penso che le forme più estreme di SOLID implichino grandi investimenti. Vale a dire. Software aziendale. Al di fuori del software aziendale, avresti pochissime ragioni per sottrarre il tuo ORM, stack tecnologico o database perché ci sono ottime possibilità che rimarrai con lo stack scelto. Allo stesso modo, legandoti a un particolare framework, database o ORM, stai violando i principi SOLID perché sei accoppiato al tuo stack. Quel livello di flessibilità di SOLID non è richiesto nella maggior parte dei lavori.
Igneous01,


1
Trasformare la maggior parte del codice in qualcosa come un framework non sembra affatto terribile. Diventa terribile solo se diventa troppo ingegnerizzato. Ma i quadri possono essere minimi e supponenti. Non sono sicuro che questa sarebbe una conseguenza inevitabile del seguire SOLID, ma è sicuramente una possibile conseguenza e una, penso, che dovresti abbracciare.
Konrad Rudolph,

Risposte:


84

La tua osservazione è corretta, i principi SOLID sono IMHO realizzati tenendo in considerazione librerie riutilizzabili o codice framework. Quando li segui tutti alla cieca, senza chiedere se ha senso o no, stai rischiando di sovra-generalizzare e investire molto più sforzo nel tuo sistema di quanto probabilmente necessario.

Questo è un compromesso e ha bisogno di esperienza per prendere le giuste decisioni su quando generalizzare e quando no. Un possibile approccio a questo è attenersi al principio YAGNI - non rendere il codice SOLIDO "per ogni evenienza" - o, per usare le tue parole: non

fornire la flessibilità di cui altri sviluppatori potrebbero aver bisogno

invece, fornisce la flessibilità di cui gli altri sviluppatori hanno effettivamente bisogno non appena ne hanno bisogno , ma non prima.

Quindi ogni volta che hai una funzione o una classe nel tuo codice non sei sicuro che possa essere riutilizzato, non metterlo nel tuo framework in questo momento. Attendi fino a quando non avrai un caso reale di riutilizzo e refactoring su "SOLIDI abbastanza per quel caso". Non implementare più configurabilità (seguendo l'OCP) o punti di ingresso dell'astrazione (usando il DIP) in una classe di cui hai veramente bisogno per il caso reale di riutilizzo. Aggiungi la flessibilità successiva quando il prossimo requisito per il riutilizzo è effettivamente presente.

Naturalmente, questo modo di lavorare richiederà sempre una certa quantità di refactoring nella base di codice esistente e funzionante. Ecco perché i test automatici sono importanti qui. Quindi rendere il tuo codice SOLID abbastanza dall'inizio sin dall'inizio per renderlo unitamente testabile non è una perdita di tempo, e farlo non contraddice YAGNI. I test automatici sono un caso valido per il "riutilizzo del codice", poiché il codice in questione viene utilizzato sia dal codice di produzione che dai test. Ma tieni a mente, aggiungi solo la flessibilità di cui hai effettivamente bisogno per far funzionare i test, niente di meno, niente di più.

Questa è in realtà una vecchia saggezza. Molto tempo fa prima che il termine SOLID ottenuto popolare, qualcuno mi ha detto, prima di provare a scrivere nuovamente il codice utilizzabile, dovremmo scrivere utilizzabile codice. E penso ancora che questa sia una buona raccomandazione.


23
Ulteriori punti controversi: attendi fino a quando non hai 3 casi d'uso in cui vedi la stessa logica prima di refactoring del codice per il riutilizzo. Se inizi a refactoring con 2 pezzi, è facile finire in una situazione in cui cambiando i requisiti o un nuovo caso d'uso finisce per rompere l'astrazione che hai fatto. Inoltre, i refactor si limitano alle cose con lo stesso caso d'uso: 2 componenti potrebbero avere lo stesso codice ma fare cose completamente diverse e, se si uniscono quei componenti, si finisce per collegare quella logica, che può causare problemi in seguito.
Nzall,

8
In genere sono d'accordo con questo, ma mi sembra che sia troppo focalizzato su app "una tantum": scrivi il codice, funziona, va bene. Tuttavia, ci sono molte app con "supporto a lungo termine". Potresti scrivere del codice e 2 anni dopo, i requisiti aziendali cambiano, quindi devi modificare il codice. A quel punto, molto altro codice potrebbe dipendere da esso - in tal caso, i principi SOLID renderanno più semplice la modifica.
R. Schmitz,

3
"Prima di provare a scrivere codice riutilizzabile, dovremmo scrivere codice utilizzabile" - Molto saggio!
Graham,

10
Probabilmente vale la pena notare che aspettare fino a quando non si avrà un caso d'uso reale migliorerà il codice SOLID , perché lavorare in ipotesi è molto difficile e si rischia di indovinare quali saranno le esigenze future. Il nostro progetto ha una serie di casi in cui le cose sono state progettate per essere SOLID e flessibili per le esigenze future ... eccetto che le esigenze future si sono rivelate cose a cui nessuno pensava in quel momento, quindi entrambi abbiamo dovuto riformattare e avevamo una flessibilità extra non era ancora necessario, il che doveva essere mantenuto di fronte al refactoring o essere demolito.
KRyan,

2
inoltre di solito dovrai comunque scrivere un codice verificabile, che di solito significa avere un primo strato di astrazione per poter passare dall'implementazione concreta a quella di prova.
Walfrat,

49

Dalla mia esperienza, quando scrivo un'app, hai tre opzioni:

  1. Scrivi il codice esclusivamente per soddisfare i requisiti,
  2. Scrivi un codice generico che anticipi i requisiti futuri, oltre a soddisfare i requisiti attuali,
  3. Scrivi un codice che soddisfi solo i requisiti attuali, ma che sia facile da modificare in seguito per soddisfare altre esigenze.

Nel primo caso, è comune finire con un codice strettamente accoppiato privo di test unitari. Certo è veloce da scrivere, ma è difficile da testare. Ed è un vero dolore reale cambiare in seguito quando cambiano i requisiti.

Nel secondo caso, si impiegano enormi quantità di tempo nel tentativo di anticipare le esigenze future. E troppo spesso quei futuri requisiti previsti non si materializzano mai. Questo sembra lo scenario che stai descrivendo. È una perdita di sforzo per la maggior parte del tempo e si traduce in un codice inutilmente complesso che è ancora difficile da modificare quando si presenta un requisito che non era previsto per la presentazione.

L'ultimo caso è quello a cui mirare a mio avviso. Usa TDD o tecniche simili per testare il codice mentre procedi e finirai con un codice liberamente accoppiato, che è facile da modificare ma ancora veloce da scrivere. E il fatto è che, facendo questo, segui naturalmente molti dei principi SOLIDI: piccole classi e funzioni; interfacce e dipendenze iniettate. E la signora Liskov è generalmente anche felice perché le lezioni semplici con responsabilità individuali raramente si rompono del suo principio di sostituzione.

L'unico aspetto di SOLID che in realtà non si applica qui è il principio aperto / chiuso. Per biblioteche e framework, questo è importante. Per un'app indipendente, non tanto. In realtà si tratta di scrivere codice che segue " SLID ": facile da scrivere (e leggere), facile da testare e facile da mantenere.


una delle mie risposte preferite su questo sito!
TheCatWhisperer,

Non sono sicuro di come tu concluda che 1) sia più difficile da testare di 3). Più difficile apportare modifiche, certo, ma perché non riesci a testare? Semmai, un software unico è più facile da testare rispetto ai requisiti di uno più generale.
Lister,

@MrLister Le due uova mano nella mano, 1. è più difficile da testare di 3. perché la definizione implica che non è scritto "in un modo che è facile cambiare in seguito per soddisfare altre esigenze."
Mark Booth,

1
0; IMVHO stai interpretando erroneamente (anche se in modo comune) il modo in cui 'O' (aperto-chiuso) funziona. Vedi ad esempio codeblog.jonskeet.uk/2013/03/15/… - anche in piccole codebase, si tratta più di avere unità di codice autonome (ad esempio classi, moduli, pacchetti, qualunque cosa), che possono essere testate in isolamento e aggiunte / rimosso in caso di necessità. Uno di questi esempi potrebbe essere un pacchetto di metodi di utilità - indipendentemente dal modo in cui li raggruppi, dovrebbero essere "chiusi", cioè autonomi e "aperti", cioè estensibili in qualche modo.
vaxquis

A proposito, anche lo zio Bob va in quel modo ad un certo punto: “Ciò che [aperto-chiuso] significa è che dovresti sforzarti di mettere il tuo codice in una posizione tale che, quando il comportamento cambia nei modi previsti, non devi fare spazzate modifiche a tutti i moduli del sistema. Idealmente, sarai in grado di aggiungere il nuovo comportamento aggiungendo un nuovo codice e cambiando poco o nessun vecchio codice ". <- Questo ovviamente si applica anche alle piccole applicazioni, se intendono essere mai modificate o riparate (e, IMVHO, questo è di solito il caso, specialmente per quanto riguarda le correzioni ridacchiano )
vaxquis,

8

La prospettiva che hai può essere distorta dall'esperienza personale. Questa è una pendenza scivolosa di fatti che sono individualmente corretti, ma l'inferenza che ne risulta non lo è, anche se a prima vista sembra corretta.

  • I frame hanno una portata maggiore rispetto ai piccoli progetti.
  • Le cattive pratiche sono significativamente più difficili da affrontare in basi di codice più grandi.
  • La costruzione di un framework (in media) richiede uno sviluppatore più abile rispetto alla costruzione di un piccolo progetto.
  • Gli sviluppatori migliori seguono di più le buone pratiche (SOLID).
  • Di conseguenza, i framework hanno un bisogno maggiore di buone pratiche e tendono a essere creati da sviluppatori che hanno una maggiore esperienza con le buone pratiche.

Ciò significa che quando interagisci con framework e librerie più piccole, il codice di buone pratiche con cui interagirai sarà più comunemente trovato nei framework più grandi.

Questo errore è molto comune, ad es ogni medico con cui sono stato curato era arrogante. Pertanto, concludo che tutti i medici sono arroganti. Questi errori soffrono sempre facendo un'inferenza generale basata su esperienze personali.

Nel tuo caso, è possibile che tu abbia sperimentato prevalentemente buone pratiche in framework più grandi e non in librerie più piccole. La tua osservazione personale non è sbagliata, ma lo è una prova aneddotica e non universalmente applicabile.


2 modalità di programmazione: creare più o meno esattamente ciò che viene richiesto tramite requisiti e KISS (programmazione tipica) o creare logiche, servizi, ecc. Molto generici e riutilizzabili che offrano la flessibilità di cui altri sviluppatori potrebbero aver bisogno (programmazione quadro)

Lo stai confermando in qualche modo qui. Pensa a cos'è un framework. Non è un'applicazione. È un "modello" generalizzato che gli altri possono utilizzare per realizzare ogni tipo di applicazione. Logicamente, ciò significa che un framework è costruito in una logica molto più astratta per essere utilizzabile da tutti.

I costruttori di framework non sono in grado di prendere scorciatoie, perché non sanno nemmeno quali sono i requisiti delle applicazioni successive. Costruire un framework li incentiva intrinsecamente a rendere il loro codice utilizzabile per gli altri.

I costruttori di applicazioni, tuttavia, hanno la capacità di scendere a compromessi in termini di efficienza logica perché sono focalizzati sulla consegna di un prodotto. Il loro obiettivo principale non è il funzionamento del codice ma piuttosto l'esperienza dell'utente.

Per un framework, l'utente finale è un altro sviluppatore, che interagirà con il tuo codice. La qualità del codice è importante per l'utente finale.
Per un'applicazione, l'utente finale è un non sviluppatore, che non interagirà con il codice. La qualità del tuo codice non ha importanza per loro.

Questo è esattamente il motivo per cui gli architetti di un team di sviluppo spesso agiscono come esecutori di buone pratiche. Sono a un passo dalla consegna del prodotto, il che significa che tendono a guardare il codice in modo obiettivo, piuttosto che concentrarsi sulla consegna dell'applicazione stessa.


Se aggiungi questi punti di ingresso di astrazione, stai davvero soddisfacendo i requisiti degli utenti o stai creando un framework in aggiunta al tuo framework esistente e stack tecnologico per rendere più facili le aggiunte future? In quale caso stai servendo gli interessi del cliente o dello sviluppatore?

Questo è un punto interessante ed è (secondo la mia esperienza) il motivo principale per cui le persone cercano ancora di giustificare l'evitamento delle buone pratiche.

Riassumendo i seguenti punti: Saltare le buone pratiche può essere giustificato solo se i tuoi requisiti (come attualmente conosciuti) sono immutabili e non ci saranno mai cambiamenti / aggiunte alla base di codice. Avviso spoiler: raramente è così.
Ad esempio, quando scrivo un'applicazione console di 5 minuti per elaborare un determinato file, non utilizzo le buone pratiche. Perché oggi userò l'applicazione e non sarà necessario aggiornarla in futuro (sarebbe più semplice scrivere un'applicazione diversa se ne avessi bisogno di nuovo).

Supponiamo che tu possa compilare debolmente un'applicazione in 4 settimane e puoi costruirla correttamente in 6 settimane. A prima vista, costruire debolmente sembra meglio. Il cliente ottiene la sua applicazione più rapidamente e l'azienda deve dedicare meno tempo ai salari degli sviluppatori. Vinci / vinci, vero?

Tuttavia, questa è una decisione presa senza pensare al futuro. A causa della qualità della base di codice, per apportare una modifica sostanziale a quella costruita in modo scadente ci vorranno 2 settimane, mentre per apportare le stesse modifiche a quella costruita correttamente richiede 1 settimana. Potrebbero esserci molti di questi cambiamenti in arrivo in futuro.

Inoltre, vi è la tendenza per le modifiche a richiedere inaspettatamente più lavoro di quanto si pensasse inizialmente in codebase costruite in modo scadente, quindi probabilmente spingendo il tempo di sviluppo a tre settimane anziché due.

E poi c'è anche la tendenza a perdere tempo a caccia di bug. Questo è spesso il caso di progetti in cui la registrazione è stata ignorata a causa di vincoli di tempo o di totale riluttanza a implementarla perché si lavora distrattamente supponendo che il prodotto finale funzionerà come previsto.

Non è nemmeno necessario che sia un aggiornamento importante. Nel mio attuale datore di lavoro, ho visto diversi progetti realizzati in modo rapido e sporco e quando il più piccolo bug / modifica doveva essere fatto a causa di una comunicazione errata nei requisiti, che portava a una reazione a catena della necessità di refactoring modulo dopo modulo . Alcuni di questi progetti finirono per crollare (e lasciare dietro di sé un caos non mantenibile) prima ancora di rilasciare la loro prima versione.

Le decisioni di scelta rapida (programmazione rapida e sporca) sono utili solo se si può garantire in modo definitivo che i requisiti sono esattamente corretti e non dovranno mai cambiare. Nella mia esperienza, non ho mai incontrato un progetto in cui questo è vero.

Investire il tempo extra in buone pratiche è investire nel futuro. I futuri bug e modifiche saranno molto più facili quando la base di codice esistente sarà basata su buone pratiche. Pagherà già i dividendi dopo che sono state apportate solo due o tre modifiche.


1
Questa è una buona risposta, ma dovrei chiarire che non sto dicendo che abbandoniamo le buone pratiche, ma quale livello di "buone pratiche" perseguiamo? È buona norma estrarre il tuo ORM in ogni progetto perché potresti "aver bisogno" di sostituirlo con un altro in seguito? Non credo, ci sono alcuni livelli di accoppiamento che sono disposto ad accettare (ad es. Sono legato al framework, alla lingua, all'ORM, al database scelto). Se seguiamo SOLID al suo livello estremista, stiamo davvero implementando il nostro framework in cima allo stack scelto?
Igneous01,

Stai negando l'esperienza di OP come "errore". Non costruttivo
max630,

@ max630 Non lo sto negando. Ho speso buona parte della risposta spiegando perché le osservazioni di OP sono valide.
Flater,

1
@ Igneous01 SOLID non è un framework. SOLID è un'astrazione, che si riscontra più comunemente in un framework. Quando si implementa qualsiasi tipo di astrazione (incluso SOLID), esiste sempre una linea di ragionevolezza. Non puoi semplicemente astrarre per l'astrazione, passeresti anni a inventare un codice che è così sovradimensionato che è difficile da seguire. Solo in astratto ciò che sospetti ragionevolmente ti sarà utile in futuro. Tuttavia, non cadere nella trappola di presumere che tu sia legato, ad esempio, al tuo server di database corrente. Non si sa mai quale nuovo database verrà rilasciato domani.
Flater,

@ Igneous01 In altre parole, hai l'idea giusta di non voler astrarre tutto, ma ho la sensazione che ti stai sporgendo leggermente leggermente in quella direzione. È molto comune per gli sviluppatori presumere che gli attuali requisiti siano fissati nella pietra e quindi prendere decisioni architettoniche basate su tale (auspicabile) ipotesi.
Flater,

7

In che modo SOLID trasforma il codice semplice in codice framework? Non sono assolutamente uno stan per SOLID ma non è proprio ovvio che cosa intendi qui.

  • BACIO è l'essenza della S Responsabilità ingle Principio.
  • Non c'è nulla nel principio O / Principio chiuso (almeno per quello che ho capito - vedi Jon Skeet ) che va contro la scrittura del codice per fare bene una cosa. (In effetti, più il codice è strettamente focalizzato, più diventa importante la parte "chiusa").
  • Il principio di sostituzione L iskov non dice che devi permettere alle persone di sottoclassare le tue classi. Si dice che se si sottoclasse vostre classi, sottoclassi dovrebbero rispettare il contratto delle loro superclassi. Questo è solo un buon design OO. (E se non hai sottoclassi, non si applica.)
  • BACIO è anche l'essenza del I nterface segregazione Principio.
  • Il D ependency inversione principio è l'unico che posso vedere da remoto l'applicazione, ma penso che sia ampiamente incompreso e esagerata. Ciò non significa che devi iniettare tutto con Guice o Spring. Significa solo che dovresti astrarre dove appropriato e non dipendere dai dettagli di implementazione.

Ammetto di non pensare io stesso in termini SOLIDI, perché mi sono imbattuto nelle scuole di programmazione Gang of Four e Josh Bloch , non nella scuola di Bob Martin. Ma penso davvero che se pensi che "SOLID" = "aggiungendo più livelli allo stack tecnologico", stai leggendo male.


PS Non vendere i vantaggi di "UX migliore per lo sviluppatore" in breve. Il codice trascorre gran parte della sua vita in manutenzione. Uno sviluppatore sei tu .


1
Per quanto riguarda SRP, si potrebbe sostenere che qualsiasi classe con un costruttore viola SRP, poiché è possibile trasferire tale responsabilità a una fabbrica. Per quanto riguarda OCP, questo è davvero un problema a livello di framework, perché una volta pubblicata un'interfaccia per il consumo per uso esterno, non è possibile modificarla. Se l'interfaccia viene utilizzata solo all'interno del tuo progetto, è possibile modificare il contratto, perché hai il potere di modificare il contratto con il tuo codice. Per quanto riguarda l'ISP, si potrebbe sostenere che un'interfaccia dovrebbe essere definita per ogni singola azione (preservando così l'SRP) e che riguarda gli utenti esterni.
Igneous01,

3
1) si potrebbe, ma dubito che chiunque abbia mai ascoltato. 2) potresti essere sorpreso dalla rapidità con cui un progetto può raggiungere dimensioni tali da modificare liberamente le interfacce interne. 3) vedi sia 1) che 2). Basti dire che penso che stai leggendo troppo in tutti e tre i principi. Ma i commenti non sono proprio il posto giusto per affrontare questi argomenti; Ti suggerisco di porre ciascuna di esse come una domanda separata e vedere che tipo di risposte ottieni.
David Moles,

4
@ Igneous01 Usando questa logica, potresti anche abbandonare getter e setter, in quanto puoi creare una classe separata per ogni setter variabile e una per ogni getter. IE: class A{ int X; int Y; } class A_setX{ f(A a, int N) { a.X = N; }} class A_getX{ int f(A a) { return X; }} class A_setY ... etc.Penso che tu lo stia guardando da un punto di vista troppo meta con la tua affermazione di fabbrica. L'inizializzazione non è un aspetto del problema del dominio.
Aaron,

@Aaron This. Le persone possono usare SOLID per argomentare male, ma ciò non significa fare cose cattive = "seguire SOLID".
David Moles,
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.