Voici un scénario courant qui est toujours frustrant pour moi.
J'ai un modèle d'objet avec un objet parent. Le parent contient des objets enfants. Quelque chose comme ça.
public class Zoo
{
public List<Animal> Animals { get; set; }
public bool IsDirty { get; set; }
}
Chaque objet enfant a différentes données et méthodes
public class Animal
{
public string Name { get; set; }
public int Age { get; set; }
public void MakeMess()
{
...
}
}
Lorsque l'enfant change, dans ce cas lorsque la méthode MakeMess est appelée, une valeur dans le parent doit être mise à jour. Disons que lorsqu'un certain seuil d'Animal a gâché, le drapeau IsDirty du zoo doit être défini.
Il existe plusieurs façons de gérer ce scénario (à ma connaissance).
1) Chaque animal peut avoir une référence de zoo parent pour communiquer les changements.
public class Animal
{
public Zoo Parent { get; set; }
...
public void MakeMess()
{
Parent.OnAnimalMadeMess();
}
}
Cela semble être la pire option car il couple Animal à son objet parent. Et si je veux un animal qui vit dans une maison?
2) Une autre option, si vous utilisez un langage qui prend en charge les événements (comme C #) est de demander au parent de s'abonner pour modifier les événements.
public class Animal
{
public event OnMakeMessDelegate OnMakeMess;
public void MakeMess()
{
OnMakeMess();
}
}
public class Zoo
{
...
public void SubscribeToChanges()
{
foreach (var animal in Animals)
{
animal.OnMakeMess += new OnMakeMessDelegate(OnMakeMessHandler);
}
}
public void OnMakeMessHandler(object sender, EventArgs e)
{
...
}
}
Cela semble fonctionner, mais l'expérience devient difficile à maintenir. Si jamais les animaux changent de zoo, vous devez vous désabonner des événements de l'ancien zoo et vous réinscrire au nouveau zoo. Cela ne fait qu'empirer à mesure que l'arbre de composition s'approfondit.
3) L'autre option consiste à déplacer la logique vers le parent.
public class Zoo
{
public void AnimalMakesMess(Animal animal)
{
...
}
}
Cela semble très peu naturel et provoque une duplication de la logique. Par exemple, si j'avais un objet House qui ne partage aucun parent d'héritage commun avec Zoo ..
public class House
{
// Now I have to duplicate this logic
public void AnimalMakesMess(Animal animal)
{
...
}
}
Je n'ai pas encore trouvé de bonne stratégie pour faire face à ces situations. Quoi d'autre est disponible? Comment simplifier cela?
la source
Réponses:
J'ai dû m'en occuper plusieurs fois. La première fois, j'ai utilisé l'option 2 (événements) et comme vous l'avez dit, c'est devenu vraiment compliqué. Si vous suivez cette voie, je suggère fortement que vous ayez besoin de tests unitaires très approfondis pour vous assurer que les événements se déroulent correctement et que vous ne laissez pas de références pendantes, sinon c'est vraiment très difficile à déboguer.
La deuxième fois, je viens d'implémenter la propriété parentale en fonction des enfants, donc gardez une
Dirty
propriété sur chaque animal, et laissezAnimal.IsDirty
revenirthis.Animals.Any(x => x.IsDirty)
. C'était dans le modèle. Au-dessus du modèle, il y avait un contrôleur, et le travail du contrôleur était de savoir qu'après avoir changé le modèle (toutes les actions sur le modèle étaient passées par le contrôleur afin qu'il sache que quelque chose avait changé), il savait qu'il devait appeler certains re -des fonctions d'évaluation, comme déclencher leZooMaintenance
département pour vérifier si leZoo
était à nouveau sale. Alternativement, je pouvais simplement repousser lesZooMaintenance
vérifications jusqu'à une heure ultérieure prévue (toutes les 100 ms, 1 seconde, 2 minutes, 24 heures, tout ce qui était nécessaire).J'ai trouvé que ce dernier était beaucoup plus simple à entretenir et mes craintes de problèmes de performances ne se sont jamais matérialisées.
modifier
Un autre moyen de résoudre ce problème est un modèle de bus de messages . Plutôt que d'utiliser un
Controller
similaire dans mon exemple, vous injectez chaque objet avec unIMessageBus
service. LaAnimal
classe peut ensuite publier un message, comme "Mess Made" et votreZoo
classe peut s'abonner au message "Mess Made". Le service de bus de messages se chargera d'avertir leZoo
moment où un animal publiera l'un de ces messages et pourra réévaluer saIsDirty
propriété.Cela a l'avantage de
Animals
ne plus avoir besoin d'uneZoo
référence, etZoo
n'a pas à se soucier de s'abonner et de se désabonner des événements de chaque singleAnimal
. La pénalité est que toutes lesZoo
classes abonnées à ce message devront réévaluer leurs propriétés, même si ce n'était pas l'un de ses animaux. Cela peut ou non être un gros problème. S'il n'y a qu'un ou deuxZoo
cas, c'est très bien.Modifier 2
Ne négligez pas la simplicité de l'option 1. Quiconque revisite le code n'aura pas beaucoup de mal à le comprendre. Il sera évident pour quelqu'un qui regarde la
Animal
classe que lorsqu'ilMakeMess
est appelé, il propage le message jusqu'auZoo
et il sera évident pour laZoo
classe d'où il tire ses messages. Souvenez-vous qu'en programmation orientée objet, un appel de méthode s'appelait autrefois un "message". En fait, le seul moment où il est très logique de rompre avec l'option 1 est de savoir si plus que simplement leZoo
doit être notifié si leAnimal
fait un gâchis. S'il y avait plus d'objets qui devaient être notifiés, je passerais probablement à un bus de messages ou à un contrôleur.la source
J'ai fait un diagramme de classe simple qui décrit votre domaine:
Chacun
Animal
a unHabitat
gâchis.Le
Habitat
ne se soucie pas de savoir combien ou combien d'animaux il possède (sauf s'il fait fondamentalement partie de votre conception, ce que vous n'avez pas décrit dans ce cas).Mais le
Animal
fait attention, car il se comportera différemment dans tous les casHabitat
.Ce diagramme est similaire au diagramme UML du modèle de conception de stratégie , mais nous l'utiliserons différemment.
Voici quelques exemples de code en Java (je ne veux pas faire d'erreurs spécifiques à C #).
Bien sûr, vous pouvez personnaliser votre conception, ce langage et ces exigences.
Voici l'interface de la stratégie:
Un exemple de béton
Habitat
. bien sûr, chaqueHabitat
sous-classe peut implémenter ces méthodes différemment.Bien sûr, vous pouvez avoir plusieurs sous-classes d'animaux, où chacune les gâche différemment:
Ceci est la classe client, cela explique essentiellement comment vous pouvez utiliser cette conception.
Bien sûr, dans votre application réelle, vous pouvez le faire
Habitat
savoir et le gérerAnimal
si vous en avez besoin.la source
J'ai eu pas mal de succès avec des architectures comme votre option 2 dans le passé. C'est l'option la plus générale et permettra la plus grande flexibilité. Mais, si vous contrôlez vos écouteurs et ne gérez pas beaucoup de types d'abonnement, vous pouvez vous abonner plus facilement aux événements en créant une interface.
L'option d'interface a l'avantage d'être presque aussi simple que votre option 1, mais vous permet également de loger des animaux assez facilement dans un
House
ouFairlyLand
.la source
Dwelling
et fournissez uneMakeMess
méthode dessus. Cela rompt la dépendance circulaire. Ensuite, lorsque l'animal fait un gâchis, il appelledwelling.MakeMess()
aussi.Dans l'esprit de lex parsimoniae , je vais aller avec celui-ci, bien que j'utiliserais probablement la solution de chaîne ci-dessous, en me connaissant. (C'est juste le même modèle que @Benjamin Albert suggère.)
Notez que si vous modélisiez des tables de base de données relationnelles, la relation irait dans l'autre sens: Animal aurait une référence à Zoo et la collection d'animaux pour un Zoo serait un résultat de requête.
Messable
et, dans chaque élément messable, incluez une référence ànext
. Après avoir créé un désordre, appelezMakeMess
l'élément suivant.Donc, le zoo ici est impliqué dans la fabrication d'un gâchis, car il devient trop salissant. Avoir:
Alors maintenant, vous avez une chaîne de choses qui reçoivent le message qu'un gâchis a été créé.
Option 2, un modèle de publication / abonnement pourrait fonctionner ici, mais semble vraiment lourd. L'objet et le conteneur ont une relation connue, il semble donc un peu difficile d'utiliser quelque chose de plus général que cela.
Option 3: dans ce cas particulier, appeler
Zoo.MakeMess(animal)
ouHouse.MakeMess(animal)
n'est pas vraiment une mauvaise option, car une maison peut avoir une sémantique différente pour devenir désordonnée qu'un zoo.Même si vous ne suivez pas la chaîne, il semble qu'il y ait deux problèmes ici: 1) le problème concerne la propagation d'un changement d'un objet vers son conteneur, 2) Il semble que vous souhaitiez désactiver une interface pour le récipient pour résumer où les animaux peuvent vivre.
...
Si vous avez des fonctions de première classe, vous pouvez passer une fonction (ou déléguer) à Animal pour l'appeler après qu'il ait fait un gâchis. C'est un peu comme l'idée de chaîne, sauf avec une fonction au lieu d'une interface.
Lorsque l'animal bouge, il suffit de définir un nouveau délégué.
la source
J'irais avec 1, mais je ferais la relation parent-enfant avec la logique de notification dans un wrapper séparé. Cela supprime la dépendance d'Animal sur Zoo et permet une gestion automatique des relations parent-enfant. Mais cela vous oblige à refaire les objets de la hiérarchie en interfaces / classes abstraites en premier et à écrire un wrapper spécifique pour chaque interface. Mais cela pourrait être supprimé en utilisant la génération de code.
Quelque chose comme :
C'est en fait ainsi que certains ORM effectuent leur suivi des modifications sur les entités. Ils créent des wrappers autour des entités et vous font travailler avec celles-ci. Ces wrappers sont généralement créés à l'aide de la réflexion et de la génération de code dynamique.
la source
Deux options que j'utilise souvent. Vous pouvez utiliser la deuxième approche et placer la logique de câblage de l'événement dans la collection elle-même sur le parent.
Une autre approche (qui peut en fait être utilisée avec l'une des trois options) consiste à utiliser le confinement. Faire un AnimalContainer (ou même en faire une collection) qui peut vivre dans la maison ou le zoo ou autre chose. Il fournit la fonctionnalité de suivi associée aux animaux, mais évite les problèmes d'héritage car il peut être inclus dans tout objet qui en a besoin.
la source
Vous commencez avec un échec de base: les objets enfants ne devraient pas connaître leurs parents.
Les chaînes savent-elles qu'elles sont dans une liste? Non. Les dates savent-elles qu'elles existent dans un calendrier? Non.
La meilleure option consiste à modifier votre conception afin que ce type de scénario n'existe pas.
Après cela, envisagez l'inversion du contrôle. Au lieu de
MakeMess
surAnimal
un effet secondaire ou d'un événement, passezZoo
à la méthode. L'option 1 est très bien si vous devez protéger l'invariant quiAnimal
doit toujours vivre quelque part. Ce n'est pas un parent, mais une association de pairs alors.Parfois, 2 et 3 iront bien, mais le principe architectural clé à suivre est que les enfants ne connaissent pas leurs parents.
la source