Beaucoup de petites classes vs héritage logique (mais) complexe

24

Je me demande ce qui est mieux en termes de bonne conception OOP, de code propre, de flexibilité et d'éviter les odeurs de code à l'avenir. Situation d'image, où vous avez beaucoup d'objets très similaires que vous devez représenter en tant que classes. Ces classes sont sans aucune fonctionnalité spécifique, juste des classes de données et sont différentes uniquement par leur nom (et leur contexte) Exemple:

Class A
{
  String name;
  string description;
}

Class B
{
  String name;
  String count;
  String description;
}

Class C
{
  String name;
  String count;
  String description;
  String imageUrl;
}

Class D
{
  String name;
  String count;
}

Class E
{
  String name;
  String count;
  String imageUrl;
  String age;
}

Serait-il préférable de les conserver dans des classes séparées, pour obtenir une "meilleure" lisibilité, mais avec de nombreuses répétitions de code, ou serait-il préférable d'utiliser l'héritage pour être plus SEC?

En utilisant l'héritage, vous vous retrouverez avec quelque chose comme ça, mais les noms de classe perdront leur signification contextuelle (à cause de is-a, not has-a):

Class A
{
  String name;
  String description;
}

Class B : A
{
  String count;
}

Class C : B
{
  String imageUrl;
}

Class D : C
{
  String age;
}

Je sais que l'héritage n'est pas et ne devrait pas être utilisé pour la réutilisation du code, mais je ne vois pas d'autre moyen possible de réduire la répétition du code dans ce cas.

user969153
la source

Réponses:

58

La règle générale se lit "Préférer la délégation à l'héritage", et non "éviter tout héritage". Si les objets ont une relation logique et qu'un B peut être utilisé partout où un A est attendu, il est recommandé d'utiliser l'héritage. Cependant, si les objets se trouvent simplement avoir des champs avec le même nom et n'ont aucune relation de domaine, n'utilisez pas l'héritage.

Très souvent, il vaut la peine de poser la question de la «raison du changement» dans le processus de conception: si la classe A obtient un champ supplémentaire, vous attendriez-vous à ce que la classe B l'obtienne également? Si c'est vrai, les objets partagent une relation, et l'héritage est une bonne idée. Sinon, souffrez de la petite répétition pour garder des concepts distincts dans un code distinct.

thiton
la source
Bien sûr, la raison du changement est un bon point, mais qu'en situation, où ces classes sont et seront toujours constantes?
user969153
20
@ user969153: Quand les classes seront vraiment constantes, cela n'a pas d'importance. Toute bonne ingénierie logicielle est destinée au futur mainteneur, qui doit faire des changements. S'il n'y aura pas de modifications futures, tout se passe, mais croyez-moi, vous aurez toujours des modifications, sauf si vous programmez pour la poubelle.
thiton
@ user969153: Cela étant dit, la "raison du changement" est une heuristique. Cela vous aide à décider, rien de plus.
thiton
2
Bonne réponse. Les raisons de changer sont S dans l'acronyme SOLID de l'oncle bob qui aidera à concevoir de bons logiciels.
Klee
4
@ user969153, D'après mon expérience, "où ces classes sont et seront toujours constantes?" n'est pas un cas d'utilisation valide :) Je n'ai pas encore travaillé sur une application de taille significative qui n'a pas connu de changement totalement inattendu.
cdkMoose
18

En utilisant l'héritage, vous vous retrouverez avec quelque chose comme ça, mais les noms de classe perdront leur signification contextuelle (à cause de is-a, not has-a):

Exactement. Regardez cela, hors contexte:

Class D : C
{
    String age;
}

Quelles sont les propriétés d'un D? Vous souvenez-vous? Et si vous compliquez davantage la hiérarchie:

Class E : B
{
    int numberOfWheels;
}

Class F : E
{
    String favouriteColour;
}

Et puis si, horreur après horreur, vous voulez ajouter un champ imageUrl à F mais pas E? Vous ne pouvez pas le dériver de C et E.

J'ai vu une situation réelle où de nombreux types de composants différents avaient tous un nom, un ID et une description. Mais ce n'était pas par la nature de leurs composants, c'était juste une coïncidence. Chacun avait un identifiant car ils étaient stockés dans une base de données. Le nom et la description devaient être affichés.

Les produits avaient également un ID, un nom et une description, pour des raisons similaires. Mais ce n'étaient pas des composants, ni des produits de composants. Vous ne verriez jamais les deux choses ensemble.

Mais, un développeur avait lu sur DRY et avait décidé de supprimer toute trace de duplication du code. Il a donc appelé tout un produit et a supprimé la nécessité de définir ces domaines. Ils étaient définis dans Product et tout ce qui avait besoin de ces champs pouvait dériver de Product.

Je ne peux pas le dire assez fort: ce n'est pas une bonne idée.

DRY ne consiste pas à supprimer le besoin de propriétés communes. Si un objet n'est pas un produit, ne l'appelez pas un produit. Si un produit N'EXIGE PAS une description, par la nature même de son caractère de produit, ne définissez pas la description comme une propriété du produit.

Il s'est avéré, finalement, que nous avions des composants sans descriptions. Nous avons donc commencé à avoir des descriptions nulles dans ces objets. Non annulable. Nul. Toujours. Pour tout produit de type X ... ou ultérieur Y ... et N.

Il s'agit d'une violation flagrante du principe de substitution de Liskov.

DRY consiste à ne jamais se mettre dans une position où vous pourriez corriger un bug à un endroit et oublier de le faire ailleurs. Cela ne va pas se produire car vous avez deux objets avec la même propriété. Jamais. Alors ne vous en faites pas.

Si le contexte des classes rend évident que vous devriez, ce qui sera très rare, alors et seulement alors, vous devriez considérer une classe parente commune. Mais, s'il s'agit de propriétés, réfléchissez à la raison pour laquelle vous n'utilisez pas d'interface à la place.

pdr
la source
4

L'hérédité est un moyen d'obtenir un comportement polymorphe. Si vos classes n'ont aucun comportement, l'héritage est inutile. Dans ce cas, je ne les considérerais même pas comme des objets / classes, mais plutôt comme des structures.

Si vous souhaitez pouvoir mettre différentes structures dans différentes parties de votre comportement, envisagez des interfaces avec des méthodes ou des propriétés get / set, comme IHasName, IHasAge, etc. Cela rendra la conception beaucoup plus propre et permettra une meilleure composition de celles-ci. genre de hiearchies.

Euphorique
la source
3

N'est-ce pas à cela que servent les interfaces? Classes sans rapport avec des fonctions différentes mais avec une propriété similaire.

IName = Interface(IInterface)
  function Getname : String;
  property Name : String read Getname
end;

Class Dog(InterfacedObject, IName)
private
  FName : String;
  Function GetName : String;
Public
  Name : Read Getname;
end;

Class Movie(InterfacedObject, IName)
private
  FCount;
  FName : String;
  Function GetName : String;
Public
  Name : String Read Getname;
  Count : read FCount; 
end;
Pieter B
la source
1

Votre question semble être encadrée dans le contexte d'un langage qui ne prend pas en charge l'héritage multiple mais ne le spécifie pas spécifiquement. Si vous travaillez avec un langage qui prend en charge l'héritage multiple, cela devient très facile à résoudre avec un seul niveau d'héritage.

Voici un exemple de la façon dont cela peut être résolu en utilisant l'héritage multiple.

trait Nameable { String name; }
trait Describable { String description; }
trait Countable { Integer count; }
trait HasImageUrl { String imageUrl; }
trait Living { String age; }

class A : Nameable, Describable;
class B : Nameable, Describable, Countable;
class C : Nameable, Describable, Countable, HasImageUrl;
class D : Nameable, Countable;
class E : Nameable, Countable, HasImageUrl, Living;
pgraham
la source