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


35

Sto sviluppando un modello a oggetti che ha molte classi genitore / figlio diverse. Ogni oggetto figlio ha un riferimento al suo oggetto padre. Mi viene in mente (e ho provato) diversi modi per inizializzare il riferimento principale, ma trovo significativi svantaggi per ogni approccio. Dati gli approcci descritti di seguito quale è il migliore ... o cosa è ancora meglio.

Non mi assicurerò che il codice riportato di seguito venga compilato, quindi prova a vedere la mia intenzione se il codice non è sintatticamente corretto.

Nota che alcuni dei costruttori della mia classe figlio accettano parametri (diversi dai genitori) anche se non ne mostro sempre nessuno.

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

    Unico inconveniente: l' impostazione del genitore è un processo in due fasi per il consumatore.

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

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

    var child = new Child(parent1);
    parent2.Children.Add(child);
  2. Parent verifica che il chiamante aggiunga child al parent 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);
      }
    }

    Unico inconveniente: il chiamante ha ancora un processo in due passaggi per impostare il genitore.

    Unico inconveniente: controllo del runtime : riduce le prestazioni e aggiunge il codice a ogni add / setter.

  3. Il genitore imposta il riferimento del genitore del figlio (su se stesso) quando il figlio viene aggiunto / assegnato a un genitore. Il setter principale è 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);
      }
    }

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

  4. Parent espone metodi di aggiunta di fabbrica in modo che un figlio abbia sempre un riferimento padre. Il ctor del bambino è interno. Il setter parent è 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;
      }
    }

    Unico inconveniente: non è possibile utilizzare la sintassi di inizializzazione come new Child(){prop = value}. Invece devi fare:

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

    Svantaggio: duplicare i parametri del costruttore figlio nei metodi add-factory.

    Unico inconveniente: non è possibile utilizzare un setter proprietà per un figlio singleton. Sembra zoppo che ho bisogno di un metodo per impostare il valore ma fornire l'accesso in lettura tramite un getter di proprietà. È sbilenco.

  5. Il figlio si aggiunge al genitore a cui fa riferimento il suo costruttore. Child ctor è pubblico. Nessun accesso pubblico aggiuntivo 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;} }
    }

    Unico inconveniente: chiamare la sintassi non è eccezionale. Normalmente si chiama un addmetodo sul genitore invece di creare 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é semplicemente creare un oggetto come questo:

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

...

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


14
La migliore pratica è che i bambini non dovrebbero conoscere i loro genitori.
Telastyn,


2
@Telastyn Non posso fare a meno di leggerlo come una lingua sulla guancia, ed è divertente. Anche completamente morto di precisione. Steven, il termine da esaminare è "aciclico" in quanto c'è molta letteratura là fuori sul perché dovresti rendere i grafici aciclici, se possibile.
Jimmy Hoffa,

10
@Telastyn dovresti provare a usare quel commento su parenting.stackexchange
Fabio Marcolini,

2
Hmm. Non so come spostare un post (non vedi un controllo flag). Ho ripubblicato ai programmatori poiché qualcuno mi ha detto che apparteneva lì.
Steven Broshar,

Risposte:


19

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

Esistono modi per trasferire messaggi da figlio a genitore attraverso eventi. In questo modo il genitore, al momento dell'aggiunta, deve semplicemente registrarsi a un evento che il figlio innesca, senza che il figlio debba conoscere direttamente il genitore. Dopotutto, questo è probabilmente l'uso previsto di un bambino che conosce il suo genitore, al fine di essere in grado di utilizzare il genitore in qualche modo. Tranne il fatto che non vorresti che il bambino svolgesse il lavoro di genitore, quindi in realtà quello che vuoi fare è semplicemente dire al genitore che è successo qualcosa. Pertanto, ciò che devi gestire è un evento sul figlio, di cui il genitore può trarre vantaggio.

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

Spero che sia d'aiuto!


6
" Stare lontano da qualsiasi scenario che richiede necessariamente al bambino di conoscere il genitore " - perché? La tua risposta dipende dal presupposto che i grafici a oggetti circolari siano una cattiva idea. Anche se a volte questo è il caso (ad esempio quando si gestisce la memoria tramite un conteggio dei riferimenti ingenuo - non è il caso in C #), non è una cosa negativa in generale. In particolare, il modello di osservatore (che viene spesso utilizzato per inviare eventi) coinvolge l'osservabile ( Child) mantenendo una serie di osservatori ( Parent), che reintroduce la circolarità in modo arretrato (e introduce una serie di problemi propri).
Amon,

1
Perché le dipendenze cicliche significano strutturare il tuo codice in modo tale da non poterne avere uno senza l'altro. Per natura della relazione genitore-figlio, dovrebbero essere entità separate oppure si rischia di avere due classi strettamente accoppiate che potrebbero anche essere una singola classe gigantesca con un elenco per tutte le cure accurate messe nel suo design. Non riesco a vedere come il modello Observer sia uguale a genitore-figlio diverso dal fatto che una classe abbia riferimenti ad altre. Per me genitore-figlio è un genitore che ha una forte dipendenza dal figlio, ma non viceversa.
Neil,

Sono d'accordo. In questo caso, gli eventi sono il modo migliore per gestire le relazioni genitore-figlio. È lo schema che uso molto spesso e rende il codice molto facile da mantenere, invece di doversi preoccupare di ciò che la classe figlio sta facendo al genitore attraverso il riferimento.
Eterno

@Neil: le dipendenze reciproche delle istanze di oggetti formano una parte naturale di molti modelli di dati. In una simulazione meccanica di un'auto, le diverse parti dell'auto dovranno trasmettere forze l'un l'altro; questo è in genere meglio gestito facendo in modo che tutte le parti dell'auto considerino il motore di simulazione stesso come un oggetto "genitore", piuttosto che avere dipendenze cicliche tra tutti i componenti, ma se i componenti sono in grado di rispondere a qualsiasi stimolo esterno al genitore avranno bisogno di un modo per informare il genitore se tali stimoli hanno effetti che il genitore deve conoscere.
Supercat,

2
@Neil: se il dominio include oggetti foresta con dipendenze cicliche irriducibili dei dati, lo farà anche qualsiasi modello del dominio. In molti casi ciò implica ulteriormente che la foresta si comporterà come un singolo oggetto gigante, indipendentemente dal fatto che uno lo voglia o no . Il modello aggregato serve a concentrare la complessità della foresta in un oggetto a singola classe chiamato radice aggregata. A seconda della complessità del dominio che viene modellato, quella radice aggregata può diventare un po 'grande e ingombrante, ma se la complessità è inevitabile (come nel caso di alcuni domini) è meglio ...
supercat

10

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

Unico inconveniente: il figlio viene creato senza un riferimento principale.

Non lo vedo come un aspetto negativo. In effetti, il design del tuo programma può beneficiare di oggetti figlio che possono essere creati senza un genitore prima. Ad esempio, può rendere molto più semplice il test dei bambini isolati. Se un utente del tuo modello dimentica di aggiungere un figlio al suo genitore e chiama un metodo che prevede la proprietà del genitore inizializzata nella tua classe figlio, allora ottiene un'eccezione riferimento null - che è esattamente quello che vuoi: un arresto anticipato per un errore utilizzo.

E se pensi che un figlio abbia bisogno che il suo attributo parent sia inizializzato nel costruttore in tutte le circostanze per ragioni tecniche, usa qualcosa come un "oggetto parent null" come valore predefinito (anche se questo ha il rischio di mascherare errori).


Se un oggetto figlio non può fare nulla di utile senza un genitore, l'avvio di un oggetto figlio senza un genitore richiederà che abbia un SetParentmetodo che dovrà supportare la modifica della parentela di un figlio esistente (che potrebbe essere difficile da fare e / o senza senso) o sarà richiamabile una sola volta. Modellare situazioni come gli aggregati (come suggerisce Mike Brown) può essere molto meglio che avere figli che iniziano senza genitori.
supercat

Se un caso d'uso richiede qualcosa anche una volta, il design deve tenerlo sempre. Quindi, limitare tale capacità a circostanze speciali è facile. L'aggiunta di tale capacità in seguito, tuttavia, è generalmente impossibile. La soluzione migliore è l'opzione 3. Probabilmente con un terzo oggetto "Relazione" tra genitore e figlio in modo che [Genitore] ---> [Relazione (Il genitore possiede un figlio)] <--- [Figlio]. Ciò consente anche più istanze [Relazione] come [Figlio] ---> [Relazione (Il figlio appartiene al genitore)] <--- [Padre].
DocSalvager

3

Non c'è nulla che impedisca un'elevata coesione tra due classi che sono usate comunemente insieme (ad esempio un Ordine e LineItem si faranno comunemente riferimento). Tuttavia, in questi casi, tendo ad aderire alle regole di progettazione guidata dal dominio e modellarle come aggregate con il genitore che è la radice aggregata. Questo ci dice che l'AR è responsabile della vita di tutti gli oggetti nel suo aggregato.

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


1
Vorrei usare una definizione leggermente più libera di aggregato, che consentirebbe l'esistenza di riferimenti esterni in parti dell'aggregato diverse dalla radice, a condizione che - dal punto di vista di un osservatore esterno - il comportamento sarebbe coerente con ogni parte dell'aggregato che detiene solo un riferimento alla radice e non a qualsiasi altra parte. A mio avviso, il principio chiave è che ogni oggetto mutevole dovrebbe avere un solo proprietario ; un aggregato è una raccolta di oggetti che appartengono tutti a un singolo oggetto (la "radice aggregata"), che dovrebbe conoscere tutti i riferimenti esistenti alle sue parti.
supercat

3

Suggerirei di avere oggetti "figlio-fabbrica" ​​che sono passati a un metodo genitore che crea un figlio (usando l'oggetto "figlio-fabbrica"), lo collega 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 "factory child" potrebbe rappresentare le specifiche per un qualche tipo di transistor; un altro potrebbe rappresentare le specifiche per un resistore; un circuito che necessita di due transistor e quattro resistori potrebbe essere creato con un 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 conservare alcun riferimento all'oggetto figlio creatore e AddComponentnon restituirà un riferimento all'oggetto creato e tenuto dal simulatore, ma piuttosto un oggetto che rappresenta una vista. Se il AddComponentmetodo è generico, l'oggetto vista potrebbe includere funzioni specifiche del componente ma non esponerebbe i membri utilizzati dal genitore per gestire l'allegato.


2

Ottima lista. Non so quale sia il metodo "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. Dopo aver notato la duplicazione del codice che può essere nominata, la si inserisce in un metodo.

Forse hai capito 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

  • forzarlo nei setter che possono portare a confusione o clausole di lancio
  • rimuovi i setter e crei speciali metodi di copia o spostamento - che sono espressivi e comprensibili

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


0

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

Tuttavia, per molti scenari le soluzioni alternative con eventi e altre meccaniche "disconnesse" comportano anche complessità e righe di codice aggiuntive.

Ad esempio, sollevare un evento da Child che verrà ricevuto dal suo genitore vincolerà entrambi, anche se in modo allentato.

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

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.