Les «hiérarchies de composition profonde» ne sont-elles pas aussi mauvaises?

19

Toutes mes excuses si "Composition Hiérarchie" n'est pas une chose, mais je vais expliquer ce que j'entends par là dans la question.

Il n'y a aucun programmeur OO qui ne soit tombé sur une variante de "Garder les hiérarchies d'héritage à plat" ou "Préférer la composition à l'héritage" et ainsi de suite. Cependant, les hiérarchies de composition profondes semblent également problématiques.

Disons que nous avons besoin d'une collection de rapports détaillant les résultats d'une expérience:

class Model {
    // ... interface

    Array<Result> m_results;
}

Chaque résultat a certaines propriétés. Ceux-ci incluent le temps de l'expérience, ainsi que certaines métadonnées de chaque étape de l'expérience:

enum Stage {
    Pre = 1,
    Post
};

class Result {
    // ... interface

    Epoch m_epoch;
    Map<Stage, ExperimentModules> m_modules; 
}

D'accord, super. Désormais, chaque module d'expérience possède une chaîne décrivant le résultat de l'expérience, ainsi qu'une collection de références à des ensembles d'échantillons expérimentaux:

class ExperimentalModules {
    // ... interface

    String m_reportText;
    Array<Sample> m_entities;
}

Et puis chaque échantillon a ... eh bien, vous obtenez l'image.

Le problème est que si je modélise des objets de mon domaine d'application, cela semble être un ajustement très naturel, mais à la fin de la journée, a Resultest juste un conteneur stupide de données! Il ne semble pas utile de créer un grand groupe de classes pour cela.

En supposant que les structures de données et les classes illustrées ci-dessus modélisent correctement les relations dans le domaine d'application, existe-t-il une meilleure façon de modéliser un tel "résultat", sans recourir à une hiérarchie de composition approfondie? Existe-t-il un contexte externe qui pourrait vous aider à déterminer si une telle conception est bonne ou non?

Sauce extraordinnaire
la source
Je ne comprends pas la partie du contexte externe.
Tulains Córdova
@ TulainsCórdova ce que j'essaie de demander ici est "y a-t-il des situations pour lesquelles la composition profonde est une bonne solution"?
AwesomeSauce
J'ai ajouté une réponse.
Tulains Córdova
5
Oui, exactement. Les problèmes que les gens tentent de résoudre en remplaçant l'héritage par la composition ne disparaissent pas comme par magie simplement parce que vous avez changé une stratégie d'agrégation de fonctionnalités pour une autre. Ce sont deux solutions pour gérer la complexité, et cette complexité est toujours là et doit être gérée d'une manière ou d'une autre.
Mason Wheeler
3
Je ne vois rien de mal avec les modèles de données profonds. Mais je ne vois pas non plus comment vous auriez pu modéliser cela avec l'héritage, car il s'agit d'une relation 1: N à chaque couche.
usr

Réponses:

17

C'est à cela que sert la loi de Déméter / principe du moindre savoir . Un utilisateur de Modelne devrait pas nécessairement avoir à connaître ExperimentalModulesl'interface pour faire son travail.

Le problème général est que lorsqu'une classe hérite d'un autre type ou a un champ public avec un certain type ou lorsqu'une méthode prend un type comme paramètre ou renvoie un type, les interfaces de tous ces types deviennent partie intégrante de l'interface effective de ma classe. La question devient: ces types dont je dépend: les utilise-t-elle entièrement dans ma classe? Ou s'agit-il simplement de détails d'implémentation que j'ai accidentellement exposés? Parce que s'il s'agit de détails d'implémentation, je viens de les exposer et de permettre aux utilisateurs de ma classe de dépendre d'eux - mes clients sont désormais étroitement liés à mes détails d'implémentation.

Donc, si je veux améliorer la cohésion, je peux limiter ce couplage en écrivant plus de méthodes qui font des trucs utiles, au lieu d'obliger les utilisateurs à accéder à mes structures privées. Ici, nous pouvons proposer des méthodes pour rechercher ou filtrer les résultats. Cela rend ma classe plus facile à refactoriser, car je peux maintenant changer la représentation interne sans changer brusquement mon interface.

Mais cela n'est pas sans coût - l'interface de ma classe exposée est maintenant gonflée d'opérations qui ne sont pas toujours nécessaires. Cela viole le principe de séparation d'interface: je ne devrais pas forcer les utilisateurs à dépendre de plus d'opérations qu'ils n'en utilisent réellement. Et c'est là que la conception est importante: la conception consiste à décider quel principe est le plus important dans votre contexte et à trouver un compromis approprié.

Lors de la conception d'une bibliothèque qui doit maintenir la compatibilité avec les sources sur plusieurs versions, je serais plus enclin à suivre plus attentivement le principe de la moindre connaissance. Si ma bibliothèque utilise une autre bibliothèque en interne, je n'exposerais jamais rien de défini par cette bibliothèque à mes utilisateurs. Si j'ai besoin d'exposer une interface de la bibliothèque interne, je créerais un simple objet wrapper. Les modèles de conception comme le modèle de pont ou le modèle de façade peuvent vous aider ici.

Mais si mes interfaces ne sont utilisées qu'en interne, ou si les interfaces que j'exposerais sont très fondamentales (faisant partie d'une bibliothèque standard, ou d'une bibliothèque si fondamentale qu'il ne serait jamais logique de la changer), alors il pourrait être inutile de se cacher ces interfaces. Comme d'habitude, il n'y a pas de réponse unique.

amon
la source
Vous avez mentionné une préoccupation majeure que j'avais. l'idée que je devais aller loin dans la mise en œuvre afin de faire quelque chose d'utile à des niveaux d'abstraction plus élevés. Une discussion très utile, merci.
AwesomeSauce
7

Les «hiérarchies de composition profonde» ne sont-elles pas aussi mauvaises?

Bien sûr. La règle devrait en fait dire «garder toutes les hiérarchies aussi plates que possible».

Mais les hiérarchies de composition profondes sont moins fragiles que les hiérarchies d'héritage profondes et plus flexibles à modifier. Intuitivement, cela ne devrait pas surprendre; les hiérarchies familiales sont les mêmes. Votre relation avec vos parents de sang est fixe dans la génétique; vos relations avec vos amis et votre conjoint ne le sont pas.

Votre exemple ne me semble pas être une "hiérarchie profonde". Une forme hérite d'un rectangle qui hérite d'un ensemble de points qui hérite des coordonnées x et y, et je n'ai même pas encore eu la tête pleine de vapeur.

Robert Harvey
la source
4

Paraphrasant Einstein:

garder toutes les hiérarchies aussi plates que possible, mais pas plus plates

Ce qui signifie que si le problème que vous modélisez est comme ça, vous ne devriez pas avoir de scrupules à le modéliser tel quel.

Si l'application n'était qu'une feuille de calcul géante, vous n'avez pas à modéliser la hiérarchie de composition telle qu'elle est. Sinon, faites-le. Je ne pense pas que la chose soit infiniment profonde. Gardez simplement les getters qui retournent des collections paresseux si le remplissage de ces collections coûte cher, comme les demander à un référentiel qui les extrait d'une base de données. Par paresseux, je veux dire, ne les remplissez pas avant d'en avoir besoin.

Tulains Córdova
la source
1

Oui, vous pouvez le modéliser comme des tables relationnelles

Result {
    Id
    ModelId
    Epoch
}

etc., ce qui peut souvent conduire à une programmation plus facile et à une mise en correspondance simple avec une base de données. mais au détriment de perdre le codage dur de vos relations

Ewan
la source
0

Je ne sais pas vraiment comment résoudre ce problème en Java, car Java est construit autour de l'idée que tout est un objet. Vous ne pouvez pas supprimer les classes intermédiaires en les remplaçant par un dictionnaire car les types dans vos objets blob de données sont hétérogènes (comme un horodatage + liste ou une chaîne + liste ...). L'alternative de remplacer une classe de conteneur par son champ (par exemple, transmettre une variable "m_epoch" avec un "m_modules") est tout simplement mauvaise, car elle crée un objet implicite (vous ne pouvez pas en avoir un sans l'autre) sans particulier avantage.

Dans mon langage de prédilection (Python), ma règle de base serait de ne pas créer d'objet si aucune logique particulière (à l'exception de l'obtention et du paramétrage de base) ne lui appartient. Mais étant donné vos contraintes, je pense que la hiérarchie de composition que vous avez est la meilleure conception possible.

Arthur Havlicek
la source