Quanto è grande per una classe?


29

Sono uno sviluppatore di lunga data (ho 49 anni) ma piuttosto nuovo nello sviluppo orientato agli oggetti. Ho letto di OO dalla Eiffel di Bertrand Meyer, ma ho programmato davvero poco OO.

Il punto è che ogni libro sul design di OO inizia con un esempio di barca, auto o qualsiasi oggetto comune che usiamo molto spesso e iniziano ad aggiungere attributi e metodi e spiegano come modellano lo stato dell'oggetto e cosa si può fare esso.

Quindi di solito vanno qualcosa come "migliore è il modello, meglio rappresenta l'oggetto nell'applicazione e meglio tutto viene fuori".

Fin qui tutto bene, ma, d'altra parte, ho trovato diversi autori che danno ricette come "una classe dovrebbe rientrare in una sola pagina" (aggiungerei "su quale dimensione del monitor?" Ora che non proviamo a non per stampare il codice!).

Prendiamo ad esempio una PurchaseOrderclasse, che ha una macchina a stati finiti che controlla il suo comportamento e una raccolta di PurchaseOrderItem, uno degli argomenti qui al lavoro è che dovremmo usare una PurchaseOrderclasse semplice, con alcuni metodi (poco più di una classe di dati), e avere una PurchaseOrderFSM"classe di esperti" che gestisce la macchina a stati finiti per il PurchaseOrder.

Direi che rientra nella classifica "Feature Envy" o "Inappropriate Intimacy" del post su Code Smells di Jeff Atwood su Coding Horror. Lo definirei semplicemente buonsenso. Se posso rilasciare, approvare o annullare l'ordine reale di acquisto, allora la PurchaseOrderclasse dovrebbe avere issuePO, approvePOe cancelPOmetodi.

Questo non vale per i principi secolari di "massimizzare la coesione" e "minimizzare l'accoppiamento" che capisco come pietre miliari di OO?

Inoltre, ciò non aiuta la manutenibilità della classe?


17
Non c'è motivo di codificare il nome tipografico nel nome del metodo. Vale a dire se si dispone PurchaseOrder, si può solo chiamare i vostri metodi issue, approvee cancel. Il POsuffisso aggiunge solo un valore negativo.
dash-tom-bang,

2
Finché stai lontano dai metodi del buco nero , dovresti essere OK. :)
Marco C

1
Con la mia attuale risoluzione dello schermo, direi 145 caratteri di grandi dimensioni.
DavRob60,

Grazie a tutti per i vostri commenti, mi hanno davvero aiutato con questo problema. Se questo sito l'avesse permesso, avrei scelto anche la risposta di TMN e Lazzaro, ma ne consente solo una, quindi è andata a Craig. I migliori saluti.
Miguel Veloso,

Risposte:


27

Una classe dovrebbe usare il principio di responsabilità singola. Le classi più grandi che ho visto fare per molte cose, motivo per cui sono troppo grandi. Guarda ogni metodo e codice decidere se dovrebbe essere in questa classe o separato, il codice duplicato è un suggerimento. Potresti avere un metodo issuePO ma contiene 10 righe di codice di accesso ai dati, ad esempio? Quel codice probabilmente non dovrebbe essere lì.


1
Craig, non ero a conoscenza del principio della responsabilità singola e ne ho appena letto. La prima cosa che mi è venuta in mente è stata, qual è lo scopo di una responsabilità? per un PO direi che sta gestendo il suo stato, e questo è il motivo dei metodi issuePO, approvePO e cancelPO, ma include anche l'interazione con la classe PurchaseOrderItem che gestisce lo stato dell'elemento PO. Quindi non sono chiaro se questo approccio sia coerente con l'SRP. cosa vorresti dire?
Miguel Veloso,

1
+1 per una risposta molto simpatica e concisa. Realisticamente, non può esserci una misura definitiva per quanto dovrebbe essere grande una classe. L'applicazione dell'SRP garantisce che la classe sia grande solo quanto deve essere.
S.Robins,

1
@MiguelVeloso - L'approvazione di un ordine di acquisto ha probabilmente una logica e regole diverse associate ad esso rispetto all'emissione di un ordine di acquisto. In tal caso, ha senso separare le responsabilità in un POApprover e un POIssuer. L'idea è che se le regole cambiano per una, perché dovresti fare casino con la classe che contiene l'altra.
Matthew Flynn,

12

A mio avviso, la dimensione della classe non ha importanza finché le variabili, le funzioni e i metodi sono rilevanti per la classe in questione.


1
D'altra parte, le grandi dimensioni indicano che probabilmente questi metodi non sono rilevanti per la classe in questione.
Billy ONeal,

5
@Billy: Direi che le grandi classi hanno un odore di codice, ma non sono automaticamente cattive.
David Thornley,

@ David: Ecco perché c'è la parola "probabilmente" lì dentro - è importante :)
Billy ONeal

Dovrei essere in disaccordo ... Lavoro su un progetto che ha una convenzione di denominazione abbastanza coerente e le cose sono ben strutturate ... ma ho ancora una classe di 50k righe di codice. È troppo grande :)
Jay,

2
Questa risposta mostra esattamente come va la "strada per l'inferno" - se uno non limita le responsabilità di una classe, ci saranno molte variabili, funzioni e metodi che diventano rilevanti per la classe - e questo porta facilmente a averne troppe .
Doc Brown,

7

Il problema più grande con le classi di grandi dimensioni è quando si tratta di testare quelle classi o la loro interazione con altre classi. Le classi di grandi dimensioni di per sé non sono un problema, ma come tutti gli odori, indicano un potenziale problema. In generale, quando la classe è grande, ciò indica che la classe sta facendo più di una singola classe.

Per l'analogia della tua auto, se la classe carè troppo grande, è probabilmente un'indicazione che deve essere suddivisa in classe engine, classe se doorclasse windshields, ecc.


8
E non dimentichiamo che un'auto dovrebbe implementare l'interfaccia del veicolo. :-)
Chris,

7

Non penso che ci sia una definizione specifica di una classe troppo grande. Tuttavia, vorrei utilizzare le seguenti linee guida per determinare se devo refactificare la mia classe o ridefinire l'ambito della classe:

  1. Ci sono molte variabili indipendenti l'una dall'altra? (La mia tolleranza personale è di circa 10 ~ 20 nella maggior parte dei casi)
  2. Esistono molti metodi progettati per custodie angolari? (La mia tolleranza personale è ... beh, dipende molto dal mio umore immagino)

Per quanto riguarda 1, immagina di definire le dimensioni del parabrezza, le dimensioni delle ruote, il modello di lampadina della luce di coda e altri 100 dettagli nella tua classe car. Sebbene siano tutti "rilevanti" per l'auto, è molto difficile rintracciare il comportamento di tale classe car. E quando un giorno il tuo collega accidentalmente (o, in alcuni casi, intenzionalmente) cambia le dimensioni del parabrezza in base al modello di lampadina della luce posteriore, ti ci vuole un'eternità per scoprire perché il parabrezza non si adatta più alla tua auto.

Per quanto riguarda il 2, immagina di provare a definire tutti i comportamenti che un'auto può avere nella classe car, ma car::open_roof()sarà disponibile solo per le decappottabili e car::open_rear_door()non si applica a quelle auto a 2 porte. Sebbene le cabriolet e le auto a 2 porte siano "auto" per definizione, implementarle nella classe carcomplica la classe. La classe diventa più difficile da usare e più difficile da mantenere.

Oltre a queste 2 linee guida, suggerirei una chiara definizione dell'ambito della classe. Dopo aver definito l'ambito, implementare la classe rigorosamente in base alla definizione. Il più delle volte, le persone ottengono una classe "troppo ampia" a causa della scarsa definizione dell'ambito o delle caratteristiche arbitrarie aggiunte alla classe


7

Di 'quello che ti serve in 300 righe

Nota: questa regola empirica presuppone che tu stia seguendo l'approccio "una classe per file" che è di per sé una buona idea di manutenibilità

Non è una regola assoluta, ma se vedo oltre 200 righe di codice in una classe sono sospettoso. 300 linee e segnalatori acustici si accendono. Quando apro il codice di qualcun altro e trovo 2000 righe, so che è tempo di refactoring senza nemmeno leggere la prima pagina.

Principalmente questo si riduce alla manutenibilità. Se l'oggetto è così complesso da non poter esprimere il suo comportamento in 300 righe, sarà molto difficile da capire per qualcun altro.

Sto scoprendo che sto piegando questa regola nel Modello -> ViewModel -> Visualizza il mondo in cui sto creando un "oggetto comportamento" per una pagina dell'interfaccia utente perché spesso ci vogliono più di 300 righe per descrivere la serie completa di comportamenti su una pagina. Tuttavia, sono ancora nuovo di questo modello di programmazione e trovo buoni modi di refactoring / riutilizzo dei comportamenti tra le pagine / i modelli di visualizzazione che sta gradualmente riducendo le dimensioni dei miei ViewModels.


La mia linea guida personale è anche di circa 300 linee. +1
Marcie,

Penso che l'OP stia cercando un'euristica, e questa è una buona idea.
neontapir,

Grazie, è utile utilizzare numeri precisi come linea guida.
Will Sheppard,

6

Andiamo indietro:

Questo non vale per i principi secolari di "massimizzare la coesione" e "minimizzare l'accoppiamento" che capisco come pietre miliari di OO?

In realtà, è una pietra miliare della programmazione, di cui OO è solo un paradigma.

Lascerò che Antoine de Saint-Exupéry risponda alla tua domanda (perché sono pigro;)):

La perfezione si ottiene, non quando non c'è altro da aggiungere, ma quando non c'è più niente da togliere.

Più la classe è semplice, più avrai la certezza che sia giusta, più facile sarà testarla completamente .


3

Sono con l'OP nella classifica Feature Envy. Niente mi irrita più di avere un sacco di classi con un sacco di metodi che funzionano su interni di altre classi. OO suggerisce di modellare i dati e le operazioni su tali dati. Ciò non significa che le operazioni vengano collocate in un posto diverso dai dati! Si sta cercando di arrivare a mettere i dati e questi metodi insieme .

Con i modelli care personcosì prevalenti, sarebbe come inserire il codice per effettuare il collegamento elettrico, o persino girare la partenza, nella personclasse. Spero che questo sarebbe ovviamente sbagliato per qualsiasi programmatore esperto.

Non ho davvero una classe o dimensione del metodo ideale, ma preferisco mantenere i miei metodi e le mie funzioni su uno schermo in modo da poter vedere la loro totalità in un unico posto. (Ho configurato Vim per aprire una finestra di 60 righe, che si trova proprio attorno al numero di righe su una pagina stampata.) Ci sono posti in cui questo non ha molto senso, ma funziona per me. Mi piace seguire le stesse linee guida per le definizioni delle classi e cercare di mantenere i miei file in alcune "pagine" lunghe. Qualunque cosa oltre 300, 400 linee inizia a sembrare ingombrante (soprattutto perché la classe ha iniziato ad assumere troppe responsabilità dirette).


+1 - buone regole empiriche ma non essere autoritario al riguardo. Direi tuttavia che solo perché una classe è suddivisa non significa che le diverse classi dovrebbero toccare i membri privati ​​degli altri.
Billy ONeal,

Non penso che classi diverse dovrebbero mai toccare le parti private dell'altra. Mentre l'accesso "amico" è conveniente per rendere operativo qualcosa, (per me) è un trampolino di lancio per un design migliore. In generale, evito anche gli accessor, poiché a seconda degli interni di un'altra classe c'è un altro odore di codice .
dash-tom-bang,

1

Quando una definizione di classe ha un paio di centinaia di metodi, è troppo grande.


1

Concordo pienamente con l'uso del principio della responsabilità unica per le classi, ma se viene seguito e si traduce in classi di grandi dimensioni, allora così sia. Il vero obiettivo dovrebbe essere che i singoli metodi e proprietà non siano troppo grandi, la complessità ciclomatica dovrebbe essere la guida lì e in seguito che potrebbe comportare molti metodi privati ​​che aumenteranno la dimensione complessiva della classe ma la lasceranno leggibile e verificabile a un livello granulare. Se il codice rimane leggibile, la dimensione è irrilevante.


1

L'emissione di un ordine di acquisto è spesso un processo complicato che coinvolge più di un semplice ordine di acquisto. In questo caso, mantenere la logica di business in una classe separata e semplificare l'ordine di acquisto è probabilmente la cosa giusta da fare. In generale, però, hai ragione: non vuoi classi di "struttura dati". Ad esempio, il metodo "Business Class" di POManager issue()dovrebbe probabilmente chiamare il issue()metodo dell'OP . L'OP può quindi impostare il suo stato su "Emesso" e prendere nota della data di emissione, mentre POManager può quindi inviare una notifica agli account pagabili per far loro sapere che si aspettano una fattura e un'altra notifica all'inventario in modo che sappiano che sta arrivando qualcosa e la data prevista.

Chiunque spinga "regole" per la dimensione del metodo / classe ovviamente non ha trascorso abbastanza tempo nel mondo reale. Come altri hanno già detto, è spesso un indicatore di refactoring, ma a volte (specialmente nel lavoro di tipo "elaborazione dati") è davvero necessario scrivere una classe con un singolo metodo che si estende su oltre 500 righe. Non lasciarti rompere.


Immagino che POManager sarebbe il "controllore" e PurchaseOrder sarebbe il "modello" nel modello MVC, giusto?
Miguel Veloso,

0

Ho trovato diversi autori che danno ricette come "una classe dovrebbe rientrare in una sola pagina"

In termini di dimensioni fisiche di una classe, vedere una grande classe è indice di un odore di codice, ma non significa necessariamente che ci siano sempre odori. (Di solito lo sono però) Come direbbero i pirati nei Pirati dei Caraibi, questo è più ciò che chiamereste "linee guida" che regole reali. Avevo provato a seguire rigorosamente il "non più di cento LOC in una classe" una volta e il risultato era MOLTO inutile ingegneria eccessiva.

Quindi di solito vanno qualcosa come "migliore è il modello, meglio rappresenta l'oggetto nell'applicazione e meglio tutto viene fuori".

Di solito inizio i miei progetti con questo. Cosa fa questa classe nel mondo reale? In che modo la mia classe dovrebbe riflettere questo oggetto come indicato nei suoi requisiti? Aggiungiamo un po 'di stato qui, un campo lì, un metodo per lavorare su quei campi, aggiungere qualche altro e voilà! Abbiamo una classe operaia. Ora riempi le firme con le implementazioni appropriate e siamo pronti per partire, o almeno così pensi. Ora abbiamo una bella classe con tutte le funzionalità di cui abbiamo bisogno. Ma il problema è che non tiene conto del modo in cui il mondo reale funziona. E con ciò intendo che non tiene conto di "CAMBIAMENTO".

Il motivo per cui le classi sono preferibilmente suddivise in parti più piccole è che è difficile cambiare le classi più grandi in seguito senza influenzare la logica esistente o introdurre un nuovo bug o due involontariamente o semplicemente rendere tutto difficile da riutilizzare. È qui che entrano in gioco refactoring, modelli di progettazione, SOLID, ecc. E il risultato finale è di solito una piccola classe che lavora su altre sottoclassi più piccole e più granulari.

Inoltre, nel tuo esempio, l'aggiunta di IssuePO, ApprovePO e Cancel PO nella classe PurchaseOrder sembra illogica. Gli ordini di acquisto non emettono, approvano o annullano se stessi. Se PO dovrebbe avere metodi, i metodi dovrebbero funzionare sul suo stato e non sulla classe nel suo insieme. Sono davvero solo classi di dati / record su cui lavorano altre classi.


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.