Si A tiene B y B tiene la referencia de A, ¿es necesario corregir el diseño defectuoso? [duplicar]

7

Supongamos que tengo clase jefe y trabajador; Boss tiene un trabajador y Worker tiene una referencia de Boss:

Boss.h

#include "Worker.h"
class Boss{
public:
    Worker worker;
};

Trabajador.h

class Boss;
class Worker{
    Boss* boss;
};

En C ++, Worker no necesita que Boss compile, pero no me siento cómodo al ver aparecer la palabra "Boss" en Worker y la palabra "Worker" aparece al mismo tiempo en "Boss". Además, si este diseño se mueve a otro idioma que no tiene sistemas como .h y .cpp (por ejemplo: Java), se vuelve dependiente el uno del otro. Por lo tanto, mi problema es que si el código C ++ tiene tal patrón, incluso Worker no requiere que Boss compile, ¿sigue siendo necesario corregir el diseño defectuoso? Si es así, ¿cómo puedo solucionarlo?

    
pregunta ggrr 24.06.2016 - 05:02

5 respuestas

20

No hay nada que sea fundamentalmente defectuoso acerca de esta idea. Lo que tienes son dos relaciones. Boss posee uno o más Worker s. Y Worker tiene una referencia no propietaria a un Boss . El uso de un puntero en bruto sugiere que Worker no posee el puntero que almacena; es simplemente usarlo. Esto significa que no controla la vida útil de ese objeto.

No hay nada malo con tal relación per se. Todo depende de cómo se use.

Por ejemplo, si la referencia Boss que Worker almacena es la instancia de objeto Boss real que posee el Worker , entonces todo está bien. ¿Por qué? Debido a que presumiblemente, una instancia Worker no puede existir sin un Boss que la posee. Y como el Boss lo posee, esto garantiza que el Worker no sobrevivirá al Boss al que hace referencia.

Bueno ... un poco. Y aquí es donde empiezas a meterte en problemas potenciales.

Primero, según las reglas de construcción de C ++, Boss::worker se construye antes de la instancia Boss . Ahora, el constructor de Boss puede pasar un puntero de this al constructor de Worker . Pero Worker no puede usarlo , ya que el objeto Boss aún no se ha construido. Worker puede almacenarlo, pero no puede acceder a nada en él.

Del mismo modo, según las reglas de destrucción de C ++, Boss::worker se destruirá después de la instancia Boss propietaria. Por lo tanto, el destructor de Worker no puede usar de forma segura el puntero Boss , ya que apunta a un objeto cuya vida útil ha finalizado.

Estas limitaciones pueden llevar en algún momento a tener que usar una construcción de dos etapas. Es decir, recuperar Worker después de que Boss se haya construido completamente, para que pueda comunicarse con él durante la construcción. Pero incluso esto puede estar bien, particularmente si Worker no necesita hablar con Boss en su constructor.

Copiar también se convierte en un problema. O más al punto, los operadores de asignación se vuelven problemáticos. Si se pretende que Worker::boss apunte a la instancia específica Boss que lo posee, entonces nunca debe copiarlo. De hecho, si ese es el caso, debe declarar Worker::boss como un puntero constante, para que el puntero se establezca en el constructor y en ninguna otra parte.

El punto de este ejemplo es que la idea que has definido no es, en sí misma, irrazonable. Tiene un significado bien definido y puedes usarlo para muchas cosas. Solo tienes que pensar sobre lo que estás haciendo.

    
respondido por el Nicol Bolas 24.06.2016 - 05:26
2

Intentaré refinar un punto planteado en la respuesta de gnasher729 .

Debemos hacer una distinción cuidadosa entre:

  • Propiedad - Boss posee un Worker . Si se destruye Boss , el Worker que posee debe destruirse simultáneamente.
  • existencia independiente (entidad)
    • Pueden existir Boss y Worker con una vida útil individual. Si uno es destruido, el otro no es necesariamente destruido.
    • Dicho esto, un Boss debe poder comunicarse con una instancia específica de Worker ; del mismo modo, un Worker necesita poder comunicar de nuevo a un determinado Boss .

No está claro si la pregunta pertenece a uno u otro caso.

Si Boss y Worker pueden existir de forma independiente, entonces solo se necesita implementar un mecanismo de comunicación mutua entre ellos, sin que cada uno mantenga una referencia a la otra. Es posible utilizar punteros que no sean de propiedad para este propósito, siempre y cuando uno implemente un mecanismo para restablecer los punteros en cada instancia sobreviviente cada vez que se elimine una instancia.

Un ejemplo de código que intenta resolver esto:

// this class lists everyone and acts as the communication hub
class CompanyDirectory
{
    // owning reference to boss and all workers
    std::unique_ptr<Boss> boss;
    std::unordered_map<int, unique_ptr<Worker>> workers;
public:
    CompanyDirectory() { ... } // either ctor, or use two-part initialization
    Boss& GetBoss() { return *boss; }
    Worker& GetWorker(int workerId) { return *workers.at(workerId); }
};

class Boss
{
    // non-owning pointer to the communication hub
    CompanyDirectory* directory; 
public:
    void Promote(int workerId, Position position)
    {
        directory->GetWorker(workerId).SetPosition(position);
    }
};

// likewise, each Worker has a non-owning pointer/reference 
// to the communication hub that is CompanyDirectory.

En este ejemplo, la obtención de Boss a solicitud de una clase Worker, or fetching a Worker by ID upon request by Boss or by a fellow worker, is the responsibility of the CompanyDirectory '.

Si una instancia Worker se invalida, también es responsabilidad de CompanyDirectory , específicamente sus métodos de recuperación ( GetBoss , GetWorker ), para lanzar una excepción.

    
respondido por el rwong 24.06.2016 - 14:33
0

No hay nada de malo en este diseño.

A Java no le importa en qué orden compilar las cosas, por lo general simplemente funciona mágicamente. Bueno, he encontrado algunos casos tremendamente complicados que involucran la inicialización estática de variables públicas estáticas y el orden de carga de clases en los que tuve problemas, pero eso es extremadamente raro.

Si tiene un problema, será con guardar estos objetos. En su base de datos (o asignación de base de datos) debe permitir que al menos un puntero sea nulo. En su caso, tendría que permitir que un usuario con un jefe nulo o un jefe sin ningún usuario (o ambos).

Si usas un ORM, estarás bien, pero si escribes tu propia forma de serializar o persistir estos objetos, es posible que tengas que detectar bucles para no entrar en un bucle infinito tratando de guardar tu objeto. gráfico.

Dicho esto, la gente hace esto todo el tiempo. Rails tiene un "último_modificador" que hace referencia al usuario. Cuando editas un usuario, te conviertes en el "último modificador" de ese usuario, por lo que la tabla se hace referencia a sí misma (auto-referencial). Cuando almacena un árbol en una base de datos SQL, cada fila generalmente tiene un puntero parent_id que hace referencia a otra fila en la misma tabla.

    
respondido por el GlenPeterson 24.06.2016 - 19:57
0

Out Of The Tar Pit (PDF), publicado en 2006, describe la programación relacional funcional. Piense en sus clases como tablas en una base de datos relacional. ¿Cómo construirías esta relación?

Jefe y Trabajador serían "Empleados". La relación entre el jefe y el trabajador es una relación de uno a muchos: cada empleado debe tener un jefe. Esta relación podría implementarse como una columna en la tabla de empleados que identifica esto como un atributo de empleado o podría representarse como una tabla separada que puede agregar capacidades adicionales (¿varios jefes?).

No todas las características de un objeto pertenecen necesariamente al objeto. Las relaciones externas pueden ser valiosas, flexibles y ayudan a reducir la redundancia (y los errores).

    
respondido por el Bill Door 24.06.2016 - 23:11
-1

En algunos idiomas (Objective-C, Swift, creo que C # y C ++ con la clase de puntero correcta, tiene referencias propietarias que evitan que se desasigne un objeto, y las referencias circulares son generalmente desaprobado porque los objetos evitan la desasignación.

En el caso de Boss y Worker, eso es una tontería. Si la persona representada por el objeto Boss es despedida o muere y el objeto Boss debería ser destruido como consecuencia, los objetos Trabajadores no deberían simplemente desaparecer porque los trabajadores todavía están allí . En la práctica, si el jefe deja la empresa, entonces el software que maneja esto tendrá que cambiar los punteros del jefe en todos los objetos del trabajador a otra cosa. A un puntero nulo oa un puntero de jefe diferente.

Ahora puede ser un diseño mucho mejor tener un lugar que administre las relaciones entre los jefes y los trabajadores. Así que el objeto trabajador no tiene un puntero a su jefe, pero pregunta quién es su jefe. Eso funciona mucho mejor en software que en la vida real. Y es mucho más flexible en cuanto a que un trabajador puede tener dos o tres jefes, o ninguno.

    
respondido por el gnasher729 24.06.2016 - 13:20

Lea otras preguntas en las etiquetas

Comentarios Recientes

Cooping ciertamente es algo que los conceptos Theta cuando programan funciones y clases podrían arreglar el diseño, pero consideren los problemas de memoria inconsistente y administración de memoria. En lugar de arreglar cuando nuestra confianza en que la modularidad funciona se ve más o menos afectada (no todos los grandes proyectos corrigen su confianza simplemente debido a la gestión adecuada de la memoria y la posible interferencia con la construcción de la unión), un cambio para indicar que en realidad sería... Lee mas