C'è qualche ragione per usare le classi "plain old data"?

38

Nel codice legacy, a volte vedo classi che non sono altro che wrapper per i dati. qualcosa come:

class Bottle {
   int height;
   int diameter;
   Cap capType;

   getters/setters, maybe a constructor
}

La mia comprensione di OO è che le classi sono strutture per i dati e i metodi per operare su quei dati. Questo sembra precludere oggetti di questo tipo. Per me non sono altro che structs e tipo di sconfiggere lo scopo di OO. Non penso che sia necessariamente malvagio, anche se potrebbe essere un odore di codice.

C'è un caso in cui tali oggetti sarebbero necessari? Se viene usato spesso, rende sospetto il design?

    
posta Michael K 27.12.2010 - 15:35
fonte

10 risposte

65

Sicuramente non male e non un odore di codice nella mia mente. I contenitori di dati sono un cittadino OO valido. A volte vuoi incapsulare insieme le informazioni correlate. È molto meglio avere un metodo come

public void DoStuffWithBottle(Bottle b)
{
    // do something that doesn't modify Bottle, so the method doesn't belong
    // on that class
}

di

public void DoStuffWithBottle(int bottleHeight, int bottleDiameter, Cap capType)
{
}

L'utilizzo di una classe consente anche di aggiungere un parametro aggiuntivo a Bottle senza modificare ogni chiamante di DoStuffWithBottle. Puoi anche creare una sottoclasse di Bottiglia e aumentare ulteriormente la leggibilità e l'organizzazione del tuo codice, se necessario.

Esistono anche semplici oggetti dati che possono essere restituiti come risultato di una query di database, ad esempio. Credo che il termine per loro in questo caso sia "Data Transfer Object".

In alcune lingue ci sono anche altre considerazioni. Ad esempio, in C # le classi e le strutture si comportano in modo diverso, poiché le strutture sono un tipo di valore e le classi sono tipi di riferimento.

    
risposta data 27.12.2010 - 15:49
fonte
25

Le classi di dati sono valide in alcuni casi. I DTO sono un buon esempio menzionato da Anna Lear. In generale, però, dovresti considerarli il seme di una classe i cui metodi non sono ancora germogliati. E se ne stai incontrando molti nel vecchio codice, trattali come un strong odore di codice. Sono spesso usati da vecchi programmatori C / C ++ che non hanno mai fatto la transizione alla programmazione OO e sono un segno di programmazione procedurale. Affidarsi a getter e setter tutto il tempo (o, peggio ancora, all'accesso diretto di membri non privati) può metterti nei guai prima che tu te ne accorga. Prendi in considerazione un esempio di un metodo esterno che richiede informazioni da Bottle .

Qui Bottle è una classe di dati):

void selectShippingContainer(Bottle bottle) {
    if (bottle.getDiameter() > MAX_DIMENSION || bottle.getHeight() > MAX_DIMENSION ||
            bottle.getCapType() == Cap.FANCY_CAP ) {
        shippingContainer = WOODEN_CRATE;
    } else {
        shippingContainer = CARDBOARD_BOX;
    }
}

Qui abbiamo dato Bottle un comportamento):

void selectShippingContainer(Bottle bottle) {
    if (bottle.isBiggerThan(MAX_DIMENSION) || bottle.isFragile()) {
        shippingContainer = WOODEN_CRATE;
    } else {
        shippingContainer = CARDBOARD_BOX;
    }
}

Il primo metodo viola il principio Tell-Don't-Ask e mantenendo Bottle stupido abbiamo lasciato conoscere implicitamente le bottiglie, come ad esempio ciò che rende un frammento (il Cap ), scivolare nella logica che è al di fuori della classe Bottle . Devi essere sempre pronto a prevenire questo tipo di "fuoriuscita" quando fai abitualmente affidamento sui getter.

Il secondo metodo richiede Bottle solo per ciò che deve fare il suo lavoro e lascia Bottle per decidere se è fragile o più grande di una determinata dimensione. Il risultato è un accoppiamento molto più morbido tra il metodo e l'implementazione di Bottle. Un piacevole effetto collaterale è che il metodo è più pulito e più espressivo.

Raramente userai questi molti campi di un oggetto senza scrivere una logica che dovrebbe risiedere nella classe con quei campi. Scopri qual è la logica e poi spostala nel punto in cui appartiene.

    
risposta data 28.02.2012 - 18:09
fonte
7

Se questo è il tipo di cosa che ti serve, va bene, ma per favore, per favore fallo come

public class Bottle {
    public int height;
    public int diameter;
    public Cap capType;

    public Bottle(int height, int diameter, Cap capType) {
        this.height = height;
        this.diameter = diameter;
        this.capType = capType;
    }
}

invece di qualcosa di simile

public class Bottle {
    private int height;
    private int diameter;
    private Cap capType;

    public Bottle(int height, int diameter, Cap capType) {
        this.height = height;
        this.diameter = diameter;
        this.capType = capType;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getDiameter() {
        return diameter;
    }

    public void setDiameter(int diameter) {
        this.diameter = diameter;
    }

    public Cap getCapType() {
        return capType;
    }

    public void setCapType(Cap capType) {
        this.capType = capType;
    }
}

.

    
risposta data 27.12.2010 - 19:29
fonte
6

Come ha detto @Anna, decisamente non male. Certo, puoi mettere le operazioni (metodi) in classi, ma solo se vuoi . Non avere a.

Permettetemi una piccola lamentela sull'idea che dovete mettere le operazioni in classi, insieme all'idea che le classi siano astrazioni. In pratica, questo incoraggia i programmatori a

  1. Crea più classi di quante ne abbiano bisogno (struttura dati ridondante). Quando una struttura dati contiene più componenti del minimo necessario, non è normalizzata e pertanto contiene stati incoerenti. In altre parole, quando è alterato, deve essere modificato in più di un posto per rimanere coerente. La mancata esecuzione di tutte le modifiche coordinate lo rende incoerente, ed è un bug.

  2. Risolvi il problema 1 inserendo i metodi notification , in modo che se la parte A viene modificata, prova a propagare le modifiche necessarie alle parti B e C. Questo è il motivo principale per cui è raccomandato di avere metodi di accesso get-and-set. Poiché questa pratica è raccomandata, sembra giustificare il problema 1, causando più problemi 1 e più soluzioni 2. Ciò si traduce non solo in bug dovuti all'implementazione incompleta delle notifiche, ma anche a un problema di prestazioni di notifiche fuori controllo. Questi non sono calcoli infiniti, solo molto lunghi.

Questi concetti sono insegnati come cose buone, generalmente da insegnanti che non hanno dovuto lavorare all'interno di app di milioni di mostri pieni di problemi.

Ecco cosa cerco di fare:

  1. Mantieni i dati il più possibile normalizzati, in modo che quando viene apportata una modifica ai dati, venga eseguita con il minor numero possibile di punti di codice, per ridurre al minimo la probabilità di immettere uno stato incoerente.

  2. Quando i dati devono essere non normalizzati e la ridondanza è inevitabile, non utilizzare le notifiche nel tentativo di mantenerle coerenti. Piuttosto, tollerare l'inconsistenza temporanea. Risolvere incoerenza con passaggi periodici attraverso i dati da un processo che fa solo quello. Ciò centralizza la responsabilità del mantenimento della coerenza, evitando allo stesso tempo i problemi di prestazioni e correttezza a cui sono soggette le notifiche. Ne risulta un codice molto più piccolo, privo di errori ed efficiente.

risposta data 27.12.2010 - 17:28
fonte
3

Questo tipo di classi è molto utile quando hai a che fare con applicazioni di dimensioni medio / grandi, per alcuni motivi:

  • è abbastanza facile creare dei test casi e garantire che i dati siano coerente.
  • contiene tutti i tipi di comportamenti coinvolgere tali informazioni, quindi il tempo di tracciamento dei bug è ridotto
  • Usarli dovrebbe mantenere il metodo args leggero.
  • Quando si utilizzano gli ORM, questa classe offre flessibilità e coerenza. Aggiungendo un attributo complesso che viene calcolato sulla base di semplici informazioni già presenti nella classe, si invoca per iscritto un metodo semplice. Questo è molto più agile e produttivo di dover controllare il database e garantire che tutti i database vengano aggiornati con nuove modifiche.

Per riassumere, nella mia esperienza di solito sono più utili che fastidiosi.

    
risposta data 27.12.2010 - 16:12
fonte
3

Con la progettazione del gioco, il sovraccarico di migliaia di chiamate di funzione e gli ascoltatori di eventi a volte possono far sì che valga la pena di avere classi che memorizzano solo dati e hanno altre classi che eseguono il ciclo di tutte le classi di dati per eseguire la logica.

    
risposta data 27.12.2010 - 21:15
fonte
3

Concorda con Anna Lear,

Definitely not evil and not a code smell in my mind. Data containers are a valid OO citizen. Sometimes you want to encapsulate related information together. It's a lot better to have a method like...

A volte le persone dimenticano di leggere le Convenzioni sul Coding Java del 1999 che rendi molto chiaro che questo tipo di programmazione è perfettamente soddisfacente. Infatti se lo eviti, il tuo codice puzzerà! (troppi getter / setter)

Da Java Code Conventions 1999: Un esempio di variabili di istanza pubbliche appropriate è il caso in cui la classe è essenzialmente una struttura di dati, senza alcun comportamento. In altre parole, se avessi usato una struct al posto di una classe (se la struttura fosse supportata da Java), allora è opportuno rendere pubbliche le variabili di istanza della classe. link

Se usati correttamente, i POD (semplici vecchie strutture dati) sono migliori dei POJO proprio come i POJO sono spesso migliori degli EJB.
link

    
risposta data 28.02.2012 - 14:33
fonte
3

Gli edifici hanno il loro posto, anche in Java. Dovresti usarli solo se le seguenti due cose sono vere:

  • Hai solo bisogno di aggregare dati che non hanno alcun comportamento, ad es. passare come parametro
  • Non importa un po 'quale tipo di valori hanno dati aggregati

Se questo è il caso, allora dovresti rendere pubblici i campi e saltare i getter / setter. Getter e setter sono comunque goffi, e Java è stupido per non avere proprietà come un linguaggio utile. Dal momento che il tuo oggetto struct-like non dovrebbe avere comunque alcun metodo, i campi pubblici hanno più senso.

Tuttavia, se uno di questi non si applica, hai a che fare con una vera classe. Ciò significa che tutti i campi dovrebbero essere privati. (Se hai assolutamente bisogno di un campo in un ambito più accessibile, usa un getter / setter.)

Per verificare se la tua presunta struttura ha un comportamento, guarda quando i campi sono usati. Se sembra violare dire, non chiedere , allora devi spostare quel comportamento nella tua classe.

Se alcuni dei tuoi dati non dovessero cambiare, allora devi rendere definitivi tutti quei campi. Potresti considerare di rendere la tua classe immutabile . Se è necessario convalidare i dati, fornire la convalida ai setter e ai costruttori. (Un trucco utile è definire un setter privato e modificare il campo all'interno della classe usando solo quel setter.)

Probabilmente il tuo esempio di Bottiglia fallirà entrambi i test. Potresti avere un codice (forzato) che assomiglia a questo:

public double calculateVolumeAsCylinder(Bottle bottle) {
    return bottle.height * (bottle.diameter / 2.0) * Math.PI);
}

Invece dovrebbe essere

double volume = bottle.calculateVolumeAsCylinder();

Se hai cambiato altezza e diametro, sarebbe la stessa bottiglia? Probabilmente no. Quelli dovrebbero essere definitivi È un valore negativo ok per il diametro? La tua bottiglia deve essere più alta di quanto sia larga? Il Cap può essere nullo? No? Come stai convalidando questo? Supponiamo che il cliente sia o stupido o cattivo. ( È impossibile capire la differenza. ) Devi controllare questi valori.

Ecco come potrebbe apparire la tua nuova classe Bottle:

public class Bottle {

    private final int height, diameter;

    private Cap capType;

    public Bottle(final int height, final int diameter, final Cap capType) {
        if (diameter < 1) throw new IllegalArgumentException("diameter must be positive");
        if (height < diameter) throw new IllegalArgumentException("bottle must be taller than its diameter");

        setCapType(capType);
        this.height = height;
        this.diameter = diameter;
    }

    public double getVolumeAsCylinder() {
        return height * (diameter / 2.0) * Math.PI;
    }

    public void setCapType(final Cap capType) {
        if (capType == null) throw new NullPointerException("capType cannot be null");
        this.capType = capType;
    }

    // potentially more methods...

}
    
risposta data 28.01.2013 - 16:23
fonte
0

IMHO spesso non ci sono abbastanza classi come questa in sistemi strongmente orientati agli oggetti. Devo qualificarlo attentamente.

Naturalmente, se i campi dei dati hanno un ampio campo di visibilità e visibilità, ciò può essere estremamente indesiderato se ci sono centinaia o migliaia di posti nel vostro database che manomettono tali dati. Questo sta chiedendo problemi e difficoltà nel mantenere invarianti. Tuttavia, allo stesso tempo, ciò non significa che ogni singola classe nell'intero codebase trae vantaggio dall'informazione nascosta.

Ma ci sono molti casi in cui tali campi di dati avranno uno scopo molto ristretto. Un esempio molto semplice è una classe Node privata di una struttura dati. Spesso può semplificare notevolmente il codice riducendo il numero di interazioni tra oggetti in corso se detto Node potrebbe semplicemente consistere in dati non elaborati. Questo serve da meccanismo di disaccoppiamento poiché la versione alternativa potrebbe richiedere un accoppiamento bidirezionale da, diciamo, Tree->Node e Node->Tree invece che semplicemente Tree->Node Data .

Un esempio più complesso sarebbe un sistema di componenti entità usato spesso nei motori di gioco. In questi casi, i componenti sono spesso solo dati e classi grezzi come quello che hai mostrato. Tuttavia, la loro visibilità / visibilità tende ad essere limitata poiché in genere ci sono solo uno o due sistemi che potrebbero accedere a quel particolare tipo di componente. Di conseguenza, si tende ancora a trovare abbastanza facile mantenere invarianti in quei sistemi e, inoltre, tali sistemi hanno pochissime interazioni di object->object , rendendo molto facile comprendere cosa sta accadendo a livello di vista.

In questi casi puoi ritrovarti con qualcosa di più simile alle interazioni (questo diagramma indica le interazioni, non l'accoppiamento, poiché un diagramma di accoppiamento potrebbe includere interfacce astratte per la seconda immagine sotto):

...alcontrariodiquesto:

... e il precedente tipo di sistema tende ad essere molto più facile da mantenere e ragionare in termini di correttezza nonostante il fatto che le dipendenze siano effettivamente fluide verso i dati. Si ottiene molto meno l'accoppiamento principalmente perché molte cose possono essere trasformate in dati grezzi invece di oggetti che interagiscono l'uno con l'altro formando un grafo molto complesso di interazioni.

    
risposta data 16.12.2017 - 10:31
fonte
-1

Esistono persino molte strane creature nel mondo OOP. Una volta ho creato una classe, che è astratta e non contiene nulla, solo per essere un genitore comune di altre due classi ... è la classe Port:

SceneMember 
Message extends SceneMember
Port extends SceneMember
InputPort extends Port
OutputPort extends Port

Quindi SceneMember è la classe base, fa tutto il lavoro, che ha richiesto di apparire sulla scena: aggiungi, cancella, ottieni l'ID, ecc. Il messaggio è la connessione tra le porte, ha una sua vita. InputPort e OutputPort contengono le funzioni specifiche di i / o proprie.

La porta è vuota. È usata per raggruppare InputPort e OutputPort insieme, per esempio, in una portlist. È vuoto perché SceneMember contiene tutto ciò che serve per agire come membro e InputPort e OutputPort contengono le attività specificate per la porta. InputPort e OutputPort sono così diversi, che non hanno funzioni comuni (tranne SceneMember). Quindi, Port è vuoto. Ma è legale.

Forse è un antipattern , ma mi piace.

(È come la parola "compagno", che è usata sia per "moglie" che per "marito". Non usi mai la parola "compagno" per una persona concreta, perché conosci il sesso di lui / lei, e non importa se lui / lei è sposato o no, quindi usi "qualcuno", ma esiste ancora, e viene usato in rare situazioni astratte, ad esempio un testo di legge.)

    
risposta data 27.12.2010 - 16:06
fonte

Leggi altre domande sui tag