Qual è il modo migliore per inizializzare il riferimento di un figlio al suo genitore?

32

Sto sviluppando un modello a oggetti che ha molte diverse classi genitore / figlio. Ogni oggetto figlio ha un riferimento al suo oggetto genitore. Posso pensare (e ho provato) diversi modi per inizializzare il riferimento genitore, ma trovo degli svantaggi significativi per ciascun approccio. Considerati gli approcci descritti qui di seguito che è il migliore ... o ciò che è ancora meglio.

Non ho intenzione di assicurarmi che il codice qui sotto sia compilato, quindi prova a vedere la mia intenzione se il codice non è sintatticamente corretto.

Si noti che alcuni dei miei costruttori di classi figlio accettano parametri (diversi dai genitori) anche se non ne mostro sempre.

  1. Il chiamante è responsabile dell'impostazione del genitore e dell'aggiunta allo stesso genitore.

    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;} }
    }
    

    Il lato negativo: l'impostazione del genitore è un processo in due fasi per il consumatore.

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

    Lato negativo: soggetto a errori. Il chiamante può aggiungere un figlio a un genitore diverso da quello utilizzato per inizializzare il bambino.

    var child = new Child(parent1);
    parent2.Children.Add(child);
    
  2. Il genitore verifica che il chiamante aggiunga un figlio al genitore per il quale è stato inizializzato.

    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);
      }
    }
    

    Lato negativo: il chiamante ha ancora una procedura in due passaggi per l'impostazione genitore.

    Lato negativo: controllo runtime - riduce le prestazioni e aggiunge codice a ogni add / setter.

  3. Il genitore imposta il riferimento genitore del figlio (a se stesso) quando il bambino viene aggiunto / assegnato a un genitore. Il setter genitore è interno.

    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);
      }
    }
    

    Lato negativo: il figlio viene creato senza un riferimento genitore. A volte l'inizializzazione / convalida richiede il genitore, il che significa che deve essere eseguita un'inizializzazione / convalida nel setter del genitore del bambino. Il codice può diventare complicato. Sarebbe molto più semplice implementare il bambino se avesse sempre il suo riferimento genitore.

  4. Il genitore espone i metodi di aggiunta di fabbrica in modo che un bambino abbia sempre un riferimento genitore. Il bambino è interno. Il setter genitore è privato.

    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;
      }
    }
    

    Lato negativo: Impossibile utilizzare la sintassi di inizializzazione come new Child(){prop = value} . Invece devi fare:

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

    Lato negativo: Devi duplicare i parametri del costruttore figlio nei metodi add-factory.

    Lato negativo: Impossibile usare un setter di proprietà per un figlio singleton. Sembra che io abbia bisogno di un metodo per impostare il valore, ma fornire l'accesso in lettura tramite un getter di proprietà. È sbilenco.

  5. Child si aggiunge al genitore referenziato nel suo costruttore. Il bambino è pubblico. Nessun accesso pubblico da parte del genitore.

    //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;} }
    }
    

    Lato negativo: la sintassi di chiamata non è eccezionale. Normalmente si chiama un metodo add sul genitore invece di creare semplicemente un oggetto come questo:

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

    E imposta una proprietà anziché creare semplicemente un oggetto come questo:

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

...

Ho appena saputo che SE non consente alcune domande soggettive e chiaramente questa è una domanda soggettiva. Ma forse è una buona domanda soggettiva.

    
posta Steven Broshar 31.10.2014 - 16:15
fonte

6 risposte

17

Vorrei stare lontano da qualsiasi scenario che richiede necessariamente al bambino di conoscere il genitore.

Ci sono modi per passare messaggi da bambino a genitore attraverso eventi. In questo modo il genitore, una volta aggiunto, deve semplicemente registrarsi a un evento che il bambino innesca, senza che il bambino debba conoscere direttamente il genitore. Dopotutto, questo è probabilmente l'uso previsto di un bambino che conosce il suo genitore, per poter usare il genitore con qualche effetto. Tranne che tu non vorresti che il bambino eseguisse il lavoro del genitore, quindi veramente quello che vuoi fare è semplicemente dire al genitore che qualcosa è successo. Pertanto, ciò che devi gestire è un evento sul bambino, di cui il genitore può trarre vantaggio.

Questo modello si adatta molto bene se questo evento diventa utile ad altre classi. Forse è un po 'eccessivo, ma ti impedisce anche di spararti al piede più tardi, dal momento che diventa tentato di voler usare il genitore nella tua classe di figlio, che accoppia solo le due classi ancora di più. Refactoring di tali classi in seguito richiede molto tempo e può facilmente creare bug nel tuo programma.

Spero che ti aiuti!

    
risposta data 31.10.2014 - 17:13
fonte
9

Penso che la tua opzione 3 potrebbe essere la più pulita. Hai scritto.

Downside: The child is created without a parent reference.

Non lo vedo come un aspetto negativo. In effetti, il design del tuo programma può trarre beneficio da oggetti figlio che possono essere creati senza un genitore. Ad esempio, può rendere molto più facile testare i child in isolamento. Se un utente del tuo modello si dimentica di aggiungere un figlio al suo genitore e chiama un metodo che si aspetta che la proprietà genitoriale sia inizializzata nella classe figlia, allora ottiene un'eccezione di riferimento null - che è esattamente quello che vuoi: un arresto anticipato per un errore utilizzo.

Se pensate che un bambino abbia bisogno che il suo attributo genitore venga inizializzato nel costruttore in tutte le circostanze per ragioni tecniche, usate qualcosa come un "oggetto genitore null" come valore predefinito (anche se questo presenta il rischio di errori di mascheramento).

    
risposta data 31.10.2014 - 23:28
fonte
3

Non c'è nulla che impedisca un'elevata coesione tra due classi che vengono comunemente utilizzate insieme (ad esempio, un ordine e LineItem si riferiscono comunemente l'un l'altro). Tuttavia, in questi casi, tendo ad aderire alle regole di Domain Driven Design e modellandole come Aggregato con il genitore come Radice aggregata Questo ci dice che l'AR è responsabile della vita di tutti gli oggetti nel suo aggregato.

Quindi sarebbe più simile al tuo scenario quattro in cui il genitore espone un metodo per creare i suoi figli accettando tutti i parametri necessari per inizializzare correttamente i bambini e aggiungerlo alla collezione.

    
risposta data 31.10.2014 - 17:17
fonte
3

Suggerirei di avere oggetti "child factory" che vengono passati a un metodo genitore che crea un child (usando l'oggetto "factory child"), lo allega e restituisce una vista. L'oggetto figlio stesso non sarà mai esposto all'esterno del genitore. Questo approccio può funzionare bene per cose come le simulazioni. In una simulazione elettronica, un particolare oggetto "fabbrica di bambini" potrebbe rappresentare le specifiche per una sorta di transistor; un altro potrebbe rappresentare le specifiche per un resistore; un circuito che ha bisogno di due transistor e quattro resistori potrebbero essere creati con codice come:

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);

Si noti che il simulatore non deve mantenere alcun riferimento all'oggetto child-creator e AddComponent non restituirà un riferimento all'oggetto creato e tenuto dal simulatore, ma piuttosto un oggetto che rappresenta una vista. Se il metodo AddComponent è generico, l'oggetto vista potrebbe includere funzioni specifiche del componente ma non esporre i membri che il genitore usa per gestire l'allegato.

    
risposta data 04.12.2014 - 05:53
fonte
2

Grande elenco. Non so quale metodo sia il "migliore" ma qui è uno per trovare i metodi più espressivi.

Inizia con la classe genitore e figlio più semplice possibile. Scrivi il tuo codice con quelli. Una volta che si nota la duplicazione del codice che può essere denominata, l'hai inserita in un metodo.

Forse ottieni addChild() . Forse ottieni qualcosa come addChildren(List<Child>) o addChildrenNamed(List<String>) o loadChildrenFrom(String) o newTwins(String, String) o Child.replicate(int) .

Se il tuo problema è davvero quello di forzare una relazione uno-a-molti, forse dovresti

  • forzalo nei setter che potrebbero causare confusione o clausole di tiro
  • rimuovi i setter e crei metodi speciali di copia o spostamento, che sono espressivi e comprensibili

Questa non è una risposta, ma spero che ne trovi una durante la lettura.

    
risposta data 03.11.2014 - 21:01
fonte
0

Apprezzo che avere collegamenti da bambino a genitore abbia i suoi lati negativi, come notato sopra.

Tuttavia, per molti scenari le soluzioni alternative con eventi e altre meccaniche "disconnesse" portano anche le loro complessità e linee extra di codice.

Ad esempio, l'innalzamento di un evento da Child per essere ricevuto dal suo genitore si vincolerà entrambi, anche se in modo approssimativo.

Forse per molti scenari è chiaro a tutti gli sviluppatori che cosa significa una proprietà Child.Parent. Per la maggior parte dei sistemi su cui ho lavorato questo ha funzionato bene. L'ingegneria eccessiva può richiedere molto tempo e ... Confusione!

Avere un metodo Parent.AttachChild () che esegue tutto il lavoro necessario per legare il Bambino al genitore. Tutti sono chiari su cosa "significhi"

    
risposta data 04.09.2018 - 18:05
fonte

Leggi altre domande sui tag