C'è qualche motivo per usare le classi "semplici vecchi dati"?


43

Nel codice legacy di tanto in tanto vedo classi che non sono altro che wrapper per i dati. qualcosa di simile a:

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 tali dati. Questo sembra precludere oggetti di questo tipo. Per me non sono altro che una structssorta di sconfitta dello scopo di OO. Non penso che sia necessariamente malvagio, sebbene possa essere un odore di codice.

Esiste un caso in cui tali oggetti sarebbero necessari? Se usato spesso, sospetta il design?


1
Questo non risponde alla tua domanda ma sembra comunque rilevante: stackoverflow.com/questions/36701/struct-like-objects-in-java
Adam Lear

2
Penso che il termine che stai cercando sia POD (Plain Old Data).
Gaurav,

9
Questo è un tipico esempio di programmazione strutturata. Non necessariamente male, ma non orientato agli oggetti.
Konrad Rudolph,

1
questo non dovrebbe essere in overflow dello stack?
Muad'Dib,

7
@ Muad'Dib: tecnicamente, si tratta di programmatori. Al compilatore non importa se si utilizzano strutture di dati vecchie semplici. Probabilmente la tua CPU si diverte (nel modo "Adoro l'odore dei dati freschi dalla cache"). Sono le persone che si rompono "questo rende la mia metodologia meno pura?" domande.
Shog9

Risposte:


67

Sicuramente non malefico e non un odore di codice nella mia mente. I contenitori di dati sono cittadini OO validi. A volte vuoi incapsulare le informazioni correlate insieme. È molto meglio avere un metodo simile

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'uso di una classe consente inoltre di aggiungere un parametro aggiuntivo a Bottle senza modificare tutti i chiamanti di DoStuffWithBottle. E puoi sottoclassare Bottle e aumentare ulteriormente la leggibilità e l'organizzazione del tuo codice, se necessario.

Esistono anche oggetti dati semplici che possono essere restituiti come risultato di una query del database, ad esempio. Credo che il termine per loro in quel caso sia "Oggetto di trasferimento dati".

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


25
Nah. DoStuffWithdovrebbe essere un metodo della classe OOP (e dovrebbe probabilmente essere immutabili, troppo). Quello che hai scritto sopra non è un buon modello in un linguaggio OO (a meno che tu non stia interfacciati con un'API legacy). Si tratta , tuttavia, un progetto valido in un ambiente non-OO. Bottle
Konrad Rudolph,

11
@Javier: Quindi anche Java non è la risposta migliore. L'enfasi di Java su OO è schiacciante, il linguaggio praticamente non è buono in nessun altro.
Konrad Rudolph,

11
@JohnL: ovviamente stavo semplificando. Ma in generale, gli oggetti in OO incapsulano lo stato , non i dati. Questa è una distinzione eccellente ma importante. Il punto di OOP è proprio quello di non avere molti dati disponibili. Sta inviando messaggi tra stati che creano un nuovo stato. Non riesco a vedere come è possibile inviare messaggi ao da un oggetto privo di metodo. (E questa è la definizione originale di OOP, quindi non la considero imperfetta.)
Konrad Rudolph,

13
@Konrad Rudolph: Questo è il motivo per cui ho esplicitamente fatto il commento all'interno del metodo. Sono d'accordo che i metodi che influenzano le istanze di Bottle dovrebbero andare in quella classe. Ma se un altro oggetto deve modificare il suo stato in base alle informazioni contenute in Bottle, penso che il mio design sarebbe abbastanza valido.
Adam Lear

10
@Konrad, non sono d'accordo sul fatto che doStuffWithBottle dovrebbe andare nella classe delle bottiglie. Perché una bottiglia dovrebbe sapere come fare cose con se stessa? doStuffWithBottle indica che qualcos'altro farà qualcosa con una bottiglia. Se la bottiglia avesse questo, sarebbe un accoppiamento stretto. Tuttavia, se la classe Bottle avesse un metodo isFull (), sarebbe del tutto appropriato.
Nemi,

25

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

Ecco Bottleuna 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 Bottleun 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, mantenendoci Bottlestupidi, abbiamo lasciato che la conoscenza implicita delle bottiglie, come ciò che rende un frammento (il Cap), scivolasse nella logica che è fuori dalla Bottleclasse. Devi stare attento a prevenire questo tipo di "fuoriuscita" quando fai affidamento abitualmente sui vincitori.

Il secondo metodo chiede Bottlesolo ciò di cui ha bisogno per fare il suo lavoro e lascia Bottledecidere se è fragile o più grande di una determinata dimensione. Il risultato è un accoppiamento molto più libero tra il metodo e l'implementazione di Bottle. Un piacevole effetto collaterale è che il metodo è più pulito ed espressivo.

Raramente utilizzerai questi molti campi di un oggetto senza scrivere alcune logiche che dovrebbero risiedere nella classe con quei campi. Scopri cos'è quella logica e poi spostala dove appartiene.


1
Non riesco a credere che questa risposta non abbia voti (beh, ora ne hai uno). Questo potrebbe essere un semplice esempio, ma quando OO viene abusato abbastanza ottieni classi di servizio che si trasformano in incubi contenenti tonnellate di funzionalità che avrebbero dovuto essere incapsulate in classi.
Alb

"dai vecchi programmatori C / C ++ che non hanno mai effettuato la transizione (sic) a OO"? I programmatori C ++ di solito sono piuttosto OO, in quanto è un linguaggio OO, cioè l'intero punto di usare C ++ invece di C.
nappyfalcon,

7

Se questo è il genere di cose di cui hai bisogno, va bene, ma per favore, per favore, per favore, fallo come piace

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 del genere

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;
    }
}

Per favore.


14
L'unico problema è che non esiste alcuna convalida. Qualsiasi logica su cosa sia una bottiglia valida dovrebbe essere nella classe Bottle. Tuttavia, usando l'implementazione proposta, posso avere una bottiglia con un'altezza e un diametro negativi - non c'è modo di far rispettare le regole di business sull'oggetto senza convalidare ogni volta che l'oggetto viene utilizzato. Usando il secondo metodo, posso assicurarmi che se ho un oggetto Bottle, lo era, è e sarà sempre un oggetto Bottle valido secondo il mio contratto.
Thomas Owens

6
Questa è un'area in cui .NET ha un leggero vantaggio rispetto alle proprietà, poiché è possibile aggiungere un programma di accesso alle proprietà con logica di convalida, con la stessa sintassi di se si accedesse a un campo. È inoltre possibile definire una proprietà in cui le classi possono ottenere il valore della proprietà, ma non impostarlo.
Giovanni L

3
@ user9521 Se sei sicuro che il tuo codice non causerà un errore irreversibile con valori "cattivi", scegli il tuo metodo. Tuttavia, se è necessaria un'ulteriore convalida, o la possibilità di utilizzare il caricamento lento, o altri controlli durante la lettura o la scrittura dei dati, utilizzare getter e setter espliciti. Personalmente tendo a mantenere private le mie variabili e ad utilizzare coerenti e getter e setter. In questo modo tutte le mie variabili vengono trattate allo stesso modo, indipendentemente dalla validazione e / o da altre tecniche "avanzate".
Jonathan,

2
Il vantaggio di usare il costruttore è che rende molto più facile rendere immutabile la classe. Questo è essenziale se si desidera scrivere codice multi-thread.
Fortyrunner,

7
Renderei i campi definitivi quando possibile. IMHO Avrei preferito che i campi fossero finali per impostazione predefinita e che avessero una parola chiave per i campi mutabili. es. var
Peter Lawrey,

6

Come ha detto @Anna, sicuramente non male. Sicuro che puoi mettere le operazioni (metodi) in classi, ma solo se lo desideri . Non hai a.

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

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

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

Questi concetti vengono insegnati come cose buone, in genere da insegnanti che non hanno dovuto lavorare all'interno di app mostruose di milioni di righe piene di questi problemi.

Ecco cosa provo a fare:

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

  2. Quando i dati devono essere non normalizzati e la ridondanza è inevitabile, non utilizzare le notifiche nel tentativo di mantenerli coerenti. Piuttosto, tollerare un'incoerenza temporanea. Risolve l'incoerenza con gli sweep periodici attraverso i dati mediante un processo che fa solo quello. Ciò centralizza la responsabilità di mantenere la coerenza, evitando al contempo i problemi di prestazioni e correttezza a cui sono soggette le notifiche. Ciò si traduce in un codice molto più piccolo, privo di errori ed efficiente.


3

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

  • è abbastanza facile creare alcuni casi di test e assicurarsi che i dati siano coerenti.
  • contiene tutti i tipi di comportamenti che coinvolgono tali informazioni, quindi il tempo di tracciamento dei bug dei dati è ridotto
  • Il loro utilizzo 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 informazioni semplici già presenti nella classe, si riparte nella scrittura di un metodo semplice. Questo è molto più agile e produttivo che dover controllare il database e assicurarsi che tutti i database siano aggiornati con nuove modifiche.

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


3

Con il design del gioco, l'overhead di migliaia di chiamate di funzione e listener di eventi a volte può valere la pena avere classi che memorizzano solo dati e altre classi che eseguono il ciclo attraverso tutti i dati solo classi per eseguire la logica.


3

Concordo con Anna Lear,

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

A volte le persone dimenticano di leggere le Convenzioni sulla codifica Java del 1999 che rendono molto chiaro che questo tipo di programmazione va benissimo. Infatti se lo eviti, allora il tuo codice avrà un odore! (troppi getter / setter)

Da Java Code Conventions 1999: Un esempio di appropriate variabili di istanza pubblica è il caso in cui la classe è essenzialmente una struttura di dati, senza alcun comportamento. In altre parole, se avessi usato una struttura anziché una classe (se Java supportava la struttura), è opportuno rendere pubbliche le variabili di istanza della classe. http://www.oracle.com/technetwork/java/javase/documentation/codeconventions-137265.html#177

Se usati correttamente, i POD (semplici vecchie strutture dati) sono migliori dei POJO proprio come i POJO sono spesso migliori degli EJB.
http://en.wikipedia.org/wiki/Plain_Old_Data_Structures


3

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

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

In questo caso, è necessario rendere pubblici i campi e saltare i getter / setter. Getter e setter sono comunque goffi e Java è sciocco per non avere proprietà come un linguaggio utile. Poiché il tuo oggetto simile a una struttura non dovrebbe avere 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 struttura presunta ha un comportamento, osserva quando vengono utilizzati i campi. Se sembra violare la parola, non chiedere , allora devi spostare quel comportamento nella tua classe.

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

Il tuo esempio Bottle probabilmente fallirebbe entrambi i test. Potresti avere un codice (inventato) 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 cambiassi altezza e diametro, sarebbe la stessa bottiglia? Probabilmente no. Quelli dovrebbero essere definitivi. Un valore negativo va bene per il diametro? La tua bottiglia deve essere più alta di quanto sia larga? Il Cap può essere nullo? No? Come lo convalidi? Supponiamo che il cliente sia stupido o malvagio. ( È impossibile dire la differenza. ) È necessario 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...

}

0

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

Naturalmente se i campi di dati hanno un ampio ambito e visibilità, ciò può essere estremamente indesiderabile se ci sono centinaia o migliaia di posti nella tua base di codice che manomettono tali dati. Ciò richiede problemi e difficoltà nel mantenere invarianti. Allo stesso tempo, ciò non significa che ogni singola classe nell'intera base di codice tragga vantaggio dal nascondere le informazioni.

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

Un esempio più complesso sarebbero i sistemi a componenti di entità usati spesso nei motori di gioco. In questi casi, i componenti sono spesso solo dati e classi non elaborati come quello che hai mostrato. Tuttavia, la loro portata / visibilità tende ad essere limitata poiché in genere esistono solo uno o due sistemi che potrebbero accedere a quel particolare tipo di componente. Di conseguenza tendi ancora a trovare abbastanza facile mantenere invarianti in quei sistemi e, inoltre, tali sistemi hanno pochissime object->objectinterazioni, rendendo molto facile capire cosa sta succedendo a livello dell'occhio dell'uccello.

In questi casi puoi finire con qualcosa di più simile a questo per quanto riguarda le interazioni (questo diagramma indica le interazioni, non l'accoppiamento, poiché un diagramma di accoppiamento potrebbe includere interfacce astratte per la seconda immagine in basso):

inserisci qui la descrizione dell'immagine

... al contrario di questo:

inserisci qui la descrizione dell'immagine

... e il primo tipo di sistema tende ad essere molto più facile da mantenere e ragionare in termini di correttezza nonostante il fatto che le dipendenze stiano effettivamente fluendo verso i dati. Si ottiene molto meno accoppiamento principalmente perché molte cose possono essere trasformate in dati grezzi invece che oggetti che interagiscono tra loro formando un grafico molto complesso di interazioni.


-1

Esistono anche 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 di base, fa tutto il lavoro necessario per apparire sulla scena: aggiungere, eliminare, ottenere l'ID di impostazione, ecc. Il messaggio è la connessione tra le porte, ha una sua vita. InputPort e OutputPort contengono le funzioni specifiche di I / o.

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

Forse è un antipasto , ma mi piace.

(È come la parola "compagno", che viene 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 è sposato o no, quindi usi "qualcuno" invece, ma esiste ancora, ed è usato in rare, astratte situazioni, ad esempio un testo di legge.)


1
Perché le tue porte devono estendere SceneMember? perché non creare le classi di porte per operare su SceneMembers?
Michael K,

1
Quindi perché non utilizzare il modello di interfaccia marcatore standard? Fondamentalmente identico alla tua classe base astratta vuota, ma è un linguaggio molto più comune.
TMN,

1
@Michael: Solo per ragioni teoriche, ho riservato Port per un uso futuro, ma questo futuro non è ancora arrivato e forse non lo farà mai. Non mi ero reso conto che non avevano assolutamente nulla in comune, a differenza dei loro nomi supponiamo.
Pagherò

1
@TMN: i tipi SceneMember (derivativi) hanno il metodo getMemberType (), ci sono diversi casi in cui Scene viene scansionato per sottogruppi di oggetti SceneMember.
ern0

4
Questo non risponde alla domanda.
Eva,
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.