Ereditarietà vs proprietà aggiuntiva con valore null


12

Per le classi con campi opzionali, è meglio usare l'ereditarietà o una proprietà nullable? Considera questo esempio:

class Book {
    private String name;
}
class BookWithColor extends Book {
    private String color;
}

o

class Book {
    private String name;
    private String color; 
    //when this is null then it is "Book" otherwise "BookWithColor"
}

o

class Book {
    private String name;
    private Optional<String> color;
    //when isPresent() is false it is "Book" otherwise "BookWithColor"
}

Il codice in base a queste 3 opzioni sarebbe:

if (book instanceof BookWithColor) { ((BookWithColor)book).getColor(); }

o

if (book.getColor() != null) { book.getColor(); }

o

if (book.getColor().isPresent()) { book.getColor(); }

Il primo approccio mi sembra più naturale, ma forse è meno leggibile a causa della necessità di fare il casting. C'è un altro modo per raggiungere questo comportamento?


1
Temo che la soluzione sia dovuta alla tua definizione di regole aziendali. Voglio dire, se tutti i tuoi libri possono avere un colore ma il colore è facoltativo, l'eredità è eccessiva e in realtà non necessaria, perché vuoi che tutti i tuoi libri sappiano che esiste la proprietà del colore. D'altra parte, se puoi avere sia libri che non sanno nulla del colore che mai e libri che hanno un colore, creare classi specializzate non è male. Anche allora, probabilmente preferirei la composizione oltre l'eredità.
Andy,

Per renderlo un po 'più specifico - ci sono 2 tipi di libri - uno con il colore e uno senza. Quindi non tutti i libri possono avere un colore.
Bojan VukasovicTest

1
Se esiste davvero un caso in cui il libro non ha affatto colore, l'eredità nel tuo caso è il modo più semplice di estendere le proprietà di un libro colorato. In questo caso non sembra che tu possa spezzare nulla ereditando la classe del libro di base e aggiungendo una proprietà color ad essa. Puoi leggere il principio di sostituzione di Mosca per saperne di più sui casi in cui è consentita l'estensione di una classe e quando non lo è.
Andy,

4
Riesci a identificare un comportamento che desideri dai libri con la colorazione? Riesci a trovare un comportamento comune tra i libri con e senza colorazione, in cui i libri con colorazione dovrebbero comportarsi in modo leggermente diverso? In tal caso, questo è un buon caso per OOP con tipi diversi e l'idea sarebbe quella di spostare i comportamenti nelle classi invece di interrogare la presenza e il valore delle proprietà esternamente.
Erik Eidt,

1
Se i possibili colori dei libri sono noti in anticipo, che ne dici di un campo Enum per BookColor con un'opzione per BookColor.NoColor?
Graham,

Risposte:


7

Dipende dalle circostanze. L'esempio specifico non è realistico poiché non si avrebbe una sottoclasse chiamata BookWithColorin un programma reale.

Ma in generale, una proprietà che ha senso solo per determinate sottoclassi dovrebbe esistere solo in quelle sottoclassi.

Ad esempio, se Bookha PhysicalBooke DigialBookcome discendenti, PhysicalBookpotrebbe avere una weightproprietà e DigitalBookuna sizeInKbproprietà. Ma DigitalBooknon avrà weighte viceversa. Booknon avrà nessuna proprietà, poiché una classe dovrebbe avere solo proprietà condivise da tutti i discendenti.

Un esempio migliore è guardare le lezioni da software reale. Il JSlidercomponente ha il campo majorTickSpacing. Poiché solo un cursore ha "segni di spunta", questo campo ha senso solo per i JSlidersuoi discendenti. Sarebbe molto confuso se altri componenti fratelli JButtonavessero un majorTickSpacingcampo.


oppure potresti sottrarre il peso per poter riflettere entrambi, ma probabilmente è troppo.
Walfrat,

3
@Walfrat: Sarebbe una cattiva idea avere lo stesso campo che rappresenti cose completamente diverse in sottoclassi diverse.
Jacques B,

E se un libro avesse edizioni sia fisiche che digitali? Dovresti creare una classe diversa per ogni permutazione di avere o non avere ogni campo opzionale. Penso che la soluzione migliore sarebbe solo quella di consentire che alcuni campi vengano omessi o meno a seconda del libro. Hai citato una situazione completamente diversa in cui le due classi si comporterebbero diversamente funzionalmente. Non è quello che implica questa domanda. Devono solo modellare una struttura di dati.
4castle,

@ 4castle: Avresti bisogno di classi separate per edizione, dato che ogni edizione potrebbe anche avere un prezzo diverso. Non puoi risolverlo con campi opzionali. Ma dall'esempio non sappiamo se l'operazione vuole un comportamento diverso per i libri con colori o "libri incolori", quindi è impossibile sapere qual è la modellazione corretta in questo caso.
Jacques B,

3
d'accordo con @JacquesB. non puoi fornire una soluzione universalmente corretta per i "libri di modellistica", perché dipende dal problema che stai cercando di risolvere e dal tuo business. Penso che sia abbastanza sicuro dire che definire le gerarchie di classi in cui si viola Viškov è categoricamente sbagliato (tm) sebbene (sì sì, il tuo caso è probabilmente molto speciale e lo garantisce (anche se ne dubito))
Sara

5

Un punto importante che non sembra essere stato menzionato: nella maggior parte delle lingue, le istanze di una classe non possono cambiare di quale classe sono istanze. Quindi, se avessi un libro senza un colore e volessi aggiungere un colore, dovrai creare un nuovo oggetto se usi classi diverse. E quindi probabilmente dovresti sostituire tutti i riferimenti al vecchio oggetto con riferimenti al nuovo oggetto.

Se "libri senza colore" e "libri con colore" sono istanze della stessa classe, quindi aggiungere il colore o rimuovere il colore sarà molto meno un problema. (Se la tua interfaccia utente mostra un elenco di "libri con colore" e "libri senza colore", tale interfaccia utente dovrebbe cambiare ovviamente, ma mi aspetto che sia qualcosa che devi comunque gestire, simile a un "elenco di rosso libri "e" elenco di libri verdi ").


Questo è davvero un punto importante! Definisce se si dovrebbe considerare una raccolta di proprietà opzionali invece di attributi di classe.
Chromanoid,

1

Pensa a un JavaBean (come il tuo Book) come a un record in un database. Le colonne opzionali sono nullquando non hanno valore, ma è perfettamente legale. Pertanto, la tua seconda opzione:

class Book {
    private String name;
    private String color; // null when not applicable
}

È il più ragionevole. 1

Fai attenzione però a come usi la Optionalclasse. Ad esempio, non lo è Serializable, che di solito è una caratteristica di un JavaBean. Ecco alcuni consigli di Stephen Colebourne :

  1. Non dichiarare alcuna variabile di istanza di tipo Optional.
  2. Utilizzare nullper indicare dati facoltativi nell'ambito privato di una classe.
  3. Utilizzare Optionalper getter che accedono al campo opzionale.
  4. Non utilizzare Optionalin setter o costruttori.
  5. Utilizzare Optionalcome tipo di ritorno per qualsiasi altro metodo di logica aziendale che abbia un risultato facoltativo.

Pertanto, all'interno della tua classe dovresti usare nullper indicare che il campo non è presente, ma quando color lascia il Book(come tipo di ritorno) dovrebbe essere racchiuso da un Optional.

return Optional.ofNullable(color); // inside the class

book.getColor().orElse("No color"); // outside the class

Ciò fornisce un design chiaro e un codice più leggibile.


1 Se hai intenzione BookWithColordi incapsulare un'intera "classe" di libri che hanno capacità specializzate rispetto ad altri libri, allora avrebbe senso usare l'eredità.


1
Chi ha detto qualcosa su un database? Se tutti i pattern OO dovessero adattarsi a un paradigma di database relazionale, dovremmo cambiare molti pattern OO.
alexanderbird,

Direi che aggiungere l'aggiunta di proprietà inutilizzate "nel caso in cui" sia l'opzione meno chiara. Come altri hanno detto, dipende dal caso d'uso, ma la situazione più desiderabile sarebbe quella di non portare in giro proprietà in cui un'applicazione esterna deve sapere se una proprietà esistente viene effettivamente utilizzata.
Volker Kueffel,

@Volker Accettiamo di non essere d'accordo, perché posso citare diverse persone che hanno giocato una mano aggiungendo la Optionalclasse a Java per questo preciso scopo. Potrebbe non essere OO, ma è sicuramente un'opinione valida.
4castle,

@Alexanderbird Stavo affrontando specificamente un JavaBean, che dovrebbe essere serializzabile. Se fossi fuori tema sollevando JavaBeans, quello era il mio errore.
4castle,

@Alexanderbird Non mi aspetto che tutti i pattern corrispondano a un database relazionale, ma mi aspetto un Java Bean a
4castle

0

Dovresti usare un'interfaccia (non ereditaria) o un qualche tipo di meccanico di proprietà (forse anche qualcosa che funziona come il framework di descrizione delle risorse).

Quindi neanche

interface Colored {
  Color getColor();
}
class ColoredBook extends Book implements Colored {
  ...
}

o

class PropertiesHolder {
  <T> extends Property<?>> Optional<T> getProperty( Class<T> propertyClass ) { ... }
  <V, T extends Property<V>> Optional<V> getPropertyValue( Class<T> propertyClass ) { ... }
}
interface Property<T> {
  String getName();
  T getValue();
}
class ColorProperty implements Property<Color> {
  public Color getValue() { ... }
  public String getName() { return "color"; }
}
class Book extends PropertiesHolder {
}

Chiarimento (modifica):

Semplicemente aggiungendo campi opzionali nella classe entità

Soprattutto con il wrapper opzionale (modifica: vedi la risposta di 4castle ) Penso che questo (aggiungere campi nell'entità originale) sia un modo praticabile per aggiungere nuove proprietà su piccola scala. Il problema più grande con questo approccio è che potrebbe funzionare contro un accoppiamento basso.

Immagina che la tua classe di libri sia definita in un progetto dedicato per il tuo modello di dominio. Ora aggiungi un altro progetto che utilizza il modello di dominio per eseguire un'attività speciale. Questa attività richiede una proprietà aggiuntiva nella classe del libro. O finisci con l'eredità (vedi sotto) o devi cambiare il modello di dominio comune per rendere possibile la nuova attività. In quest'ultimo caso potresti finire con un mucchio di progetti che dipendono tutti dalle loro proprietà aggiunte alla classe del libro mentre la classe del libro in un certo senso dipende da questi progetti, perché non sei in grado di capire la classe del libro senza il progetti aggiuntivi.

Perché l'ereditarietà è problematica quando si tratta di un modo per fornire proprietà aggiuntive?

Quando vedo la tua classe di esempio "Libro", penso a un oggetto di dominio che spesso finisce per avere molti campi e sottotipi opzionali. Immagina di voler aggiungere una proprietà per i libri che includono un CD. Ora ci sono quattro tipi di libri: libri, libri con colori, libri con CD, libri con colori e CD. Non è possibile descrivere questa situazione con ereditarietà in Java.

Con le interfacce aggiri questo problema. È possibile comporre facilmente le proprietà di una classe di libri specifica con interfacce. La delega e la composizione ti consentiranno di ottenere esattamente la classe che desideri. Con l'ereditarietà di solito si finiscono con alcune proprietà opzionali nella classe che sono lì solo perché una classe di pari livello ne ha bisogno.

Ulteriori letture sul perché l'eredità è spesso un'idea problematica:

Perché l'eredità è generalmente vista come una cosa negativa dai sostenitori di OOP

JavaWorld: Perché l'estensione è malvagia

Il problema con l'implementazione di una serie di interfacce per comporre una serie di proprietà

Quando usi le interfacce per l'estensione, tutto va bene purché tu ne abbia solo un piccolo set. Soprattutto quando il tuo modello a oggetti viene utilizzato ed esteso da altri sviluppatori, ad esempio nella tua azienda, la quantità di interfacce aumenterà. E alla fine si finisce per creare una nuova "interfaccia di proprietà" ufficiale che aggiunge un metodo già utilizzato dai colleghi nel loro progetto per il cliente X per un caso d'uso completamente non correlato: Ugh.

modifica: Un altro aspetto importante è menzionato da gnasher729 . Spesso si desidera aggiungere dinamicamente proprietà opzionali a un oggetto esistente. Con l'ereditarietà o le interfacce dovresti ricreare l'intero oggetto con un'altra classe che è tutt'altro che facoltativa.

Quando ti aspetti estensioni del tuo modello di oggetto a tal punto, starai meglio modellando esplicitamente la possibilità di estensione dinamica . Propongo qualcosa come sopra dove ogni "estensione" (in questo caso proprietà) ha la sua classe. La classe funziona come spazio dei nomi e identificatore per l'estensione. Quando si impostano le convenzioni di denominazione dei pacchetti di conseguenza, in questo modo si consente una quantità infinita di estensioni senza inquinare lo spazio dei nomi per i metodi nella classe di entità originale.

Nello sviluppo del gioco si incontrano spesso situazioni in cui si desidera comporre comportamenti e dati in molte varianti. Questo è il motivo per cui l'architettura modello entità-componente-sistema è diventato molto popolare nei circoli di sviluppo del gioco. Questo è anche un approccio interessante che potresti voler guardare quando ti aspetti molte estensioni al tuo modello di oggetti.


E non ha affrontato la domanda "come decidere tra queste opzioni", è solo un'altra opzione. Perché è sempre meglio di tutte le opzioni presentate dall'OP?
alexanderbird,

Hai ragione, migliorerò la risposta.
chromanoid,

@ 4castle In Java le interfacce non sono ereditarietà! L'implementazione di interfacce è un modo per conformarsi a un contratto mentre ereditare una classe ne sta sostanzialmente estendendo il comportamento. È possibile implementare più interfacce mentre non è possibile estendere più classi in una classe. Nel mio esempio estendi con le interfacce mentre usi l'ereditarietà per ereditare il comportamento dell'entità originale.
chromanoid,

Ha senso. Nel tuo esempio, PropertiesHolderprobabilmente sarebbe una classe astratta. Ma cosa suggeriresti essere un'implementazione per getPropertyo getPropertyValue? I dati devono ancora essere archiviati in qualche tipo di campo di istanza da qualche parte. O useresti a Collection<Property>per memorizzare le proprietà invece di usare i campi?
4castle,

1
Ho fatto l' Bookestensione PropertiesHolderper motivi di chiarezza. Di PropertiesHoldersolito dovrebbe essere un membro di tutte le classi che necessitano di questa funzionalità. L'implementazione dovrebbe contenere una Map<Class<? extends Property<?>>,Property<?>>o qualcosa del genere. Questa è la parte brutta dell'implementazione ma fornisce un framework davvero carino che funziona anche con JAXB e JPA.
chromanoid,

-1

Se la Bookclasse è derivabile senza proprietà del colore come di seguito

class E_Book extends Book{
   private String type;
}

allora l'eredità è ragionevole.


Non sono sicuro di capire il tuo esempio? Se colorè privato, qualsiasi classe derivata non dovrebbe preoccuparsi colorcomunque. Basta aggiungere alcuni costruttori Bookche ignorano del colortutto e per impostazione predefinita null.
4castle,
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.