Modèles de propagation des modifications dans un modèle d'objet ..?

22

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?

ConditionRacer
la source
Vous avez raison de dire que le n ° 1 est mauvais, et je ne suis pas non plus enthousiasmé par le n ° 2; en général, vous voulez éviter les effets secondaires, et au lieu de cela, vous les augmentez. Concernant l'option # 3, pourquoi ne pouvez-vous pas factoriser AnimalMakeMess dans une méthode statique que toutes les classes peuvent appeler?
Doval
4
# 1 n'est pas nécessairement mauvais si vous communiquez via une interface (IAnimalObserver) au lieu de cette classe Parent spécifique.
coredump

Réponses:

11

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 Dirtypropriété sur chaque animal, et laissez Animal.IsDirtyrevenir this.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 le ZooMaintenancedépartement pour vérifier si le Zooétait à nouveau sale. Alternativement, je pouvais simplement repousser les ZooMaintenancevé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 Controllersimilaire dans mon exemple, vous injectez chaque objet avec un IMessageBusservice. La Animalclasse peut ensuite publier un message, comme "Mess Made" et votre Zooclasse peut s'abonner au message "Mess Made". Le service de bus de messages se chargera d'avertir le Zoomoment où un animal publiera l'un de ces messages et pourra réévaluer sa IsDirtypropriété.

Cela a l'avantage de Animalsne plus avoir besoin d'une Zooréférence, et Zoon'a pas à se soucier de s'abonner et de se désabonner des événements de chaque single Animal. La pénalité est que toutes les Zooclasses 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 deux Zoocas, 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 Animalclasse que lorsqu'il MakeMessest appelé, il propage le message jusqu'au Zooet il sera évident pour la Zooclasse 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 le Zoodoit être notifié si le Animalfait 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.

Scott Whitlock
la source
5

J'ai fait un diagramme de classe simple qui décrit votre domaine: entrez la description de l'image ici

Chacun Animal a un Habitat gâchis.

Le Habitatne 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 Animalfait attention, car il se comportera différemment dans tous les cas Habitat.

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:

public interface Habitat {
    public void messUp(float magnitude);

    public float getCleanliness();
}

Un exemple de béton Habitat. bien sûr, chaque Habitatsous-classe peut implémenter ces méthodes différemment.

public class Zoo implements Habitat {
    public float cleanliness = 1;

    public float getCleanliness() {
        return cleanliness;
    }

    public void messUp(float magnitude) {
        cleanliness -= magnitude;
    }
}

Bien sûr, vous pouvez avoir plusieurs sous-classes d'animaux, où chacune les gâche différemment:

public class Animel {
    private Habitat habitat;

    public void makeMess() {
        habitat.messUp(.05f);
    }

    public Animel addTo(Habitat habitat) {
        this.habitat = habitat;
        return this;
    }
}

Ceci est la classe client, cela explique essentiellement comment vous pouvez utiliser cette conception.

public class ZooKeeper {
    public Habitat zoo = new Zoo();

    public ZooKeeper() {
        new Animal()
            .addTo( zoo )
            .makeMess();

        if (zoo.getCleanliness() < 0.5f) {
            System.out.println("The zoo is really messy");
        } else {
            System.out.println("The zoo looks clean");
        }
    }
}

Bien sûr, dans votre application réelle, vous pouvez le faire Habitatsavoir et le gérer Animalsi vous en avez besoin.

Benjamin Albert
la source
3

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.

interface MessablePlace
{
  void OnMess(object sender, MessEvent e);
}

class MessEvent
{
  String DetailsOrWhatever;
}

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 Houseou FairlyLand.

svidgen
la source
3
  • L'option 1 est en fait assez simple. C'est juste une référence arrière. Mais généralisez-le avec l'interface appelée Dwellinget fournissez une MakeMessméthode dessus. Cela rompt la dépendance circulaire. Ensuite, lorsque l'animal fait un gâchis, il appelle dwelling.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.

  • Prenant cette idée plus loin, vous pouvez utiliser une architecture chaînée. Autrement dit, créez une interface Messableet, dans chaque élément messable, incluez une référence à next. Après avoir créé un désordre, appelez MakeMessl'élément suivant.

Donc, le zoo ici est impliqué dans la fabrication d'un gâchis, car il devient trop salissant. Avoir:

Zoo implements Messable
House implements Messable
Animal implements Messable
   Messable next

   MakeMess()
       messy = true
       next.MakeMess

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)ou House.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.

public Animal
    Function afterMess

    public MakeMess()
        messy = true
        afterMess()

Lorsque l'animal bouge, il suffit de définir un nouveau délégué.

  • Poussé à l'extrême, vous pouvez utiliser la programmation orientée aspect (AOP) avec des conseils "après" sur MakeMess.
Rob
la source
2

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 :

public interface IAnimal
{
    string Name { get; set; }
    int Age { get; set; }

    void MakeMess();
}

public class Animal : IAnimal
{
    public string Name { get; set; }
    public int Age { get; set; }

    public void MakeMess()
    {
        // makes mess
    }
}

public class ZooAnimals
{
    class AnimalInZoo : IAnimal
    {
        public IAnimal _animal;
        public ZooAnimals _zoo;

        public AnimalInZoo(IAnimal animal, ZooAnimals zoo)
        {
            _animal = animal;
            _zoo = zoo;
        }

        public string Name { get { return _animal.Name; } set { _animal.Name = value; } }
        public int Age { get { return _animal.Age; } set { _animal.Age = value; } }

        public void MakeMess()
        {
            _animal.MakeMess();
            _zoo.IsDirty = true;
        }
    }

    private Collection<AnimalInZoo> animals = new Collection<AnimalInZoo>();

    public IAnimal Add(IAnimal animal)
    {
        if (animal is AnimalInZoo)
        {
            var inZoo = (AnimalInZoo)animal;
            if (inZoo._zoo != this)
            {
                // animal is in a different zoo, what to do ?
                // either move animal to this zoo
                // or throw an exception so caller is forced to remove the animal from previous zoo first
            }
        }

        var anim = new AnimalInZoo(animal, this);
        animals.Add(anim);
        return anim;
    }

    public IAnimal Remove(IAnimal animal)
    {
        if (!(animal is AnimalInZoo))
        {
            // animal is not in zoo, throw an exception?
        }
        var inZoo = (AnimalInZoo)animal;
        if (inZoo._zoo != this)
        {
            // animal is in a different zoo, throw an exception?
        }

        animals.Remove(inZoo);
        return inZoo._animal;
    }

    public bool IsDirty { get; set; }
}

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.

Euphorique
la source
1

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.

AJ Henderson
la source
0

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 MakeMesssur Animalun effet secondaire ou d'un événement, passez Zooà la méthode. L'option 1 est très bien si vous devez protéger l'invariant qui Animaldoit 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.

Telastyn
la source
Je soupçonne que cela ressemble plus à un bouton d'envoi sous une forme qu'à une chaîne dans une liste.
svidgen
1
@svidgen - Puis passez un rappel. Plus infaillible qu'un événement, plus facile à raisonner et aucune référence coquine à des choses qu'il n'a pas besoin de savoir.
Telastyn