Quel est le meilleur moyen d'initialiser la référence d'un enfant à son parent?


35

Je développe un modèle objet qui comporte de nombreuses classes parent / enfant. Chaque objet enfant a une référence à son objet parent. Je peux penser (et avoir essayé) plusieurs façons d’initialiser la référence parente, mais je trouve des inconvénients importants pour chaque approche. Compte tenu des approches décrites ci-dessous, quelle est la meilleure ... ou encore, la meilleure solution?

Je ne vais pas m'assurer que le code ci-dessous est compilé, essayez donc de voir mon intention si le code n'est pas correct du point de vue de la syntaxe.

Notez que certains de mes constructeurs de classes enfants prennent des paramètres (autres que parent) même si je n'en affiche pas toujours.

  1. L'appelant est responsable de la définition du parent et de l'ajout au même parent.

    class Child {
      public Child(Parent parent) {Parent=parent;}
      public Parent Parent {get; private set;}
    }
    class Parent {
      // singleton child
      public Child Child {get; set;}
      //children
      private List<Child> _children = new List<Child>();
      public List<Child> Children { get {return _children;} }
    }
    

    Inconvénient: la définition du parent est un processus en deux étapes pour le consommateur.

    var child = new Child(parent);
    parent.Children.Add(child);
    

    Inconvénient: risque d'erreur. L'appelant peut ajouter un enfant à un parent différent de celui utilisé pour l'initialiser.

    var child = new Child(parent1);
    parent2.Children.Add(child);
    
  2. Parent vérifie que l'appelant ajoute l'enfant au parent pour lequel il a été initialisé.

    class Child {
      public Child(Parent parent) {Parent = parent;}
      public Parent Parent {get; private set;}
    }
    class Parent {
      // singleton child
      private Child _child;
      public Child Child {
        get {return _child;}
        set {
          if (value.Parent != this) throw new Exception();
          _child=value;
        }
      }
      //children
      private List<Child> _children = new List<Child>();
      public ReadOnlyCollection<Child> Children { get {return _children;} }
      public void AddChild(Child child) {
        if (child.Parent != this) throw new Exception();
        _children.Add(child);
      }
    }
    

    Inconvénient: l' appelant a toujours un processus en deux étapes pour définir le parent.

    Inconvénient: vérification de l'exécution - réduit les performances et ajoute du code à chaque ajout / configurateur.

  3. Le parent définit la référence parent de l'enfant (à lui-même) lorsque l'enfant est ajouté / attribué à un parent. Le parent parent est interne.

    class Child {
      public Parent Parent {get; internal set;}
    }
    class Parent {
      // singleton child
      private Child _child;
      public Child Child {
        get {return _child;}
        set {
          value.Parent = this;
          _child = value;
        }
      }
      //children
      private List<Child> _children = new List<Child>();
      public ReadOnlyCollection<Child> Children { get {return _children;} }
      public void AddChild(Child child) {
        child.Parent = this;
        _children.Add(child);
      }
    }

    Inconvénient: l'enfant est créé sans référence parent. Parfois, l’initialisation / validation nécessite le parent, ce qui signifie qu’une initialisation / validation doit être effectuée dans le parent parent de l’enfant. Le code peut devenir compliqué. Il serait tellement plus facile d'implémenter l'enfant s'il avait toujours sa référence parent.

  4. Le parent expose des méthodes d'ajout d'usine afin qu'un enfant ait toujours une référence parent. Le ctor de l'enfant est interne. Le parent parent est privé.

    class Child {
      internal Child(Parent parent, init-params) {Parent = parent;}
      public Parent Parent {get; private set;}
    }
    class Parent {
      // singleton child
      public Child Child {get; private set;}
      public void CreateChild(init-params) {
          var child = new Child(this, init-params);
          Child = value;
      }
      //children
      private List<Child> _children = new List<Child>();
      public ReadOnlyCollection<Child> Children { get {return _children;} }
      public Child AddChild(init-params) {
        var child = new Child(this, init-params);
        _children.Add(child);
        return child;
      }
    }

    Inconvénient: impossible d'utiliser une syntaxe d'initialisation telle que new Child(){prop = value}. Au lieu de faire:

    var c = parent.AddChild(); 
    c.prop = value;

    Inconvénient: Doivent dupliquer les paramètres du constructeur enfant dans les méthodes add-factory.

    Inconvénient: impossible d'utiliser un créateur de propriété pour un enfant singleton. Il semble boiteux que j'ai besoin d'une méthode pour définir la valeur mais fournir un accès en lecture via un getter de propriété. C'est déséquilibré.

  5. Child s'ajoute au parent référencé dans son constructeur. Le ctor de l'enfant est public. Aucun public n'ajoute l'accès du parent.

    //singleton
    class Child{
      public Child(ParentWithChild parent) {
        Parent = parent;
        Parent.Child = this;
      }
      public ParentWithChild Parent {get; private set;}
    }
    class ParentWithChild {
      public Child Child {get; internal set;}
    }
    
    //children
    class Child {
      public Child(ParentWithChildren parent) {
        Parent = parent;
        Parent._children.Add(this);
      }
      public ParentWithChildren Parent {get; private set;}
    }
    class ParentWithChildren {
      internal List<Child> _children = new List<Child>();
      public ReadOnlyCollection<Child> Children { get {return _children;} }
    }

    Inconvénient: la syntaxe d'appel n'est pas géniale. Normalement, on appelle une addméthode sur le parent au lieu de simplement créer un objet comme ceci:

    var parent = new ParentWithChildren();
    new Child(parent); //adds child to parent
    new Child(parent);
    new Child(parent);

    Et définit une propriété plutôt que de simplement créer un objet comme celui-ci:

    var parent = new ParentWithChild();
    new Child(parent); // sets parent.Child

...

Je viens d'apprendre que SE n'autorise pas certaines questions subjectives et qu'il s'agit clairement d'une question subjective. Mais, peut-être que c'est une bonne question subjective.


14
La meilleure pratique est que les enfants ne doivent pas connaître leurs parents.
Telastyn


2
@Telastyn Je ne peux pas m'empêcher de lire cela comme une langue dans la joue, et c'est hilarant. Aussi complètement mort sanglant précis. Steven, le terme à examiner est «acyclique», car il existe de nombreux ouvrages sur les raisons pour lesquelles vous devriez rendre les graphiques acycliques, dans la mesure du possible.
Jimmy Hoffa

10
@Telastyn, vous devriez essayer d'utiliser ce commentaire sur parenting.stackexchange
Fabio Marcolini

2
Hmm. Je ne sais pas comment déplacer une publication (ne voit pas de contrôle de drapeau). J'ai réaffiché aux programmeurs depuis que quelqu'un m'a dit que cela appartenait là.
Steven Broshar

Réponses:


19

Je resterais à l'écart de tout scénario qui obligerait nécessairement l'enfant à connaître le parent.

Il existe des moyens de transmettre des messages d'un enfant à un parent via des événements. De cette façon, le parent, lors de l’ajout, doit simplement s’inscrire à un événement déclenché par l’enfant, sans que celui-ci ait à connaître le parent directement. Après tout, il s’agit probablement de l’utilisation prévue d’un enfant connaissant son parent afin de pouvoir utiliser celui-ci à bon escient. Sauf que vous ne voudriez pas que l'enfant d' effectuer le travail du parent, donc vraiment ce que vous voulez faire est de dire simplement le parent que quelque chose est arrivé. Par conséquent, vous devez gérer un événement sur l’enfant, dont le parent peut tirer parti.

Ce modèle évolue également très bien si cet événement devenait utile à d'autres classes. C’est peut-être un peu exagéré, mais cela vous empêche également de vous tirer une balle dans le pied plus tard, car il devient tentant de vouloir utiliser parent dans votre classe enfant qui ne couple encore plus les deux classes. La refactorisation ultérieure de telles classes prend beaucoup de temps et peut facilement créer des bogues dans votre programme.

J'espère que ça t'as aidé!


6
Éloignez-vous de tout scénario qui oblige nécessairement l'enfant à connaître le parent ” - pourquoi? Votre réponse repose sur l'hypothèse que les graphes d'objets circulaires sont une mauvaise idée. Bien que cela soit parfois le cas (par exemple, lorsque la gestion de la mémoire via un comptage par référence naïf - pas le cas en C #), ce n’est pas une mauvaise chose en général. Notamment, le modèle d'observateur (qui est souvent utilisé pour envoyer des événements) implique l'observable ( Child) maintenant un ensemble d'observateurs ( Parent), ce qui réintroduit la circularité de manière rétrograde (et introduit un certain nombre de problèmes qui lui sont propres).
amon

1
Parce que les dépendances cycliques impliquent de structurer votre code de telle sorte que vous ne pouvez avoir l'un sans l'autre. En raison de la nature de la relation parent-enfant, elles doivent être des entités distinctes, sinon vous risquez d’avoir deux classes étroitement couplées qui pourraient aussi bien être une classe gigantesque unique avec une liste de tous les soins apportés à sa conception. Je ne vois pas en quoi le motif Observer est identique à parent-enfant, à part le fait qu'une classe a des références à plusieurs autres. Pour moi, parent-enfant est un parent fortement dépendant de l'enfant, mais pas l'inverse.
Neil

Je suis d'accord. Les événements sont le meilleur moyen de gérer la relation parent-enfant dans ce cas. C’est le modèle que j’utilise très souvent et qui rend le code très facile à gérer, au lieu d’avoir à se soucier de ce que la classe enfant fait au parent par le biais de la référence.
Eternal21

@Neil: Les dépendances mutuelles d' instances d' objets font naturellement partie de nombreux modèles de données. Dans une simulation mécanique d'une voiture, différentes parties de la voiture devront se transmettre des efforts; cela est généralement mieux géré si toutes les parties de la voiture considèrent le moteur de simulation lui-même comme un objet "parent", plutôt que d'avoir des dépendances cycliques entre tous les composants, mais si les composants sont capables de répondre à des stimuli extérieurs au parent ils auront besoin d'un moyen d'avertir le parent si de tels stimuli ont des effets que le parent doit connaître.
Supercat

2
@Neil: si le domaine inclut des objets de forêt avec des dépendances de données cycliques irréductibles, tout modèle du domaine le fera également. Dans de nombreux cas, cela impliquera en outre que la forêt se comportera comme un seul objet géant, qu'on le veuille ou non . Le modèle d'agrégats permet de concentrer la complexité de la forêt dans un objet de classe unique appelé racine d'agrégat. En fonction de la complexité du domaine modélisé, cette racine agrégée peut devenir un peu volumineuse et difficile à manier, mais si la complexité est inévitable (comme c'est le cas avec certains domaines), il vaut mieux ...
supercat

10

Je pense que votre option 3 pourrait être la plus propre. Tu as écrit.

Inconvénient: l'enfant est créé sans référence parent.

Je ne vois pas cela comme un inconvénient. En fait, la conception de votre programme peut tirer parti d’objets enfants pouvant être créés sans parent au préalable. Par exemple, cela peut faciliter beaucoup le test des enfants en isolation. Si un utilisateur de votre modèle oublie d’ajouter un enfant à son parent et appelle une méthode qui attend la propriété parent initialisée dans votre classe enfant, il obtient alors une exception null ref, ce qui est exactement ce que vous souhaitez: un crash précoce pour une erreur. usage.

Et si vous pensez qu'un enfant a besoin que son attribut parent soit initialisé dans le constructeur dans toutes les circonstances pour des raisons techniques, utilisez quelque chose comme un "objet parent null" comme valeur par défaut (bien que cela risque de masquer des erreurs).


Si un objet enfant ne peut rien faire d'utile sans parent, le démarrage d'un objet enfant sans parent nécessitera une SetParentméthode permettant de modifier la filiation d'un enfant existant (ce qui peut s'avérer difficile / ou absurde) ou ne sera appelable qu'une fois. Modéliser de telles situations sous forme d'agrégats (comme le suggère Mike Brown) peut être beaucoup mieux que d'avoir des enfants qui commencent sans parents.
Supercat

Si un cas d'utilisation appelle quelque chose, même une fois, la conception doit en tenir compte. Ensuite, il est facile de limiter cette capacité à des circonstances spéciales. L'ajout d'une telle capacité après est cependant généralement impossible. La meilleure solution est l'option 3. Probablement avec un troisième objet, "Relation" entre le parent et l'enfant, tel que [Parent] ---> [Relation (le parent possède l'enfant)] <--- [Enfant]. Cela permet également plusieurs instances [Relation] telles que [Enfant] ---> [Relation (l'enfant appartient au parent)] <--- [Parent].
DocSalvager

3

Rien n'empêche une forte cohésion entre deux classes couramment utilisées ensemble (par exemple, Order et LineItem se référencent généralement). Toutefois, dans ces cas, j’ai tendance à adhérer aux règles de conception gérées par le domaine et à les modéliser sous la forme d’un agrégat, le parent étant la racine de l’agrégat. Cela nous indique que le RA est responsable de la durée de vie de tous les objets de son agrégat.

Cela ressemble donc beaucoup à votre scénario quatre où le parent expose une méthode pour créer ses enfants en acceptant tous les paramètres nécessaires pour initialiser correctement les enfants et les ajouter à la collection.


1
J'utiliserais une définition légèrement plus souple de l'agrégat, qui permettrait l'existence de références externes dans des parties de l'agrégat autres que la racine, à condition que - du point de vue d'un observateur extérieur - le comportement soit cohérent. chaque partie de l'agrégat ne contenant qu'une référence à la racine et à aucune autre partie. Selon moi, le principe clé est que chaque objet mutable doit avoir un propriétaire ; un agrégat est une collection d'objets qui appartiennent tous à un seul objet (la "racine d'agrégat"), qui doit connaître toutes les références existant dans ses parties.
Supercat

3

Je suggérerais d'avoir des objets "fabrique enfant" qui sont transmis à une méthode parent qui crée un enfant (à l'aide de l'objet "fabrique enfant"), l'attache et renvoie une vue. L'objet enfant lui-même ne sera jamais exposé en dehors du parent. Cette approche peut bien fonctionner pour des choses comme les simulations. Dans une simulation électronique, un objet "d'usine enfant" particulier peut représenter les spécifications d'un type de transistor; un autre pourrait représenter les spécifications d'une résistance; un circuit nécessitant deux transistors et quatre résistances peut être créé avec un code tel que:

var q2N3904 = new TransistorSpec(TransistorType.NPN, 0.691, 40);
var idealResistor4K7 = new IdealResistorSpec(4700.0);
var idealResistor47K = new IdealResistorSpec(47000.0);

var Q1 = Circuit.AddComponent(q2N3904);
var Q2 = Circuit.AddComponent(q2N3904);
var R1 = Circuit.AddComponent(idealResistor4K7);
var R2 = Circuit.AddComponent(idealResistor4K7);
var R3 = Circuit.AddComponent(idealResistor47K);
var R4 = Circuit.AddComponent(idealResistor47K);

Notez que le simulateur n'a pas besoin de conserver de AddComponentréférence à l'objet créé par l' enfant et ne renvoie pas de référence à l'objet créé et détenu par le simulateur, mais plutôt à un objet représentant une vue. Si la AddComponentméthode est générique, l'objet de la vue pourrait inclure des fonctions spécifiques au composant, mais n'exposerait pas les membres que le parent utilise pour gérer la pièce jointe.


2

Grande liste. Je ne sais pas quelle méthode est la "meilleure" mais en voici une pour trouver les méthodes les plus expressives.

Commencez avec les classes parents et enfants les plus simples possibles. Écrivez votre code avec ceux-ci. Une fois que vous avez remarqué la duplication de code que vous pouvez nommer, vous l'insérez dans une méthode.

Peut-être que vous obtenez addChild(). Peut-être que vous obtenez quelque chose comme addChildren(List<Child>)ou addChildrenNamed(List<String>)ou loadChildrenFrom(String)ou newTwins(String, String)ou Child.replicate(int).

Si votre problème est vraiment de forcer une relation un-à-plusieurs, vous devriez peut-être

  • forcer dans les setters, ce qui peut créer des clauses de confusion ou de rejet
  • vous supprimez les setters et créez des méthodes spéciales de copie ou de déplacement - expressives et compréhensibles

Ce n'est pas une réponse, mais j'espère que vous en trouverez une en lisant ceci.


0

J'apprécie que le fait d'avoir des liens d'un enfant à l'autre de ses parents présente les inconvénients mentionnés ci-dessus.

Cependant, pour de nombreux scénarios, les solutions de contournement avec des événements et d'autres mécanismes "déconnectés" apportent également leurs propres complexités et des lignes de code supplémentaires.

Par exemple, le fait de lever un événement de Child pour qu'il soit reçu par son parent liera les deux ensemble, bien que de manière lâche.

Peut-être que pour de nombreux scénarios, tous les développeurs savent ce que signifie une propriété Child.Parent. Pour la majorité des systèmes sur lesquels j'ai travaillé, cela a très bien fonctionné. Plus d'ingénierie peut prendre beaucoup de temps et…. Déroutant!

Avoir une méthode Parent.AttachChild () qui effectue tout le travail nécessaire pour lier l’enfant à son parent. Tout le monde sait ce que cela signifie

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.