it-swarm.it

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

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 quei dati. Questo sembra precludere oggetti di questo tipo. Per me non sono nulla più di structs e il tipo di sconfiggere lo 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?

44
Michael K

Sicuramente non male e non un odore di codice nella mia mente. I contenitori di dati sono validi OO cittadino. A volte si desidera incapsulare informazioni correlate insieme. È molto meglio disporre di 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'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.

68
Adam Lear

Le classi di dati sono valide in alcuni casi. I DTO sono un buon esempio menzionato da Anna Lear. In generale, però, 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 a OO e sono un segno di programmazione procedurale. Affidarsi sempre a getter e setter (o peggio ancora, su l'accesso diretto di membri non privati) può metterti nei guai prima di conoscerlo. Considera 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 a Bottle alcuni comportamenti):

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 permesso alla conoscenza implicita delle bottiglie, come ciò che rende un frammento (il Cap), scivolare dentro logica esterna alla classe Bottle. Devi stare attento a prevenire questo tipo di "fuoriuscita" quando ti affidi abitualmente ai vincitori.

Il secondo metodo chiede a Bottle solo ciò di cui ha bisogno per fare il suo lavoro, e lascia Bottle a decidere 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.

25
Mike E

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.

7
compman

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

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 i metodi 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 get-and -set metodi di accesso. 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.

6
Mike Dunlavey

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
AttackingHobo

Concordo con Anna Lear,

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

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

Da Java Code Conventions 1999: Un esempio di variabili di istanza pubblica appropriate è 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 supporta 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
Richard Faber

Questo tipo di classi è abbastanza 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
guiman

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

  • Devi solo aggregare i dati che non hanno alcun comportamento, ad es. 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. Dato che 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 dillo, 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 prendere in considerazione l'idea di rendere la tua classe immutabile . 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. ) 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...

}
3
Eva

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

Ovviamente se i campi di dati hanno 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 classe privata Node di una struttura di 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 funge da meccanismo di disaccoppiamento poiché la versione alternativa potrebbe richiedere un accoppiamento bidirezionale da, diciamo, Tree->Node e Node->Tree anziché 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 a 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 pochissimi object->object interazioni, rendendo molto semplice comprendere 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):

enter image description here

... al contrario di questo:

enter image description here

... 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.

0
user204677