Perché abbiamo bisogno di così tante classi nei modelli di progettazione?

204

Sono uno sviluppatore junior tra gli anziani e sto lottando molto con la comprensione del loro modo di pensare e ragionare.

Sto leggendo Domain-Driven Design (DDD) e non riesco a capire perché dobbiamo creare così molte classi Se seguiamo quel metodo di progettazione del software, finiamo con 20-30 classi che possono essere sostituite con al massimo due file e 3-4 funzioni. Sì, questo potrebbe essere disordinato, ma è molto più gestibile e leggibile.

Ogni volta che voglio vedere che cosa fa un po 'di EntityTransformationServiceImpl , devo seguire molte classi, interfacce, le loro chiamate di funzioni, costruttori, la loro creazione e così via.

Semplice matematica:

  • 60 righe di codice fittizio vs 10 classi X 10 (diciamo che abbiamo completamente diverse logiche del genere) = 600 righe di codice disordinato contro 100 classi + un po 'di più per avvolgerle e gestirle; non dimenticare di aggiungere l'iniezione di dipendenza.
  • Leggere 600 righe di codice disordinato = un giorno
  • 100 classi = una settimana, ancora dimentica quale si fa cosa, quando

Tutti dicono che è facile da mantenere, ma per cosa? Ogni volta che aggiungi nuove funzionalità, aggiungi altre cinque classi con fabbriche, entità, servizi e valori. Mi sento come se questo tipo di codice si muovesse molto più lentamente del codice disordinato.

Diciamo che se scrivi un codice disordinato di 50K LOC in un mese, la cosa DDD richiede molte revisioni e modifiche (non mi importa test in entrambi i casi). Una semplice aggiunta può richiedere una settimana se non di più.

In un anno, scrivi un sacco di codice disordinato e puoi persino riscriverlo più volte, ma con lo stile DDD, non hai ancora abbastanza funzionalità per competere con il codice disordinato.

Per favore, spiega. Perché abbiamo bisogno di questo stile DDD e di molti modelli?

UPD 1 : ho ricevuto tante grandi risposte, puoi per favore aggiungere commenti da qualche parte o modificare la risposta con il link per la lista di lettura (non sono sicuro da quale iniziare, DDD, Design Patterns, UML, Code Complete, Refactoring, Pragmatic, ... così tanti buoni libri), ovviamente con sequenza, così posso anche iniziare a capire e diventare anziano come alcuni di voi.

    
posta user1318496 10.04.2018 - 18:10
fonte

10 risposte

328

Questo è un problema di ottimizzazione

Un buon ingegnere comprende che un problema di ottimizzazione non ha senso senza un obiettivo. Non puoi semplicemente ottimizzare, devi ottimizzare per qualcosa. Ad esempio, le opzioni del compilatore includono l'ottimizzazione della velocità e l'ottimizzazione della dimensione del codice; questi sono obiettivi a volte opposti.

Mi piace dire a mia moglie che la mia scrivania è ottimizzata per gli add. È solo una pila, ed è molto facile aggiungere roba. Mia moglie lo preferirebbe se ottimizzassi il recupero, organizzassi un po 'le mie cose in modo da poter trovare le cose. Ciò rende più difficile, naturalmente, aggiungere.

Il software è allo stesso modo. Puoi certamente ottimizzare la creazione del prodotto, generare un sacco di codice monolitico il più rapidamente possibile, senza preoccuparti di organizzare esso. Come hai già notato, questo può essere molto, molto veloce. L'alternativa è ottimizzare per la manutenzione: rendere la creazione un tocco più difficile, ma rendere le modifiche più facili o meno rischiose. Questo è lo scopo del codice strutturato.

Suggerirei che un prodotto software di successo verrà creato una sola volta ma modificato molte, molte volte. Ingegneri esperti hanno visto basi di codice non strutturate assumere una vita propria e diventare prodotti, crescendo in dimensioni e complessità, fino a quando anche piccoli cambiamenti sono molto difficili da fare senza introdurre enormi rischi. Se il codice fosse strutturato, il rischio può essere contenuto. Questo è il motivo per cui affrontiamo tutti questi problemi.

La complessità deriva dalle relazioni, non dagli elementi

Nella tua analisi noto che stai guardando quantità, quantità di codice, numero di classi, ecc. Mentre queste sono interessanti, il vero impatto viene dalle relazioni tra elementi, che esplodono in modo combinatorio. Ad esempio, se hai 10 funzioni e nessuna idea che dipende da quale, hai 90 possibili relazioni (dipendenze) di cui ti devi preoccupare-- ognuna delle dieci funzioni potrebbe dipendere da una qualsiasi delle altre nove funzioni, e 9 x 10 = 90. Potresti non sapere quali funzioni modificano le variabili o il modo in cui i dati vengono passati, così i programmatori hanno un sacco di cose di cui preoccuparsi quando risolvono qualsiasi problema particolare. Al contrario, se hai 30 classi ma sono organizzate in modo intelligente, possono avere solo 29 relazioni, ad es. se sono stratificati o disposti in una pila.

In che modo ciò influenza il throughput del tuo team? Bene, ci sono meno dipendenze, il problema è molto più trattabile; I programmatori non devono destreggiarsi tra un milione di cose in testa ogni volta che apportano un cambiamento. Ridurre al minimo le dipendenze può essere un enorme incentivo per la tua capacità di ragionare su un problema in modo competente. Questo è il motivo per cui dividiamo le cose in classi o moduli e le variabili di ambito nel modo più stretto possibile e usiamo SOLID principi.

    
risposta data 10.04.2018 - 20:51
fonte
65

Bene, prima di tutto, la leggibilità e la maneggevolezza sono spesso negli occhi di chi guarda.

Ciò che è leggibile per te potrebbe non essere per il tuo prossimo.

La manutenibilità spesso si riduce alla rilevabilità (con quale facilità un comportamento o un concetto viene scoperto nella base di codice) e la rilevabilità è un'altra cosa soggettiva.

DDD

Uno dei modi in cui DDD aiuta i team di sviluppatori consiste nel suggerire un modo specifico (ma ancora soggettivo) di organizzare i concetti e i comportamenti del codice. Questa convenzione rende più facile scoprire le cose e quindi più facile mantenere l'applicazione.

  • I concetti di dominio sono codificati come entità e aggregati
  • Il comportamento del dominio risiede in entità o servizi di dominio
  • La coerenza è assicurata dalle directory di aggregazione
  • I problemi di persistenza sono gestiti dai repository

Questa disposizione non è oggettivamente più facile da mantenere. Tuttavia, misurabili è più facile da mantenere quando tutti capiscono che stanno operando in un contesto DDD.

Corsi

Le classi aiutano con manutenibilità, leggibilità, rilevabilità, ecc ... perché sono una ben nota convenzione .

Nelle impostazioni orientate agli oggetti, le Classi vengono in genere utilizzate per raggruppare comportamenti strettamente correlati e per incapsulare lo stato che deve essere controllato attentamente.

So che sembra molto astratto, ma puoi pensarci in questo modo:

Con Classi, non devi necessariamente sapere in che modo funziona il codice in esse contenuto. Devi solo sapere di cosa è responsabile per la classe.

Le classi ti consentono di ragionare sulla tua applicazione in termini di interazioni tra componenti ben definiti .

Ciò riduce l'onere cognitivo nel ragionamento su come funziona la tua applicazione. Invece di ricordare quali 600 linee di codice riescono, puoi pensare a come interagiscono 30 componenti .

E, considerando che questi 30 componenti probabilmente coprono 3 strati della tua applicazione, probabilmente dovrai ragionare su circa 10 componenti alla volta.

Sembra abbastanza gestibile.

Sommario

In sostanza, ciò che vedi sono gli sviluppatori senior:

Stanno suddividendo l'applicazione in facile ragionare sulle classi.

Quindi li organizzano in facili da ragionare sui livelli.

Lo stanno facendo perché sanno che, man mano che l'applicazione cresce, diventa sempre più difficile ragionare su di esso nel suo complesso. Abbattendolo in livelli e classi significa che non devono mai ragionare sull'intera applicazione. Hanno solo sempre bisogno di ragionare su un piccolo sottoinsieme di esso.

    
risposta data 10.04.2018 - 18:35
fonte
29

Please explain me, why do we need this DDD style, lots of Patterns?

In primo luogo, una nota: la parte importante di DDD non sono i pattern , ma l'allineamento dello sforzo di sviluppo con il business. Greg Young ha osservato che i capitoli nel libro blu sono in l'ordine sbagliato .

Ma alla tua domanda specifica: tendono ad essere molte più classi di quelle che ti aspetteresti, (a) perché si sta facendo uno sforzo per distinguere il comportamento del dominio dall'impianto idraulico e (b) perché si sta compiendo uno sforzo extra per assicurati che i concetti nel modello di dominio siano espressi esplicitamente.

Senza mezzi termini, se hai due concetti diversi nel dominio, allora dovrebbero essere distinti nel modello anche se capita di condividere lo stesso nella rappresentazione della memoria .

In effetti, stai costruendo un linguaggio specifico del dominio che descrive il tuo modello nella lingua dell'azienda, in modo che un esperto di dominio possa essere in grado di guardarlo e individuare gli errori.

Inoltre, si nota un po 'più di attenzione per la separazione delle preoccupazioni; e la nozione di isolare i consumatori di una certa capacità dai dettagli di implementazione. Vedi D. L. Parnas . I confini chiari ti consentono di modificare o estendere l'implementazione senza che gli effetti si increspino per tutta la tua soluzione.

La motivazione qui: per un'applicazione che fa parte della competenza di base di un'azienda (ovvero un luogo in cui si ottiene un vantaggio competitivo), vorrai essere in grado di sostituire facilmente e a buon mercato un comportamento di dominio con una variazione migliore . In effetti, si hanno parti del programma che si desidera evolvere rapidamente (come si evolve lo stato nel tempo) e altre parti che si desidera modificare lentamente (come viene memorizzato lo stato); gli strati extra di astrazione aiutano ad evitare di accoppiare inavvertitamente l'uno con l'altro.

In tutta onestà: alcuni di questi sono anche danni al cervello a oggetti. I modelli originariamente descritti da Evans sono basati su progetti Java a cui ha partecipato più di 15 anni fa; stato e comportamento sono strettamente accoppiati in quello stile, il che porta a complicazioni che potresti preferire evitare; vedi Percezione e azione di Stuart Halloway o Inlining Code di John Carmack.

No matter what language you work in, programming in a functional style provides benefits. You should do it whenever it is convenient, and you should think hard about the decision when it isn't convenient. Carmack, 2012

    
risposta data 10.04.2018 - 18:38
fonte
29

Ci sono molti buoni punti nelle altre risposte, ma penso che manchi o non enfatizzi un importante errore concettuale che fai:

Stai confrontando lo sforzo per comprendere il programma completo.

Questo non è un compito realistico con la maggior parte dei programmi. Anche i programmi semplici sono costituiti da così tanto codice che è semplicemente impossibile gestirli tutti in testa in un dato momento. La tua unica possibilità è quella di trovare la parte di un programma che è rilevante per l'attività in corso (correggere un bug, implementare una nuova funzione) e lavorare con quello.

Se il tuo programma consiste di enormi funzioni / metodi / classi questo è quasi impossibile. Dovrai capire centinaia di righe di codice solo per decidere se questo pezzo di codice è pertinente al tuo problema. Con le stime che hai dato diventa facile passare una settimana solo per trovare il pezzo di codice su cui devi lavorare.

Confrontalo con un codice base con piccole funzioni / metodi / classi nominati e organizzati in pacchetti / spazi dei nomi che rendono ovvio dove trovare / inserire un dato pezzo di logica. Se fatto bene, in molti casi puoi saltare direttamente nel posto giusto per risolvere il tuo problema, o almeno in un luogo da cui sparare al debugger ti porterà nel punto giusto in un paio di hop.

Ho lavorato in entrambi i tipi di sistemi. La differenza può facilmente essere di due ordini di grandezza in termini di prestazioni per compiti comparabili e dimensioni del sistema comparabili.

L'effetto che ha su altre attività:

    Il test
  • diventa molto più semplice con le unità più piccole
  • meno conflitti di unione perché le possibilità per due sviluppatori di lavorare sullo stesso pezzo di codice sono minori.
  • meno duplicazioni perché è più facile riutilizzare i pezzi (e trovare i pezzi in primo luogo).
risposta data 11.04.2018 - 08:05
fonte
19

Perché il codice di test è più difficile rispetto alla scrittura del codice

Molte risposte hanno dato un buon ragionamento dal punto di vista dello sviluppatore - che la manutenzione può essere ridotta, al costo di rendere il codice più faticoso da scrivere in primo luogo.

Tuttavia, c'è un altro aspetto da considerare: il test può essere solo a grana fine come il codice originale.

Se scrivi tutto in un monolite, l'unico test efficace che puoi scrivere è "dati questi input, l'output è corretto?". Questo significa che tutti i bug trovati, sono mirati a "da qualche parte in quella gigantesca pila di codice".

Ovviamente, puoi avere uno sviluppatore seduto con un debugger e trovare esattamente dove è stato avviato il problema e lavorare su una correzione. Ciò richiede molte risorse ed è un cattivo uso del tempo di uno sviluppatore. Immagina che un bug sempre minore tu abbia, comporta uno sviluppatore che ha bisogno di eseguire di nuovo il debug dell'intero programma.

La soluzione: molti test più piccoli che individuano uno specifico, potenziale errore ciascuno.

Questi piccoli test (ad esempio Test unitari) hanno il vantaggio di controllare un'area specifica della base di codici e di aiutare a trovare errori entro un ambito limitato. Questo non solo accelera il debug quando un test fallisce, ma significa anche che se tutti i piccoli test falliscono - è più facile trovare il fallimento nei test più grandi (cioè se non è in una funzione testata specifica, deve essere nell'interazione tra di loro).

Come dovrebbe essere chiaro, per fare test più piccoli significa che il tuo codebase deve essere diviso in blocchi più piccoli testabili. Il modo per farlo, su una base di codice commerciale di grandi dimensioni, spesso genera un codice che assomiglia a quello con cui stai lavorando.

Proprio come una nota a margine: questo non vuol dire che le persone non prenderanno le cose "troppo lontano". Ma esiste una ragione legittima per separare le basi di codice in parti più piccole / meno collegate - se fatto in modo ragionevole.

    
risposta data 11.04.2018 - 15:09
fonte
17

Please explain me, why do we need this DDD style, lots of Patterns?

Molti (la maggior parte ...) tra noi non hanno bisogno di loro. Teorici e programmatori esperti e molto avanzati scrivono libri su teorie e metodologie come risultato di molte ricerche e della loro profonda esperienza - ciò non significa che tutto ciò che scrivono sia applicabile a ogni programmatore nella loro pratica quotidiana.

Come sviluppatore junior, è bello leggere libri come quello che hai citato per ampliare le tue prospettive e renderti consapevole di alcuni problemi. Ti eviterà anche di sentirti imbarazzato e sconcertato quando i tuoi colleghi più esperti utilizzano una terminologia che non conosci. Se trovi qualcosa di molto difficile e non sembra avere senso o sembrare utile, non ucciderti su di esso - ti basta archiviarlo in testa che esiste un tale concetto o approccio.

Nel tuo sviluppo quotidiano, a meno che tu non sia un accademico, il tuo compito è trovare soluzioni praticabili e manutenibili. Se le idee che trovi in un libro non ti aiutano a raggiungere questo obiettivo, non preoccuparti per questo, finché il tuo lavoro sarà ritenuto soddisfacente.

Potrebbe sopraggiungere un momento in cui scoprirai che puoi usare un po 'di quello che leggi ma che non è stato abbastanza "immediato" all'inizio o forse no.

    
risposta data 11.04.2018 - 00:53
fonte
8

Poiché la tua domanda copre un sacco di motivi, con molte ipotesi, esporrò l'argomento della tua domanda:

Why do we need so many classes in design patterns

Non lo facciamo. Non esiste una regola generalmente accettata che dice che ci devono essere molte classi nei modelli di progettazione.

Esistono due guide fondamentali per decidere dove inserire il codice e come tagliare i compiti in diverse unità di codice:

  • Coesione: qualsiasi unità di codice (sia esso un pacchetto, un file, una classe o un metodo) dovrebbe appartenere insieme . Ad esempio, qualsiasi metodo specifico dovrebbe avere un task one e farlo bene. Qualsiasi classe dovrebbe essere responsabile dell'argomento one (qualunque cosa sia). Vogliamo un'elevata coesione.
  • Accoppiamento: qualsiasi coppia di codice dovrebbe dipendere il meno possibile l'una dall'altra - in particolare non dovrebbero esserci dipendenze circolari. Vogliamo un accoppiamento basso.

Perché quei due dovrebbero essere importanti?

  • Coesione: un metodo che fa un sacco di cose (ad esempio, uno script CGI vecchio stile che fa GUI, logica, accesso DB ecc. tutto in un lungo pasticcio di codice) diventa ingombrante. Al momento della stesura di questo articolo, si è tentati di mettere il tuo treno di pensieri in un metodo lungo. Funziona, è facile da presentare e così, e puoi farcela. I problemi sorgono più tardi: dopo alcuni mesi, potresti dimenticare ciò che hai fatto. Una riga di codice in alto potrebbe essere a poche schermate da una linea in basso; è facile dimenticare tutti i dettagli. Qualsiasi modifica in qualsiasi parte del metodo può rompere qualsiasi quantità di comportamenti della cosa complessa. Sarà abbastanza ingombrante per refactoring o riutilizzare parti del metodo. E così via.
  • Accoppiamento: ogni volta che cambi un'unità di codice, puoi rompere potenzialmente tutte le altre unità che dipendono da essa. In linguaggi rigorosi come Java, è possibile ottenere suggerimenti durante la compilazione (ad esempio, sui parametri, sulle eccezioni dichiarate e così via). Ma molti cambiamenti non stanno innescando tale (cioè cambiamenti comportamentali), e altre, più dinamiche, lingue, non hanno tali possibilità. Più alto è l'accoppiamento, più difficile è cambiare qualcosa e potresti fermarti, dove è necessaria una completa riscrittura per raggiungere un obiettivo.

Questi due aspetti sono i "driver" di base per qualsiasi scelta di "dove mettere cosa" in qualsiasi linguaggio di programmazione e qualsiasi paradigma (non solo OO). Non tutti ne sono consapevoli esplicitamente, e ci vuole tempo, spesso anni, per ottenere un sentimento davvero radicato e automatico su come questi software influenzano.

Ovviamente, questi due concetti non ti dicono nulla su cosa effettivamente fare . Alcune persone sbagliano dalla parte di troppo, altre dalla parte di troppo poco. Alcuni linguaggi (guardandoti qui, Java) tendono a favorire molte classi a causa della natura estremamente statica e pedante del linguaggio stesso (questa non è un'istruzione di valore, ma è ciò che è). Ciò diventa particolarmente evidente quando lo si confronta con linguaggi dinamici e più espressivi, ad esempio Ruby.

Un altro aspetto è che alcune persone si iscrivono all'approccio agile del solo codice di scrittura che è necessario adesso , e al refactoring molto più tardi, quando necessario. In questo stile di sviluppo, non creerai un interface quando hai solo una classe di implementazione. Dovresti semplicemente implementare la classe concreta. Se, in seguito, hai bisogno di una seconda classe, dovresti refactoring.

Alcune persone semplicemente non funzionano in questo modo. Creano interfacce (o, più in generale, classi di base astratte) per qualsiasi cosa possa mai essere usata più in generale; questo porta rapidamente all'esplosione di classe.

Ancora, ci sono argomenti a favore e contro, e non importa quale io o voi preferiate. Nella tua vita di sviluppatore di software, incontrerai tutti gli estremi, dai lunghi metodi di spaghetti, ai progetti di classe illuminati, appena grandi abbastanza, fino a schemi di classi incredibilmente esplosi che sono molto più ingegnosi. Man mano che diventi più esperto, crescerai di più in ruoli "architettonici" e potrai iniziare a influenzarlo nella direzione che desideri. Scoprirai un centro d'oro per te stesso e troverai comunque che molte persone non saranno d'accordo con te, qualunque cosa tu faccia.

Quindi, mantenere una mente aperta è il punto più importante, qui, e questo sarebbe il mio primo consiglio per te, visto che sembri molto doloroso per il problema, a giudicare dal resto della tua domanda ...

    
risposta data 11.04.2018 - 19:22
fonte
7

I codificatori esperti hanno imparato:

  • Perché quei programmi e le lingue più piccoli iniziano a crescere, specialmente quelli di successo. Quelli semplici schemi che hanno funzionato a un livello semplice non si ridimensionano.
  • Perché può sembrare oneroso dover aggiungere / modificare diversi artefatti per ogni aggiunta / modifica, ma sai cosa aggiungere ed è facile farlo. 3 minuti di battitura digitano 3 ore di codice intelligente.
  • Un sacco di piccole classi non è "disordinato" solo perché non capisci perché il sovraccarico sia effettivamente una buona idea
  • Senza la conoscenza del dominio aggiunta, il codice "ovvio per me" è spesso misterioso per i miei compagni di squadra ... più il futuro per me.
  • La conoscenza tribale può rendere i progetti incredibilmente difficili da aggiungere ai membri del team in modo semplice e veloce per essere produttivi rapidamente.
  • Il naming è uno dei due problemi più difficili nell'informatica, è ancora vero e molte classi e metodi sono spesso un intenso esercizio di denominazione che molti trovano essere un grande vantaggio.
risposta data 11.04.2018 - 04:06
fonte
2

Le risposte finora, tutte positive, dopo aver iniziato con la ragionevole supposizione che al richiedente manca qualcosa, che anche il richiedente riconosce. È anche possibile che il richiedente sia fondamentalmente nel giusto, e vale la pena discutere di come questa situazione può verificarsi.

L'esperienza e la pratica sono potenti e se gli anziani acquisiscono la loro esperienza in progetti grandi e complessi in cui l'unico modo per tenere le cose sotto controllo è con un sacco di EntityTransformationServiceImpl , diventano veloci e constrongvoli con schemi di progettazione e aderenza stretta al DDD. Sarebbero molto meno efficaci utilizzando un approccio leggero, anche per i programmi di piccole dimensioni. Per quanto strano, dovresti adattarti e sarà una grande esperienza di apprendimento.

Pur adattandosi, dovresti prenderlo come una lezione sull'equilibrio tra l'apprendimento di un singolo approccio in profondità, fino al punto in cui puoi farlo funzionare ovunque, anziché rimanere versatile e sapere quali strumenti sono disponibili senza necessariamente essere un esperto con qualsiasi di loro. Ci sono vantaggi per entrambi ed entrambi sono necessari nel mondo.

    
risposta data 13.04.2018 - 08:22
fonte
-4

Rendere più classe e funzione a seconda dell'utilizzo sarà un ottimo approccio per risolvere i problemi in un modo specifico che aiuterà in futuro a risolvere eventuali problemi.

Le classi multiple aiutano a identificare il loro lavoro di base e ogni classe può essere chiamata in qualsiasi momento.

Tuttavia, se hai molte classi e funzioni dal loro nome, sono facili da chiamare e gestire. Questo è chiamato un codice pulito.

    
risposta data 13.04.2018 - 17:16
fonte

Leggi altre domande sui tag