Héritage vs propriété supplémentaire avec une valeur nulle

12

Pour les classes avec des champs facultatifs, est-il préférable d'utiliser l'héritage ou une propriété nullable? Considérez cet exemple:

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

ou

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

ou

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

Le code dépendant de ces 3 options serait:

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

ou

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

ou

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

La première approche me semble plus naturelle, mais peut-être est-elle moins lisible à cause de la nécessité de faire du casting. Existe-t-il un autre moyen d'obtenir ce comportement?

Bojan VukasovicTest
la source
1
Je crains que la solution ne se résume à votre définition des règles commerciales. Je veux dire, si tous vos livres peuvent avoir une couleur mais que la couleur est facultative, l'héritage est excessif et en fait inutile, car vous voulez que tous vos livres sachent que la propriété de couleur existe. D'un autre côté, si vous pouvez avoir à la fois des livres qui ne connaissent rien à la couleur et des livres qui ont une couleur, créer des cours spécialisés n'est pas mauvais. Même alors, je choisirais probablement la composition plutôt que l'héritage.
Andy
Pour le rendre un peu plus précis - il existe 2 types de livres - un avec couleur et un sans. Donc, tous les livres ne peuvent pas avoir une couleur.
Bojan VukasovicTest
1
S'il y a vraiment un cas, où le livre n'a pas du tout de couleur, dans votre cas, l'héritage est le moyen le plus simple d'étendre les propriétés d'un livre coloré. Dans ce cas, il ne semble pas que vous brisiez quoi que ce soit en héritant de la classe de livre de base et en lui ajoutant une propriété color. Vous pouvez lire sur le principe de substitution liskov pour en savoir plus sur les cas où l'extension d'une classe est autorisée et quand elle ne l'est pas.
Andy
4
Pouvez-vous identifier un comportement que vous souhaitez dans les livres à colorier? Pouvez-vous trouver un comportement courant parmi les livres avec et sans coloration, où les livres avec coloration devraient se comporter légèrement différemment? Si c'est le cas, c'est un bon cas pour la POO avec différents types, et l'idée serait de déplacer les comportements dans les classes au lieu d'interroger la présence et la valeur des propriétés en externe.
Erik Eidt
1
Si les couleurs possibles du livre sont connues à l'avance, que diriez-vous d'un champ Enum pour BookColor avec une option pour BookColor.NoColor?
Graham

Réponses:

7

Cela dépend des circonstances. L'exemple spécifique est irréaliste car vous n'auriez pas de sous-classe appelée BookWithColordans un vrai programme.

Mais en général, une propriété qui n'a de sens que pour certaines sous-classes ne devrait exister que dans ces sous-classes.

Par exemple, si Bookhas PhysicalBooket DigialBookas descendants, alors PhysicalBookpeut avoir une weightpropriété et DigitalBookune sizeInKbpropriété. Mais DigitalBookn'aura pas weightet vice versa. Bookn'aura aucune propriété, car une classe ne devrait avoir que des propriétés partagées par tous les descendants.

Un meilleur exemple est de regarder les classes de vrais logiciels. Le JSlidercomposant a le champ majorTickSpacing. Étant donné que seul un curseur a des "ticks", ce champ n'a de sens que pour JSliderses descendants. Ce serait très déroutant si d'autres composants frères comme JButtonavaient un majorTickSpacingchamp.

JacquesB
la source
ou vous pouvez soustraire le poids pour pouvoir refléter les deux, mais c'est probablement trop.
Walfrat
3
@Walfrat: Ce serait une très mauvaise idée que le même champ représente des choses complètement différentes dans différentes sous-classes.
JacquesB
Et si un livre avait des éditions à la fois physiques et numériques? Vous devez créer une classe différente pour chaque permutation d'avoir ou non chaque champ facultatif. Je pense que la meilleure solution serait simplement de permettre à certains champs d'être omis ou non en fonction du livre. Vous avez cité une situation complètement différente où les deux classes se comporteraient différemment sur le plan fonctionnel. Ce n'est pas ce que cette question implique. Ils ont juste besoin de modéliser une structure de données.
4castle
@ 4castle: Vous auriez alors besoin de classes séparées par édition, car chaque édition peut également avoir un prix différent. Vous ne pouvez pas résoudre ce problème avec des champs facultatifs. Mais nous ne savons pas à partir de l'exemple si l'op souhaite un comportement différent pour les livres en couleur ou les "livres incolores", il est donc impossible de savoir quelle est la bonne modélisation dans ce cas.
JacquesB
3
d'accord avec @JacquesB. vous ne pouvez pas donner une solution universellement correcte pour les "livres de modélisation", car cela dépend du problème que vous essayez de résoudre et de votre activité. Je pense qu'il est assez sûr de dire que définir des hiérarchies de classes où vous violez liskov est catégoriquement erroné (tm) (ouais ouais, votre cas est probablement très spécial et le justifie (bien que j'en doute))
sara
5

Un point important qui ne semble pas avoir été mentionné: dans la plupart des langues, les instances d'une classe ne peuvent pas changer de quelle classe elles sont des instances. Donc, si vous aviez un livre sans couleur et que vous vouliez ajouter une couleur, vous auriez besoin de créer un nouvel objet si vous utilisez différentes classes. Et puis vous devrez probablement remplacer toutes les références à l'ancien objet par des références au nouvel objet.

Si les "livres sans couleur" et les "livres avec couleur" sont des instances de la même classe, l'ajout ou la suppression de la couleur sera alors beaucoup moins problématique. (Si votre interface utilisateur affiche une liste de "livres avec couleur" et "livres sans couleur", alors cette interface utilisateur devra évidemment changer, mais je m'attends à ce que ce soit quelque chose que vous devez gérer de toute façon, semblable à une "liste de rouge"). livres "et" liste de livres verts ").

gnasher729
la source
C'est vraiment un point important! Il définit si l'on doit considérer une collection de propriétés facultatives au lieu d'attributs de classe.
chromanoïde
1

Considérez un JavaBean (comme le vôtre Book) comme un enregistrement dans une base de données. Les colonnes facultatives sont nulllorsqu'elles n'ont aucune valeur, mais c'est parfaitement légal. Par conséquent, votre deuxième option:

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

Est le plus raisonnable. 1

Soyez prudent cependant avec la façon dont vous utilisez la Optionalclasse. Par exemple, ce n'est pas le cas Serializable, ce qui est généralement une caractéristique d'un JavaBean. Voici quelques conseils de Stephen Colebourne :

  1. Ne déclarez aucune variable d'instance de type Optional.
  2. Utilisez nullpour indiquer des données facultatives dans la portée privée d'une classe.
  3. À utiliser Optionalpour les accesseurs qui accèdent au champ facultatif.
  4. Ne pas utiliser Optionalchez les poseurs ou les constructeurs.
  5. À utiliser Optionalcomme type de retour pour toutes les autres méthodes de logique métier qui ont un résultat facultatif.

Par conséquent, dans votre classe, vous devez utiliser nullpour représenter que le champ n'est pas présent, mais lorsque le color quitte le Book(en tant que type de retour), il doit être entouré d'un Optional.

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

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

Cela fournit une conception claire et un code plus lisible.


1 Si vous avez l' intention d'un BookWithColorpour encapsuler une « classe » de l' ensemble des livres qui ont des capacités spécialisées dans d' autres livres, alors il serait logique d'héritage d'utilisation.

4castle
la source
1
Qui a parlé d'une base de données? Si tous les modèles OO devaient s'insérer dans un paradigme de base de données relationnelle, nous devrions changer beaucoup de modèles OO.
alexanderbird
Je dirais que l'ajout de propriétés inutilisées "au cas où" est l'option la moins claire. Comme d'autres l'ont dit, cela dépend du cas d'utilisation, mais la situation la plus souhaitable serait de ne pas transporter des propriétés où une application externe doit savoir si une propriété existante est réellement utilisée.
Volker Kueffel
@Volker Soyons d'accord pour ne pas être d'accord, car je peux citer plusieurs personnes qui ont joué un rôle dans l'ajout de la Optionalclasse à Java dans ce but précis. Ce n'est peut-être pas OO, mais c'est certainement une opinion valable.
4castle
@Alexanderbird Je m'attaquais spécifiquement à un JavaBean, qui devrait être sérialisable. Si j'étais hors sujet en évoquant JavaBeans, c'était mon erreur.
4castle
@Alexanderbird Je ne m'attends pas à ce que tous les modèles correspondent à une base de données relationnelle, mais je m'attends à ce qu'un Java Bean
4castle
0

Vous devez soit utiliser une interface (pas d'héritage) ou une sorte de mécanisme de propriété (peut-être même quelque chose qui fonctionne comme le cadre de description des ressources).

Donc soit

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

ou

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

Clarification (modifier):

Ajouter simplement des champs facultatifs dans la classe d'entité

Surtout avec le wrapper facultatif (modifier: voir la réponse de 4castle ) Je pense que cela (en ajoutant des champs dans l'entité d'origine) est un moyen viable d'ajouter de nouvelles propriétés à petite échelle. Le plus gros problème avec cette approche est qu'elle pourrait fonctionner contre un faible couplage.

Imaginez que votre classe de livres soit définie dans un projet dédié à votre modèle de domaine. Vous ajoutez maintenant un autre projet qui utilise le modèle de domaine pour effectuer une tâche spéciale. Cette tâche nécessite une propriété supplémentaire dans la classe de livres. Soit vous vous retrouvez avec l'héritage (voir ci-dessous), soit vous devez changer le modèle de domaine commun pour rendre la nouvelle tâche possible. Dans ce dernier cas, vous pouvez vous retrouver avec un tas de projets qui dépendent tous de leurs propres propriétés ajoutées à la classe de livre tandis que la classe de livre elle-même dépend en quelque sorte de ces projets, car vous ne pouvez pas comprendre la classe de livre sans le projets supplémentaires.

Pourquoi l'héritage est-il problématique lorsqu'il s'agit de fournir des propriétés supplémentaires?

Quand je vois votre exemple de classe "Book", je pense à un objet de domaine qui finit souvent par avoir de nombreux champs et sous-types facultatifs. Imaginez simplement que vous souhaitez ajouter une propriété pour les livres qui incluent un CD. Il existe maintenant quatre types de livres: livres, livres avec couleur, livres avec CD, livres avec couleur et CD. Vous ne pouvez pas décrire cette situation avec héritage en Java.

Avec les interfaces, vous contournez ce problème. Vous pouvez facilement composer les propriétés d'une classe de livres spécifique avec des interfaces. La délégation et la composition vous permettront d'obtenir facilement la classe souhaitée. Avec l'héritage, vous vous retrouvez généralement avec des propriétés facultatives dans la classe qui ne sont là que parce qu'une classe soeur en a besoin.

Pour en savoir plus, pourquoi l'héritage est souvent une idée problématique:

Pourquoi l'héritage est-il généralement considéré comme une mauvaise chose par les partisans de la POO

JavaWorld: Pourquoi s'étendre est mal

Le problème avec l'implémentation d'un ensemble d'interfaces pour composer un ensemble de propriétés

Lorsque vous utilisez des interfaces pour l'extension, tout va bien tant que vous n'en avez qu'un petit ensemble. Surtout lorsque votre modèle d'objet est utilisé et étendu par d'autres développeurs, par exemple dans votre entreprise, la quantité d'interfaces augmentera. Et finalement, vous finissez par créer une nouvelle "propriété-interface" officielle qui ajoute une méthode que vos collègues ont déjà utilisée dans leur projet pour le client X pour un cas d'utilisation totalement indépendant - Ugh.

edit: Un autre aspect important est mentionné par gnasher729 . Vous souhaitez souvent ajouter dynamiquement des propriétés facultatives à un objet existant. Avec l'héritage ou les interfaces, vous devrez recréer l'objet entier avec une autre classe qui est loin d'être facultative.

Lorsque vous vous attendez à des extensions de votre modèle d'objet dans une telle mesure, vous serez mieux en modélisant explicitement la possibilité d' extension dynamique . Je propose quelque chose comme ci-dessus où chaque "extension" (dans ce cas la propriété) a sa propre classe. La classe fonctionne comme espace de noms et identifiant pour l'extension. Lorsque vous définissez les conventions de dénomination des packages en conséquence, cette méthode autorise une quantité infinie d'extensions sans polluer l'espace de noms pour les méthodes de la classe d'entité d'origine.

Dans le développement de jeux, vous rencontrez souvent des situations où vous souhaitez composer le comportement et les données dans de nombreuses variantes. C'est pourquoi le système entité-composant de modèle d'architecture est devenu très populaire dans les cercles de développement de jeux. C'est également une approche intéressante que vous voudrez peut-être examiner lorsque vous attendez de nombreuses extensions de votre modèle d'objet.

chromanoïde
la source
Et il n'a pas abordé la question "comment décider entre ces options", c'est juste une autre option. Pourquoi est-ce toujours mieux que toutes les options présentées par le PO?
alexanderbird
Vous avez raison, je vais améliorer la réponse.
chromanoïde
@ 4castle Les interfaces Java ne sont pas un héritage! L'implémentation d'interfaces est un moyen de se conformer à un contrat tandis que l'héritage d'une classe étend essentiellement son comportement. Vous pouvez implémenter plusieurs interfaces sans pouvoir étendre plusieurs classes dans une seule classe. Dans mon exemple, vous étendez des interfaces pendant que vous utilisez l'héritage pour hériter du comportement de l'entité d'origine.
chromanoïde
Logique. Dans votre exemple, ce PropertiesHolderserait probablement une classe abstraite. Mais que proposez-vous comme une implémentation pour getPropertyou getPropertyValue? Les données doivent encore être stockées dans une sorte de champ d'instance quelque part. Ou utiliseriez-vous un Collection<Property>pour stocker les propriétés au lieu d'utiliser des champs?
4castle
1
J'ai fait une Bookextension PropertiesHolderpour des raisons de clarté. Le PropertiesHolderdevrait généralement être membre de toutes les classes qui ont besoin de cette fonctionnalité. L'implémentation contiendrait un Map<Class<? extends Property<?>>,Property<?>>ou quelque chose comme ça. C'est la partie laide de l'implémentation mais elle fournit un cadre vraiment sympa qui fonctionne même avec JAXB et JPA.
chromanoïde
-1

Si la Bookclasse est dérivable sans propriété de couleur comme ci-dessous

class E_Book extends Book{
   private String type;
}

alors l'héritage est raisonnable.

adem caglin
la source
Je ne suis pas sûr de comprendre votre exemple? Si colorest privé, aucune classe dérivée ne devrait de colortoute façon être concernée . Ajoutez simplement quelques constructeurs à Bookcela ignorer colorcomplètement et ce sera par défaut null.
4castle