web-dev-qa-db-fra.com

Quelle est la meilleure façon d'initialiser la référence d'un enfant à son parent?

Je développe un modèle d'objet qui a beaucoup de classes parent/enfant différentes. 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 parent, mais je trouve des inconvénients importants à chaque approche. Compte tenu des approches décrites ci-dessous, quelle est la meilleure ... ou ce qui est encore mieux.

Je ne vais pas m'assurer que le code ci-dessous se compile alors essayez de voir mon intention si le code n'est pas syntaxiquement correct.

Notez que certains de mes constructeurs de classes enfants acceptent des paramètres (autres que parent) même si je n'en montre 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: sujet aux erreurs. L'appelant peut ajouter un enfant à un parent différent de celui utilisé pour initialiser l'enfant.

    var child = new Child(parent1);
    parent2.Children.Add(child);
    
  2. Le parent vérifie que l'appelant ajoute un 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/définition.

  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 setter 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 certaine initialisation/validation doit être effectuée dans le setter 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 les méthodes d'ajout d'usine afin qu'un enfant ait toujours une référence parent. Le ctor enfant est interne. Le parent setter 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 la syntaxe d'initialisation telle que new Child(){prop = value}. Au lieu de cela, il faut 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 setter de propriétés 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. L'enfant s'ajoute au parent référencé dans son constructeur. Le ctor d'enfant est public. Aucun accès d'ajout public depuis le 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 méthode add sur le parent au lieu de simplement créer un objet comme celui-ci:

    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 ne permet pas certaines questions subjectives et clairement c'est une question subjective. Mais, c'est peut-être une bonne question subjective.

36
Steven Broshar

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

Il existe des moyens de transmettre des messages de l'enfant au parent par le biais d'événements. De cette façon, le parent, une fois ajouté, doit simplement s'inscrire à un événement que l'enfant déclenche, sans que l'enfant ait à connaître directement le parent. Après tout, il s'agit probablement de l'usage prévu d'un enfant connaissant son parent, afin de pouvoir utiliser le parent dans une certaine mesure. Sauf que vous ne voudriez pas que l'enfant exécute le travail du parent, alors vraiment ce que vous voulez faire est simplement de dire au parent que quelque chose s'est passé. Par conséquent, ce que vous devez gérer est un événement sur l'enfant, dont le parent peut profiter.

Ce modèle évolue également très bien si cet événement devient 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 le parent dans votre classe enfant qui ne couple que les deux classes encore plus. La refactorisation de ces classes par la suite prend du temps et peut facilement créer des bogues dans votre programme.

J'espère que cela pourra aider!

19
Neil

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 bénéficier d'objets enfants qui peuvent être créés sans parent au préalable. Par exemple, cela peut faciliter le test des enfants de manière isolée. 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, alors il obtient une exception de référence nulle - ce qui est exactement ce que vous voulez: 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 en toutes circonstances pour des raisons techniques, utilisez quelque chose comme un "objet parent nul" comme valeur par défaut (même si cela risque de masquer les erreurs).

10
Doc Brown

Je suggère d'avoir des objets "child-factory" qui sont passés à une méthode parent qui crée un enfant (en utilisant l'objet "child factory"), l'attache et retourne une vue. L'objet enfant lui-même ne sera jamais exposé en dehors du parent. Cette approche peut très bien fonctionner pour des choses comme les simulations. Dans une simulation électronique, un objet particulier "usine enfant" peut représenter les spécifications d'un transistor; un autre pourrait représenter les spécifications d'une résistance; un circuit qui nécessite deux transistors et quatre résistances pourrait être créé avec un code comme:

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 référence à l'objet créateur enfant et AddComponent ne renverra pas de référence à l'objet créé et détenu par le simulateur, mais plutôt un objet qui représente une vue. Si la méthode AddComponent est générique, l'objet view pourrait inclure des fonctions spécifiques au composant mais pas exposerait les membres que le parent utilise pour gérer la pièce jointe.

3
supercat

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

Ce serait donc plus comme 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 en l'ajoutant à la collection.

3
Michael Brown

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 la classe parent et enfant la plus simple possible. Écrivez votre code avec ceux-ci. Une fois que vous remarquez une duplication de code qui peut être nommée, vous la mettez dans une méthode.

Vous obtenez peut-être addChild(). Vous obtenez peut-être 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

  • le forcer dans les setters qui peut conduire à la confusion ou à des clauses de lancer
  • vous supprimez les setters et créez des méthodes spéciales de copie ou de déplacement - ce qui est expressif et compréhensible

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

2
User

J'apprécie qu'avoir des liens de l'enfant au parent avait ses inconvénients comme indiqué ci-dessus.

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

Par exemple, le fait d'élever un événement de l'Enfant à être reçu par son Parent liera les deux ensemble, mais de manière lâche.

Peut-être que pour de nombreux scénarios, tous les développeurs savent clairement 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é. La suringénierie peut être longue 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 clairement ce que cela signifie

0
Rax