¿Cuál es la mejor manera de inicializar la referencia de un niño a su padre?

35

Estoy desarrollando un modelo de objetos que tiene muchas clases diferentes de padres / hijos. Cada objeto hijo tiene una referencia a su objeto padre. Puedo pensar (y he intentado) varias formas de inicializar la referencia principal, pero encuentro inconvenientes significativos para cada enfoque. Dados los enfoques que se describen a continuación, cuál es el mejor ... o qué es aún mejor.

No voy a asegurarme de que el siguiente código se compila, así que intente ver mi intención si el código no es sintácticamente correcto.

Tenga en cuenta que algunos de mis constructores de clases secundarias toman parámetros (que no sean los primarios) aunque no siempre muestre ninguno.

  1. La persona que llama es responsable de establecer el padre y agregar al mismo padre.

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

    Desventaja: establecer el padre es un proceso de dos pasos para el consumidor.

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

    Desventaja: propenso a errores. La persona que llama puede agregar un niño a un padre diferente al que se usó para inicializar el niño.

    var child = new Child(parent1);
    parent2.Children.Add(child);
  2. Parent verifica que la persona que llama agrega child a parent para la cual se inicializó.

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

    Desventaja: la persona que llama aún tiene un proceso de dos pasos para configurar a los padres.

    Desventaja: comprobación de tiempo de ejecución: reduce el rendimiento y agrega código a cada agregado / configurador.

  3. El padre establece la referencia del padre del niño (a sí mismo) cuando el niño se agrega / asigna a un padre. El setter primario es 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);
      }
    }

    Desventaja: el hijo se crea sin una referencia principal. A veces, la inicialización / validación requiere el padre, lo que significa que se debe realizar alguna inicialización / validación en el setter primario del niño. El código puede complicarse. Sería mucho más fácil implementar el elemento secundario si siempre tuviera su referencia principal.

  4. Principal expone métodos de adición de fábrica para que un hijo siempre tenga una referencia principal. El niño ctor es interno. El setter principal es privado.

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

    Desventaja: no se puede usar la sintaxis de inicialización como new Child(){prop = value}. En cambio tiene que hacer:

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

    Desventaja: tiene que duplicar los parámetros del constructor hijo en los métodos add-factory.

    Desventaja: no se puede usar un establecedor de propiedades para un hijo único. Parece lamentable que necesite un método para establecer el valor pero proporcionar acceso de lectura a través de un captador de propiedades. Es desigual.

  5. El niño se agrega al padre al que se hace referencia en su constructor. El niño ctor es público. No hay acceso de agregar público desde el padre.

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

    Desventaja: la sintaxis de llamadas no es excelente. Normalmente uno llama a un addmétodo en el padre en lugar de simplemente crear un objeto como este:

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

    Y establece una propiedad en lugar de simplemente crear un objeto como este:

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

...

Acabo de enterarme de que SE no permite algunas preguntas subjetivas y claramente esta es una pregunta subjetiva. Pero, tal vez es una buena pregunta subjetiva.

Steven Broshar
fuente
14
La mejor práctica es que los niños no deben saber sobre sus padres.
Telastyn
44
por favor, no publique mensajes
mosquito
2
@Telastyn No puedo evitar leer eso como una lengua en la mejilla, y es muy gracioso. También completamente muerto, sangriento y preciso. Steven, el término a considerar es "acíclico", ya que hay mucha literatura sobre por qué deberías hacer gráficos acíclicos si es posible.
Jimmy Hoffa
10
@Telastyn, deberías intentar usar ese comentario en parenting.stackexchange
Fabio Marcolini el
2
Hmm No sé cómo mover una publicación (no veo un control de bandera). Volví a publicar en los programadores ya que alguien me dijo que pertenecía allí.
Steven Broshar

Respuestas:

19

Me mantendría alejado de cualquier escenario que necesariamente requiera que el niño sepa sobre el padre.

Hay formas de pasar mensajes del niño al padre a través de eventos. De esta manera, el padre, al agregarlo, simplemente tiene que registrarse en un evento que el niño desencadena, sin que el niño tenga que saber sobre el padre directamente. Después de todo, este es probablemente el uso previsto de un niño que conoce a su padre, para poder usar el padre con algún efecto. Excepto que no querrías que el niño realice el trabajo del padre, así que realmente lo que quieres hacer es simplemente decirle al padre que algo ha sucedido. Por lo tanto, lo que necesita manejar es un evento en el niño, del cual el padre puede aprovechar.

Este patrón también se escala muy bien si este evento se vuelve útil para otras clases. Quizás es un poco exagerado, pero también evita que te dispares en el pie más tarde, ya que es tentador querer usar a los padres en la clase de tu hijo, que solo combina las dos clases aún más. Posteriormente, la refactorización de tales clases lleva mucho tiempo y puede crear fácilmente errores en su programa.

¡Espero que ayude!

Neil
fuente
66
" Mantenerse alejado de cualquier escenario que necesariamente requiera que el niño sepa sobre el padre " - ¿por qué? Su respuesta depende del supuesto de que los gráficos de objetos circulares son una mala idea. Si bien este es a veces el caso (por ejemplo, cuando se administra la memoria mediante un recuento de referencias ingenuo, no es el caso en C #), en general no es algo malo. En particular, el patrón de observador (que a menudo se utiliza para enviar eventos) implica que el observable ( Child) mantenga un conjunto de observadores ( Parent), que reintroduce la circularidad de forma inversa (e introduce una serie de problemas propios).
amon
1
Debido a que las dependencias cíclicas significan estructurar su código de tal manera que no pueda tener uno sin el otro. Por la naturaleza de la relación padre-hijo, deberían ser entidades separadas o de lo contrario corre el riesgo de tener dos clases estrechamente acopladas que también podrían ser una sola clase gigantesca con una lista de todos los cuidados cuidadosos que se ponen en su diseño. No veo cómo el patrón de Observador es el mismo que padre-hijo, aparte del hecho de que una clase tiene referencias a varias otras. Para mí, padre-hijo es un padre que tiene una fuerte dependencia del niño, pero no a la inversa.
Neil
Estoy de acuerdo. Los eventos son la mejor manera de manejar la relación padre-hijo en este caso. Es el patrón que uso muy a menudo, y hace que el código sea muy fácil de mantener, en lugar de tener que preocuparse por lo que la clase secundaria le está haciendo al padre a través de la referencia.
Eternal21
@Neil: las dependencias mutuas de instancias de objetos forman una parte natural de muchos modelos de datos. En una simulación mecánica de un automóvil, diferentes partes del automóvil necesitarán transmitir fuerzas entre sí; Por lo general, esto se maneja mejor haciendo que todas las partes del automóvil consideren el motor de simulación en sí mismo como un objeto "principal", que teniendo dependencias cíclicas entre todos los componentes, pero si los componentes pueden responder a cualquier estímulo externo. necesitarán una forma de notificar a los padres si tales estímulos tienen algún efecto que los padres deben conocer.
supercat
2
@Neil: si el dominio incluye objetos de bosque con dependencias de datos cíclicos irreducibles, cualquier modelo del dominio también lo hará. En muchos casos, esto implicará además que el bosque se comportará como un único objeto gigante, lo desee o no . El patrón agregado sirve para concentrar la complejidad del bosque en un único objeto de clase llamado Raíz Agregada. Dependiendo de la complejidad del dominio que se está modelando, esa raíz agregada puede ser algo grande y difícil de manejar, pero si la complejidad es inevitable (como es el caso con algunos dominios) es mejor ...
supercat
10

Creo que su opción 3 podría ser la más limpia. Tu escribiste.

Desventaja: el hijo se crea sin una referencia principal.

No veo eso como un inconveniente. De hecho, el diseño de su programa puede beneficiarse de los objetos secundarios que se pueden crear sin un padre primero. Por ejemplo, puede hacer que las pruebas de niños en forma aislada sean mucho más fáciles. Si un usuario de su modelo olvida agregar un elemento secundario a su elemento primario y llama a un método que espera que la propiedad principal se inicialice en su clase secundaria, obtiene una excepción de referencia nula, que es exactamente lo que desea: un bloqueo anticipado por un error uso.

Y si cree que un elemento secundario necesita que su atributo principal se inicialice en el constructor en todas las circunstancias por razones técnicas, use algo como un "objeto primario nulo" como valor predeterminado (aunque esto tiene el riesgo de enmascarar errores).

Doc Brown
fuente
Si un objeto hijo no puede hacer nada útil sin un padre, hacer que un objeto hijo comience sin un padre requerirá que tenga un SetParentmétodo que deba admitir el cambio de la paternidad de un hijo existente (que puede ser difícil de hacer y / o sin sentido) o solo se podrá llamar una vez. Modelar situaciones como los agregados (como sugiere Mike Brown) puede ser mucho mejor que hacer que los niños comiencen sin padres.
supercat
Si un caso de uso requiere algo, incluso una vez, el diseño debe permitirlo siempre. Entonces, restringir esa capacidad a circunstancias especiales es fácil. Sin embargo, agregar esa capacidad después suele ser imposible. La mejor solución es la Opción 3. Probablemente con un tercer objeto, "Relación" entre el padre y el hijo, de modo que [Padre] ---> [Relación (El padre posee al Niño)] <--- [Niño]. Esto también permite múltiples instancias de [Relación] como [Niño] ---> [Relación (El niño es propiedad de los padres)] <--- [Padre].
DocSalvager
3

No hay nada que evite la alta cohesión entre dos clases que se usan comúnmente juntas (por ejemplo, un Order y LineItem se referenciarán entre sí). Sin embargo, en estos casos, tiendo a adherirme a las reglas de diseño impulsado por dominio y modelarlas como un agregado con el padre como la raíz agregada. Esto nos dice que el AR es responsable de la vida útil de todos los objetos en su conjunto.

Por lo tanto, sería más parecido a su escenario cuatro donde el padre expone un método para crear sus hijos aceptando los parámetros necesarios para inicializar adecuadamente los hijos y agregarlos a la colección.

Michael Brown
fuente
1
Usaría una definición de agregado ligeramente más flexible, que permitiría la existencia de referencias externas en partes del agregado que no sean la raíz, siempre que, desde el punto de vista de un observador externo, el comportamiento sea consistente cada parte del agregado contiene solo una referencia a la raíz y no a ninguna otra parte. En mi opinión, el principio clave es que cada objeto mutable debe tener un propietario ; un agregado es una colección de objetos que son propiedad de un solo objeto (la "raíz agregada"), que debe conocer todas las referencias que existen a sus partes.
supercat
3

Sugeriría tener objetos "child-factory" que se pasan a un método padre que crea un hijo (usando el objeto "child factory"), lo adjunta y devuelve una vista. El objeto hijo en sí nunca se expondrá fuera del padre. Este enfoque puede funcionar bien para cosas como las simulaciones. En una simulación electrónica, un objeto particular de "fábrica secundaria" podría representar las especificaciones para algún tipo de transistor; otro podría representar las especificaciones para una resistencia; Un circuito que necesita dos transistores y cuatro resistencias podría crearse con un código como:

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

Tenga en cuenta que el simulador no necesita retener ninguna referencia al objeto hijo-creador, y AddComponentno devolverá una referencia al objeto creado y mantenido por el simulador, sino más bien un objeto que representa una vista. Si el AddComponentmétodo es genérico, el objeto de vista podría incluir funciones específicas del componente, pero no expondría a los miembros que el padre utiliza para administrar el archivo adjunto.

Super gato
fuente
2

Gran listado No sé qué método es el "mejor", pero aquí hay uno para encontrar los métodos más expresivos.

Comience con la clase padre e hijo más simple posible. Escribe tu código con esos. Una vez que note la duplicación de código que se puede nombrar, lo pone en un método.

Quizás lo consigas addChild(). Tal vez obtienes algo como addChildren(List<Child>)o addChildrenNamed(List<String>)o loadChildrenFrom(String)o newTwins(String, String)o Child.replicate(int).

Si su problema es realmente sobre forzar una relación uno a muchos, tal vez debería

  • forzarlo en los setters lo que puede generar confusión o cláusulas de lanzamiento
  • eliminas los setters y creas métodos especiales de copia o movimiento, lo cual es expresivo y comprensible

Esta no es una respuesta, pero espero que encuentres una mientras lees esto.

Usuario
fuente
0

Aprecio que tener vínculos de niño a padre tuviera sus desventajas, como se señaló anteriormente.

Sin embargo, para muchos escenarios, las soluciones alternativas con eventos y otras mecánicas "desconectadas" también traen sus propias complejidades y líneas de código adicionales.

Por ejemplo, generar un evento de Child para que sea recibido por su Parent unirá a ambos, aunque de forma flexible.

Quizás para muchos escenarios es claro para todos los desarrolladores lo que significa una propiedad Child.Parent. Para la mayoría de los sistemas en los que trabajé, esto ha funcionado bien. El exceso de ingeniería puede llevar mucho tiempo y ... ¡Confuso!

Tenga un método Parent.AttachChild () que realice todo el trabajo necesario para vincular el elemento secundario a su elemento primario. Todos tienen claro lo que esto "significa"

Rax
fuente