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 Result
est 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?
la source
Réponses:
C'est à cela que sert la loi de Déméter / principe du moindre savoir . Un utilisateur de
Model
ne devrait pas nécessairement avoir à connaîtreExperimentalModules
l'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.
la source
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.
la source
Paraphrasant Einstein:
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.
la source
Oui, vous pouvez le modéliser comme des tables relationnelles
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
la source
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.
la source