Gestire e organizzare il numero enormemente aumentato di classi dopo il passaggio a SOLID?


50

Negli ultimi anni, stiamo lentamente passando al codice scritto progressivamente migliore, alcuni piccoli passi alla volta. Stiamo finalmente iniziando a passare a qualcosa che almeno assomiglia a SOLID, ma non siamo ancora del tutto lì. Da quando è stato effettuato il passaggio, una delle maggiori lamentele da parte degli sviluppatori è che non riescono a sopportare la revisione tra pari e attraversare dozzine e dozzine di file in cui in precedenza ogni attività richiedeva allo sviluppatore di toccare solo 5-10 file.

Prima di iniziare a fare il passaggio, la nostra architettura era organizzata in modo simile al seguente (garantito, con uno o due ordini di grandezza in più file):

Solution
- Business
-- AccountLogic
-- DocumentLogic
-- UsersLogic
- Entities (Database entities)
- Models (Domain Models)
- Repositories
-- AccountRepo
-- DocumentRepo
-- UserRepo
- ViewModels
-- AccountViewModel
-- DocumentViewModel
-- UserViewModel
- UI

Per quanto riguarda i file, tutto era incredibilmente lineare e compatto. Ovviamente c'era un sacco di duplicazione del codice, accoppiamento stretto e mal di testa, tuttavia, tutti potevano attraversarlo e capirlo. I principianti completi, persone che non avevano mai aperto Visual Studio, potevano capirlo in poche settimane. La mancanza di complessità complessiva dei file rende relativamente semplice per gli sviluppatori alle prime armi e i nuovi assunti iniziare a contribuire senza troppo tempo di accelerazione. Ma questo è praticamente dove qualsiasi beneficio dello stile di codice esce dalla finestra.

Approvo con tutto il cuore ogni tentativo che facciamo per migliorare la nostra base di codice, ma è molto comune ottenere un respingimento dal resto del team su enormi cambiamenti di paradigma come questo. Un paio dei maggiori punti critici attualmente sono:

  • Test unitari
  • Conteggio delle classi
  • Complessità della Peer Review

I test unitari sono stati una vendita incredibilmente difficile per il team in quanto tutti credono di essere una perdita di tempo e che sono in grado di gestire il loro codice molto più rapidamente nel suo complesso rispetto a ogni singolo pezzo. L'uso dei test unitari come supporto per SOLID è stato per lo più inutile ed è diventato per lo più uno scherzo a questo punto.

Il conteggio delle classi è probabilmente il maggiore ostacolo da superare. Le attività che in precedenza richiedevano 5-10 file ora possono richiedere 70-100! Mentre ciascuno di questi file ha uno scopo distinto, il volume di file puro può essere travolgente. La risposta del team è stata principalmente gemiti e grattarsi la testa. In precedenza un'attività poteva richiedere uno o due repository, un modello o due, un livello logico e un metodo controller.

Ora, per creare una semplice applicazione di salvataggio dei file, hai una classe per verificare se il file esiste già, una classe per scrivere i metadati, una classe da astrarre in DateTime.Nowmodo da poter iniettare i tempi per i test delle unità, interfacce per ogni file contenente logica, file per contenere unit test per ogni classe là fuori e uno o più file per aggiungere tutto al contenitore DI.

Per applicazioni di piccole e medie dimensioni, SOLID è una vendita super facile. Tutti vedono il vantaggio e la facilità di manutenibilità. Tuttavia, non stanno vedendo una proposta di buon valore per SOLID su applicazioni su larga scala. Quindi sto cercando di trovare modi per migliorare l'organizzazione e la gestione per farci superare i problemi crescenti.


Ho pensato di dare un esempio un po 'più forte del volume del file basato su un'attività completata di recente. Mi è stato assegnato il compito di implementare alcune funzionalità in uno dei nostri più recenti microservizi per ricevere una richiesta di sincronizzazione dei file. Quando viene ricevuta la richiesta, il servizio esegue una serie di ricerche e controlli e infine salva il documento su un'unità di rete, nonché su 2 tabelle di database separate.

Per salvare il documento sull'unità di rete, avevo bisogno di alcune classi specifiche:

- IBasePathProvider 
-- string GetBasePath() // returns the network path to store files
-- string GetPatientFolderName() // gets the name of the folder where patient files are stored
- BasePathProvider // provides an implementation of IBasePathProvider
- BasePathProviderTests // ensures we're getting what we expect

- IUniqueFilenameProvider
-- string GetFilename(string path, string fileType);
- UniqueFilenameProvider // performs some filesystem lookups to get a unique filename
- UniqueFilenameProviderTests

- INewGuidProvider // allows me to inject guids to simulate collisions during unit tests
-- Guid NewGuid()
- NewGuidProvider 
- NewGuidProviderTests

- IFileExtensionCombiner // requests may come in a variety of ways, need to ensure extensions are properly appended.
- FileExtensionCombiner
- FileExtensionCombinerTests

- IPatientFileWriter
-- Task SaveFileAsync(string path, byte[] file, string fileType)
-- Task SaveFileAsync(FilePushRequest request) 
- PatientFileWriter
- PatientFileWriterTests

Quindi sono in totale 15 classi (esclusi POCO e ponteggi) per eseguire un salvataggio abbastanza semplice. Questo numero è aumentato in modo significativo quando avevo bisogno di creare POCO per rappresentare entità in alcuni sistemi, creato alcuni repository per comunicare con sistemi di terze parti incompatibili con i nostri altri ORM e costruito metodi logici per gestire la complessità di determinate operazioni.


52
"Le attività che in passato richiedevano 5-10 file ora possono richiedere 70-100!" Come diavolo? Questo non è affatto normale. Che tipo di modifiche stai apportando che richiedono la modifica di tanti file ??
Euforico

43
Il fatto che devi modificare più file per attività (significativamente di più!) Significa che stai sbagliando SOLID. Il punto è organizzare il codice (nel tempo) in modo da riflettere i modelli di modifica osservati, rendendo le modifiche più semplici. Ogni principio presente in SOLID è accompagnato da un certo ragionamento (quando e perché dovrebbe essere applicato); sembra che vi siate messi in questa situazione applicandoli ciecamente. Stessa cosa con unit testing (TDD); se lo stai facendo senza avere una buona conoscenza di come fare il design / l'architettura, ti scaverai in un buco.
Filip Milovanović,

60
Hai chiaramente adottato il SOLID come una religione piuttosto che uno strumento pragmatico per aiutarti a fare il lavoro. Se qualcosa in SOLID sta rendendo più lavoro o rendendo le cose più difficili, non farlo.
whatsisname

25
@Euforico: il problema può verificarsi in entrambi i modi. Sospetto che tu stia rispondendo alla possibilità che 70-100 classi siano eccessive. Ma non è impossibile che questo sia solo un enorme progetto che è stato stipato in 5-10 file (ho lavorato in file 20KLOC prima ...) e 70-100 è in realtà la giusta quantità di file.
Flater

18
C'è un disordine del pensiero che io chiamo "malattia della felicità degli oggetti", che è la convinzione che le tecniche OO siano un fine in sé, piuttosto che solo una delle molte tecniche possibili per ridurre i costi di lavoro in una base di codice di grandi dimensioni. Hai una forma particolarmente avanzata, "SOLIDE malattia della felicità". SOLID non è l'obiettivo. L'obiettivo è ridurre i costi di mantenimento della base di codice. Valuta le tue proposte in quel contesto, non nel caso sia SOLID di dottrina. (Che probabilmente anche le tue proposte non siano in realtà dottrine SOLID è un buon punto da considerare.)
Eric Lippert

Risposte:


104

Ora, per creare una semplice applicazione di salvataggio dei file, hai una classe per verificare se il file esiste già, una classe per scrivere i metadati, una classe per sottrarre DateTime. Ora puoi iniettare i tempi per i test unitari, le interfacce per ogni file contenente logica, file per contenere unit test per ogni classe là fuori e uno o più file per aggiungere tutto al contenitore DI.

Penso che tu abbia frainteso l'idea di un'unica responsabilità. La sola responsabilità di una classe potrebbe essere "salvare un file". Per fare ciò, può quindi suddividere quella responsabilità in un metodo che controlla l'esistenza di un file, un metodo che scrive metadati ecc. Ciascuno di questi metodi ha quindi una singola responsabilità, che fa parte della responsabilità generale della classe.

Una classe da astrarre DateTime.Nowsuona bene. Ma hai solo bisogno di uno di questi e potrebbe essere racchiuso con altre caratteristiche ambientali in una singola classe con la responsabilità di astrarre le caratteristiche ambientali. Ancora una volta una singola responsabilità con molteplici sotto-responsabilità.

Non hai bisogno di "interfacce per ogni file contenente logica", hai bisogno di interfacce per le classi che hanno effetti collaterali, ad esempio quelle classi che leggono / scrivono in file o database; e anche allora, sono necessari solo per le parti pubbliche di tale funzionalità. Quindi, ad esempio AccountRepo, potresti non aver bisogno di alcuna interfaccia, potresti aver bisogno solo di un'interfaccia per l'accesso effettivo al database che viene iniettato in quel repository.

I test unitari sono stati una vendita incredibilmente difficile per il team in quanto tutti credono di essere una perdita di tempo e che sono in grado di gestire il loro codice molto più rapidamente nel suo complesso rispetto a ogni singolo pezzo. L'uso dei test unitari come supporto per SOLID è stato per lo più inutile ed è diventato per lo più uno scherzo a questo punto.

Questo suggerisce che hai frainteso anche i test unitari. L '"unità" di un test unitario non è un'unità di codice. Cos'è anche un'unità di codice? Una classe? Un metodo? Una variabile? Un'istruzione singola macchina? No, "unità" si riferisce a un'unità di isolamento, ovvero codice che può essere eseguito isolatamente da altre parti del codice. Un semplice test per stabilire se un test automatico è un test unitario o meno è se è possibile eseguirlo in parallelo con tutti gli altri test unitari senza influenzarne il risultato. Ci sono altre due regole empiriche nei test unitari, ma questa è la tua misura chiave.

Quindi, se parti del tuo codice possono davvero essere testate nel loro insieme senza influenzare altre parti, allora fallo.

Sii sempre pragmatico e ricorda che tutto è un compromesso. Più aderisci a DRY, più il codice deve essere strettamente accoppiato. Più si introducono le astrazioni, più è facile testare il codice, ma più è difficile da capire. Evita l'ideologia e trova un buon equilibrio tra l'ideale e mantenerlo semplice. Qui sta il punto debole della massima efficienza sia per lo sviluppo che per la manutenzione.


27
Vorrei aggiungere che un mal di testa simile si presenta quando le persone cercano di aderire al mantra troppo spesso ripetuto di "i metodi dovrebbero fare solo una cosa" e finire con tonnellate di metodi di una linea solo perché tecnicamente può essere trasformato in un metodo .
Logarr

8
Ri " Sii sempre pragmatico e ricorda che tutto è un compromesso" : i discepoli di zio Bob non sono noti per questo (non importa l'intento originale).
Peter Mortensen,

13
Per riassumere la prima parte, di solito hai un tirocinante per il caffè, non una suite completa di percolatore plug-in, interruttore a scatto, controllo se lo zucchero ha bisogno di riempimento, frigorifero aperto, latte in uscita, ottenere -cucchiai, tazze per scendere, versare-caffè, aggiungere zucchero, aggiungere latte, mescolare-tazza e consegnare stagisti. ; P
Justin Time 2 Ripristina Monica il

12
La causa principale del problema del PO sembra essere l'incomprensione della differenza tra funzioni che dovrebbero svolgere un singolo compito e classi che dovrebbero avere un'unica responsabilità.
alephzero,

6
"Le regole sono per la guida dei saggi e l'obbedienza degli sciocchi." - Douglas Bader
Calanus,

29

Le attività che in precedenza richiedevano 5-10 file ora possono richiedere 70-100!

Questo è l' opposto del principio della responsabilità singola (SRP). Per arrivare a quel punto, devi aver suddiviso la tua funzionalità in modo molto preciso, ma non è questo il problema dell'SRP: fare ciò ignora l'idea chiave di coesione .

Secondo l'SRP, il software dovrebbe essere suddiviso in moduli lungo linee definite dai loro possibili motivi di modifica, in modo che una singola modifica di progettazione possa essere applicata in un solo modulo senza richiedere modifiche altrove. Un singolo "modulo" in questo senso può corrispondere a più di una classe, ma se una modifica richiede di toccare decine di file, allora si tratta in realtà di più modifiche o si sta facendo un errore SRP.

Bob Martin, che aveva formulato originariamente l'SRP, alcuni anni fa ha scritto un post sul blog per cercare di chiarire la situazione. Discute a lungo di quale "ragione per cambiare" sia ai fini dell'SRP. Vale la pena di leggerlo nella sua interezza, ma tra le cose che meritano particolare attenzione c'è questa formulazione alternativa dell'SRP:

Riunisci le cose che cambiano per gli stessi motivi . Separare quelle cose che cambiano per diversi motivi.

(enfasi mia). L'SRP non consiste nel dividere le cose nei pezzi più piccoli possibili. Non è un buon design e il tuo team ha ragione a resistere. Rende la tua base di codice più difficile da aggiornare e mantenere. Sembra che tu stia provando a vendere la tua squadra sulla base di considerazioni sui test unitari, ma questo metterebbe il carrello davanti al cavallo.

Allo stesso modo, il principio di segregazione dell'interfaccia non dovrebbe essere considerato come assoluto. Non è più un motivo per dividere il codice in modo così fine di quanto lo sia l'SRP, e generalmente si allinea abbastanza bene con l'SRP. Che un'interfaccia contenga alcuni metodi che alcuni client non usano non è un motivo per romperlo. Sei di nuovo alla ricerca di coesione.

Inoltre, vi esorto a non prendere il principio aperto-chiuso o il principio di sostituzione di Liskov come motivo per favorire le gerarchie di eredità profonde. Non esiste un accoppiamento più stretto di una sottoclasse con le sue superclassi e l'accoppiamento stretto è un problema di progettazione. Preferisci invece la composizione rispetto all'eredità ovunque abbia senso farlo. Ciò ridurrà il tuo accoppiamento, e quindi il numero di file che potrebbe essere necessario toccare una particolare modifica, e si allinea perfettamente con l'inversione di dipendenza.


1
Immagino che sto solo cercando di capire dove sia la linea. In un compito recente, ho dovuto eseguire un'operazione abbastanza semplice, ma era in una base di codice senza molte impalcature o funzionalità esistenti. In quanto tale, tutto ciò che dovevo fare era molto semplice, ma tutto abbastanza unico e non sembrava adattarsi alle lezioni condivise. Nel mio caso, dovevo salvare un documento su un'unità di rete e registrarlo in due tabelle di database separate. Le regole che circondano ogni passaggio erano abbastanza particolari. Anche la generazione del nome file (un semplice guid) aveva alcune classi per rendere i test più convenienti.
JD Davis,

3
Ancora una volta, @JDDavis, la scelta di più classi su una sola per scopi di testabilità sta mettendo il carrello davanti al cavallo e va direttamente contro l'SRP, che richiede il raggruppamento di funzionalità coesive. Non posso darti consigli su particolari, ma il problema che i singoli cambiamenti funzionali richiedono la modifica di molti file è un problema che dovresti affrontare (e tentare di evitare), non uno che dovresti cercare di giustificare.
John Bollinger,

D'accordo, aggiungo questo. Per citare Wikipedia, "Martin definisce una responsabilità come una ragione per cambiare, e conclude che una classe o un modulo dovrebbe avere una, e una sola, ragione per essere cambiata (cioè riscritta)". e "ha affermato più di recente" Questo principio riguarda le persone "." In effetti, credo che ciò significhi che la "responsabilità" in SRP si riferisce alle parti interessate, non alla funzionalità. Una classe dovrebbe essere responsabile delle modifiche richieste da un solo stakeholder (persona che richiede di modificare il programma), in modo da cambiare il più possibile le cose in risposta a diverse parti interessate che richiedono cambiamenti.
Corrodias,

12

Le attività che in precedenza richiedevano 5-10 file ora possono richiedere 70-100!

Questa è una bugia. Le attività non hanno mai richiesto solo 5-10 file.

Non stai risolvendo alcuna attività con meno di 10 file. Perché? Perché stai usando C #. C # è un linguaggio di alto livello. Stai usando più di 10 file solo per creare ciao mondo.

Oh certo che non li noti perché non li hai scritti. Quindi non ci guardi dentro. Ti fidi di loro.

Il problema non è il numero di file. È che ora hai così tante cose che non ti fidi.

Quindi scopri come far funzionare quei test al punto che una volta superati ti fidi di questi file nel modo in cui ti fidi dei file in .NET. Questo è il punto del test unitario. A nessuno importa del numero di file. Si preoccupano del numero di cose di cui non possono fidarsi.

Per applicazioni di piccole e medie dimensioni, SOLID è una vendita super facile. Tutti vedono il vantaggio e la facilità di manutenibilità. Tuttavia, non stanno vedendo una proposta di buon valore per SOLID su applicazioni su larga scala.

Il cambiamento è difficile su applicazioni su larga scala, non importa quello che fai. La migliore saggezza da applicare qui non viene da Zio Bob. Viene da Michael Feathers nel suo libro Working Effectively with Legacy Code.

Non avviare un fest di riscrittura. Il vecchio codice rappresenta la conoscenza conquistata duramente. Eliminarlo perché ha problemi e non è espresso nel paradigma nuovo e migliorato X sta solo chiedendo una nuova serie di problemi e nessuna conoscenza acquisita con fatica.

Trova invece modi per rendere testabile il tuo vecchio codice non verificabile (il codice legacy in Feathers parla). In questa metafora il codice è come una camicia. Parti grandi vengono unite in corrispondenza di cuciture naturali che possono essere annullate per separare il codice nel modo in cui si rimuoveranno le cuciture. Fare questo per consentire di allegare "maniche" di prova che consentono di isolare il resto del codice. Ora quando crei le maniche di prova hai fiducia nelle maniche perché lo hai fatto con una camicia da lavoro. (ow, questa metafora sta iniziando a fare male).

Questa idea deriva dal presupposto che, come nella maggior parte dei negozi, gli unici requisiti aggiornati siano nel codice di lavoro. Ciò consente di bloccarlo nei test che consentono di apportare modifiche al codice di lavoro comprovato senza che perda parte del suo stato di funzionamento comprovato. Ora, con questa prima ondata di test in atto, puoi iniziare ad apportare modifiche che rendono testabile il codice "legacy" (non testabile). Puoi essere audace perché i test delle cuciture ti supportano dicendo che è quello che ha sempre fatto e i nuovi test mostrano che il tuo codice fa effettivamente quello che pensi che faccia.

Cosa c'entra tutto ciò con:

Gestire e organizzare il numero enormemente aumentato di classi dopo il passaggio a SOLID?

Astrazione.

Puoi farmi odiare qualsiasi base di codice con cattive astrazioni. Una cattiva astrazione è qualcosa che mi fa guardare dentro. Non sorprendermi quando guardo dentro. Sii praticamente quello che mi aspettavo.

Dammi un buon nome, test leggibili (esempi) che mostrano come utilizzare l'interfaccia e organizzarla in modo da poter trovare le cose e non mi importa se abbiamo usato 10, 100 o 1000 file.

Mi aiuti a trovare cose con buoni nomi descrittivi. Metti le cose con buoni nomi in cose con buoni nomi.

Se fai tutto bene, i file verranno estratti in modo da finire un'attività in base a 3-5 altri file. I file 70-100 sono ancora lì. Ma si nascondono dietro il 3 a 5. Funziona solo se ti fidi del 3 a 5 per farlo bene.

Quindi ciò di cui hai veramente bisogno è il vocabolario per trovare buoni nomi per tutte queste cose e prove di cui le persone si fidano, così smetteranno di guadare tutto. Senza quello, mi faresti impazzire anche io.

@Delioth fa un buon punto sui dolori della crescita. Quando sei abituato a tenere i piatti nell'armadio sopra la lavastoviglie, ci vuole un po 'di tempo per abituarsi a trovarli sopra il bancone della colazione. Rende alcune cose più difficili. Semplifica alcune cose. Ma provoca tutti i tipi di incubi se le persone non sono d'accordo su dove vanno i piatti. In una grande base di codice il problema è che puoi spostare solo alcuni piatti alla volta. Quindi ora hai i piatti in due posti. È confusionario. Rende difficile credere che i piatti siano dove dovrebbero essere. Se vuoi superare questo, però l'unica cosa da fare è continuare a spostare i piatti.

Il problema è che ti piacerebbe davvero sapere se vale la pena mangiare i piatti al bar per la colazione prima di passare tutte queste sciocchezze. Bene, tutto ciò che posso consigliare è andare in campeggio.

Quando provi un nuovo paradigma per la prima volta, l'ultimo posto in cui dovresti applicarlo si trova in una base di codice di grandi dimensioni. Questo vale per ogni membro del team. Nessuno dovrebbe credere che SOLID funzioni, che OOP funzioni o che funzioni la programmazione funzionale. Ogni membro del team dovrebbe avere la possibilità di giocare con la nuova idea, qualunque essa sia, in un progetto giocattolo. Permette loro di vedere almeno come funziona. Permette loro di vedere cosa non va bene. Li permette di imparare a farlo subito prima di fare un gran casino.

Dare alle persone un posto sicuro dove giocare li aiuterà ad adottare nuove idee e darà loro la certezza che i piatti potrebbero davvero funzionare nella loro nuova casa.


3
Potrebbe valere la pena ricordare che parte del dolore della domanda è probabilmente anche solo un dolore crescente - mentre, sì, potrebbero aver bisogno di creare 15 file per questa cosa ... ora non devono più scrivere un GUIDProvider o BasePathProvider , o un ExtensionProvider, ecc. È lo stesso tipo di ostacolo che si ottiene quando si avvia un nuovo progetto greenfield: un sacco di funzionalità di supporto che sono per lo più banali, stupide da scrivere e che devono ancora essere scritte. Fa schifo per costruirli, ma una volta che sono lì non dovresti pensarci ... mai.
Delioth,

@Delioth Sono incredibilmente propenso a credere che sia così. In precedenza, se avevamo bisogno di un sottoinsieme di funzionalità (supponiamo che volessimo semplicemente un URL ospitato in AppSettings), avevamo semplicemente una classe enorme che veniva passata in giro e utilizzata. Con il nuovo approccio, non c'è motivo di passare per l'intero AppSettingssolo per ottenere un URL o il percorso del file.
JD Davis,

1
Non avviare un fest di riscrittura. Il vecchio codice rappresenta la conoscenza conquistata duramente. Eliminarlo perché ha problemi e non è espresso nel paradigma nuovo e migliorato X sta solo chiedendo una nuova serie di problemi e nessuna conoscenza acquisita con fatica. Questo. Assolutamente.
Flot2011

10

Sembra che il tuo codice non sia molto ben disaccoppiato e / o le dimensioni delle tue attività siano troppo grandi.

Le modifiche al codice dovrebbero essere di 5-10 file a meno che non si stia eseguendo un codemod o un refactoring su larga scala. Se una singola modifica tocca molti file, probabilmente significa che le modifiche sono in cascata. Alcune astrazioni migliorate (più singola responsabilità, segregazione dell'interfaccia, inversione di dipendenza) dovrebbero aiutare. È anche possibile che tu abbia assunto una responsabilità troppo singola e che potresti usare un po 'più di pragmatismo: gerarchie di tipi più brevi e più sottili. Ciò dovrebbe rendere anche più facile la comprensione del codice poiché non è necessario comprendere dozzine di file per sapere cosa sta facendo il codice.

Potrebbe anche essere un segno che il tuo lavoro è troppo grande. Invece di "hey, aggiungi questa funzione" (che richiede modifiche all'interfaccia utente e modifiche API e modifiche all'accesso ai dati e modifiche di sicurezza e modifiche di test e ...) suddividendole in blocchi più utili. Questo diventa più facile da rivedere e da capire perché richiede di stabilire contratti decenti tra i bit.

E, naturalmente, i test unitari aiutano tutto questo. Ti costringono a creare interfacce decenti. Ti costringono a rendere il tuo codice abbastanza flessibile da iniettare i bit necessari per testare (se è difficile testarlo, sarà difficile riutilizzarlo). E allontanano le persone da cose troppo ingegneristiche perché più ingegneri più è necessario testare.


2
I file da 5-10 a 70-100 file sono un po 'più di un ipotetico. Il mio ultimo compito era quello di creare alcune funzionalità in uno dei nostri nuovi microservizi. Il nuovo servizio doveva ricevere una richiesta e salvare un documento. Nel fare ciò, avevo bisogno di classi per rappresentare le entità utente in 2 database separati e repository per ciascuno. Repos per rappresentare altre tabelle che dovevo scrivere. Classi dedicate per la gestione dei dati dei file e la generazione dei nomi. E la lista continua. Per non parlare del fatto che ogni classe che conteneva la logica era rappresentata da un'interfaccia in modo che potesse essere derisa per i test unitari.
JD Davis,

1
Per quanto riguarda le nostre vecchie basi di codice, sono tutte strettamente accoppiate e incredibilmente monolitiche. Con l'approccio SOLID, il solo accoppiamento tra le classi è stato nel caso dei POCO, tutto il resto è passato attraverso DI e interfacce.
JD Davis,

3
@JDDavis - aspetta, perché un microservizio funziona direttamente con più database?
Telastyn,

1
È stato un compromesso con il nostro manager di sviluppo. Preferisce massicciamente software monolitico e procedurale. Pertanto, i nostri microservizi sono molto più macro di quanto dovrebbero essere. Man mano che la nostra infrastruttura migliora, lentamente le cose si sposteranno nei loro microservizi. Per ora stiamo in qualche modo seguendo l'approccio più strano per spostare determinate funzionalità nei microservizi. Poiché più servizi hanno bisogno di accedere a una risorsa specifica, stiamo spostando anche quelli nei loro microservizi.
JD Davis,

4

Vorrei esporre alcune delle cose già menzionate qui, ma più da una prospettiva di dove sono tracciati i confini degli oggetti. Se stai seguendo qualcosa di simile al Domain-Driven Design, probabilmente i tuoi oggetti rappresenteranno aspetti della tua attività. Customere Order, per esempio, sarebbero oggetti. Ora, se dovessi fare un'ipotesi in base ai nomi delle classi che avevi come punto di partenza, la tua AccountLogicclasse aveva un codice che sarebbe stato eseguito per qualsiasi account. In OO, tuttavia, ogni classe deve avere un contesto e un'identità. Non dovresti ottenere un Accountoggetto, quindi passarlo a una AccountLogicclasse e fare in modo che quella classe apporti delle modifiche Accountall'oggetto. Questo è ciò che viene chiamato un modello anemico e non rappresenta molto bene OO. Invece, il tuoAccountLa classe dovrebbe avere un comportamento, come Account.Close()o Account.UpdateEmail(), e quei comportamenti influenzerebbero solo quell'istanza dell'account.

Ora, COME questi comportamenti vengono gestiti possono (e in molti casi dovrebbero) essere scaricati su dipendenze rappresentate da astrazioni (cioè interfacce). Account.UpdateEmail, ad esempio, potrebbe voler aggiornare un database o un file o inviare un messaggio a un bus di servizio, ecc. E ciò potrebbe cambiare in futuro. Quindi la tua Accountclasse potrebbe avere una dipendenza, ad esempio, da un IEmailUpdate, che potrebbe essere una delle molte interfacce implementate da un AccountRepositoryoggetto. Non vorresti passare un'intera IAccountRepositoryinterfaccia Accountall'oggetto perché probabilmente farebbe troppo, come cercare e trovare altri (qualsiasi) account, a cui potresti non voler Accountaccedere all'oggetto, ma anche se AccountRepositorypotresti implementare entrambi IAccountRepositorye IEmailUpdateinterfacce, ilAccountl'oggetto avrebbe accesso solo alle piccole porzioni di cui ha bisogno. Questo ti aiuta a mantenere il principio di segregazione dell'interfaccia .

Realisticamente, come altri hanno già detto, se hai a che fare con un'esplosione di classi, è probabile che stai usando il principio SOLID (e, per estensione, OO) nel modo sbagliato. SOLID dovrebbe aiutarti a semplificare il tuo codice, non a complicarlo. Ma ci vuole tempo per capire veramente cosa significano cose come SRP. La cosa più importante, tuttavia, è che il funzionamento di SOLID dipenderà molto dal dominio e dai contesti limitati (un altro termine DDD). Non ci sono proiettili d'argento o taglia unica.

Un'altra cosa che mi piace sottolineare alle persone con cui lavoro: ancora una volta, un oggetto OOP dovrebbe avere un comportamento ed è, in effetti, definito dal suo comportamento, non dai suoi dati. Se il tuo oggetto non ha altro che proprietà e campi, ha ancora un comportamento, sebbene probabilmente non il comportamento che intendevi. Una proprietà scrivibile / impostabile pubblicamente senza altra logica impostata implica che il comportamento per la sua classe di contenimento è che chiunque in qualsiasi luogo e per qualsiasi motivo e in qualsiasi momento è autorizzato a modificare il valore di quella proprietà senza alcuna logica aziendale o convalida intermedie. Di solito non è il comportamento che le persone intendono, ma se si dispone di un modello anemico, questo è generalmente il comportamento che le classi stanno annunciando a chiunque li usi.


2

Quindi sono in totale 15 classi (esclusi POCO e ponteggi) per eseguire un salvataggio abbastanza semplice.

È pazzesco .... ma queste lezioni sembrano qualcosa che scrivo da solo. Quindi diamo un'occhiata a loro. Ignoriamo le interfacce e i test per ora.

  • BasePathProvider- IMHO qualsiasi progetto non banale che lavora con i file ne ha bisogno. Quindi suppongo che ci sia già una cosa del genere e puoi usarla così com'è.
  • UniqueFilenameProvider - Sicuro, ce l'hai già, vero?
  • NewGuidProvider - Lo stesso caso, a meno che tu non stia solo usando GUID.
  • FileExtensionCombiner - Lo stesso caso.
  • PatientFileWriter - Immagino che questa sia la classe principale per l'attività corrente.

Per me, sembra buono: devi scrivere una nuova classe che ha bisogno di quattro classi di supporto. Tutte e quattro le classi di supporto sembrano abbastanza riutilizzabili, quindi scommetto che sono già da qualche parte nella tua base di codice. Altrimenti, o è sfortuna (sei davvero la persona nel tuo team a scrivere file e utilizzare GUID ???) o qualche altro problema.


Per quanto riguarda le classi di test, sicuramente, quando crei una nuova classe o la aggiorni, dovrebbe essere testata. Quindi scrivere cinque classi significa scrivere anche cinque lezioni di prova. Ma questo non rende il design più complicato:

  • Non userete mai le classi di test altrove poiché verranno eseguite automaticamente e questo è tutto.
  • Vuoi sempre rivederli, a meno che non aggiorni le classi sotto test o a meno che non le usi come documentazione (idealmente, i test mostrano chiaramente come dovrebbe essere usata una classe).

Per quanto riguarda le interfacce, sono necessarie solo quando il framework DI o il framework di test non sono in grado di gestire le classi. Potresti vederli come un tributo per strumenti imperfetti. Oppure potresti vederli come un'astrazione utile che ti consente di dimenticare che ci sono cose più complicate: leggere la fonte di un'interfaccia richiede molto meno tempo che leggere la fonte della sua implementazione.


Sono grato per questo punto di vista. In questo caso specifico, stavo scrivendo funzionalità in un microservizio abbastanza nuovo. Sfortunatamente, anche nella nostra base di codice principale, mentre abbiamo in uso alcuni dei precedenti, nessuno di questi è realmente riutilizzabile in remoto. Tutto ciò che deve essere riutilizzabile è finito in una classe statica o è semplicemente copiato e incollato attorno al codice. Penso di essere andato ancora un po 'lontano, ma concordo sul fatto che non tutto deve essere completamente sezionato e disaccoppiato.
JD Davis,

@JDDavis Stavo cercando di scrivere qualcosa di diverso dalle altre risposte (con cui concordo principalmente). Ogni volta che copi e incolli qualcosa, stai impedendo il riutilizzo in quanto invece di generalizzare qualcosa crei un altro pezzo di codice non riutilizzabile, che ti costringerà a copiare e incollare più un giorno. IMHO è il secondo peccato più grande, subito dopo aver seguito ciecamente le regole. Devi trovare il tuo punto debole, in cui le seguenti regole ti rendono più produttivo (in particolare i cambiamenti futuri) e occasionalmente romperli un po 'aiuta nei casi in cui lo sforzo non sarebbe inappropriato. È tutto relativo.
Maaartinus,

@JDDavis E tutto dipende dalla qualità dei tuoi strumenti. Esempio: ci sono persone che sostengono che DI sia intraprendente e complicato, mentre io sostengo che sia per lo più gratuito . +++Per quanto riguarda la violazione delle regole: ci sono quattro classi, ho bisogno in alcuni punti, dove potrei iniettarle solo dopo un importante refactoring rendendo il codice più brutto (almeno per i miei occhi), quindi ho deciso di trasformarle in singoli (un programmatore migliore potrebbe trovare un modo migliore, ma ne sono felice; il numero di questi singoli non cambia da secoli.
Maaartinus,

Questa risposta esprime praticamente ciò che stavo pensando quando l'OP ha aggiunto l'esempio alla domanda. @JDDavis Consentitemi di aggiungere che è possibile salvare alcuni codici / classi di piatti utilizzando gli strumenti funzionali per i casi semplici. Un fornitore di GUI, ad esempio - invece di introdurre una nuova interfaccia in una nuova classe per questo, perché non utilizzarla solo Func<Guid>per questo e iniettare un metodo anonimo come ()=>Guid.NewGuid()nel costruttore? E non è necessario testare questa funzione .Net framework, questo è qualcosa che Microsoft ha fatto per te. In totale, questo ti farà risparmiare 4 lezioni.
Doc Brown,

... e dovresti controllare se gli altri casi che hai presentato possono essere semplificati allo stesso modo (probabilmente non tutti).
Doc Brown,

2

A seconda delle astrazioni, la creazione di classi a responsabilità singola e la scrittura di test unitari non sono scienze esatte. È perfettamente normale oscillare troppo in una direzione durante l'apprendimento, andare all'estremo e quindi trovare una norma che abbia senso. Sembra solo che il tuo pendolo abbia oscillato troppo e potrebbe anche essere bloccato.

Ecco dove sospetto che questo stia andando fuori dai binari:

I test unitari sono stati una vendita incredibilmente difficile per il team in quanto tutti credono di essere una perdita di tempo e che sono in grado di gestire il loro codice molto più rapidamente nel suo complesso rispetto a ogni singolo pezzo. L'uso dei test unitari come supporto per SOLID è stato per lo più inutile ed è diventato per lo più uno scherzo a questo punto.

Uno dei vantaggi che deriva dalla maggior parte dei principi SOLID (certamente non l'unico vantaggio) è che semplifica la scrittura di unit test per il nostro codice. Se una classe dipende da un'astrazione possiamo deridere le astrazioni. Le astrazioni che sono separate sono più facili da deridere. Se una classe fa una cosa, è probabile che abbia una complessità inferiore, il che significa che è più facile conoscere e testare tutti i suoi possibili percorsi.

Se il tuo team non sta scrivendo test unitari, stanno accadendo due cose correlate:

In primo luogo, stanno facendo molto lavoro extra per creare tutte queste interfacce e classi senza realizzare tutti i vantaggi. Ci vuole un po 'di tempo e pratica per vedere come scrivere test unit ci semplifichi la vita. Ci sono ragioni per cui le persone che imparano a scrivere unit test si attengono a questo, ma devi persistere abbastanza a lungo da scoprirle da te. Se la tua squadra non ci sta provando, sentiranno che il resto del lavoro extra che stanno facendo è inutile.

Ad esempio, cosa succede quando hanno bisogno di refactoring? Se hanno un centinaio di piccole classi ma nessun test per dire loro se i loro cambiamenti funzioneranno o meno, quelle classi e interfacce extra sembreranno un peso, non un miglioramento.

In secondo luogo, la scrittura di unit test può aiutarti a capire quanta astrazione ha realmente bisogno il tuo codice. Come ho detto, non è una scienza. Partiamo male, viriamo dappertutto e miglioriamo. I test unitari hanno un modo peculiare di integrare SOLID. Come fai a sapere quando è necessario aggiungere un'astrazione o rompere qualcosa? In altre parole, come fai a sapere quando sei "abbastanza SOLIDO?" Spesso la risposta è quando non puoi provare qualcosa.

Forse il tuo codice sarebbe testabile senza creare tante piccole astrazioni e classi. Ma se non stai scrivendo i test, come puoi dirlo? Quanto lontano andiamo? Possiamo diventare ossessionati dallo spezzare le cose sempre più piccoli. È una tana di coniglio. La capacità di scrivere test per il nostro codice ci aiuta a vedere quando abbiamo raggiunto il nostro scopo in modo da poter smettere di ossessionare, andare avanti e divertirci a scrivere altro codice.

I test unitari non sono un proiettile d'argento che risolve tutto, ma sono davvero un fantastico proiettile che migliora la vita degli sviluppatori. Non siamo perfetti, e nemmeno i nostri test. Ma i test ci danno fiducia. Ci aspettiamo che il nostro codice sia corretto e siamo sorpresi quando è sbagliato, non viceversa. Non siamo perfetti e nemmeno i nostri test. Ma quando il nostro codice viene testato abbiamo fiducia. Siamo meno propensi a morderci le unghie quando viene distribuito il nostro codice e ci chiediamo cosa succederà questa volta e se sarà colpa nostra.

Inoltre, una volta capito, scrivere test unit rende lo sviluppo del codice più veloce, non più lento. Trascorriamo meno tempo a rivisitare il vecchio codice o eseguire il debug per trovare problemi simili a aghi in un pagliaio.

Gli insetti diminuiscono, facciamo di più e sostituiamo l'ansia con la fiducia. Non è una moda o olio di serpente. È vero. Molti sviluppatori lo confermeranno. Se la tua squadra non l'ha sperimentato, deve superare quella curva di apprendimento e superare la gobba. Dai una possibilità, rendendoti conto che non otterranno risultati istantaneamente. Ma quando succede, saranno contenti di averlo fatto e non guarderanno mai indietro. (O diventeranno paria isolati e scriveranno post su blog arrabbiati su come i test unitari e la maggior parte delle altre conoscenze di programmazione accumulate siano una perdita di tempo.)

Da quando è stato effettuato il passaggio, una delle maggiori lamentele da parte degli sviluppatori è che non riescono a sopportare la revisione tra pari e attraversare dozzine e dozzine di file in cui in precedenza ogni attività richiedeva allo sviluppatore di toccare solo 5-10 file.

La revisione tra pari è molto più semplice quando tutti i test unitari superano e gran parte di quella revisione si sta solo assicurando che i test siano significativi.

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.