web-dev-qa-db-fra.com

Si A a B et B contient la référence de A, est-ce qu'une conception défectueuse doit être corrigée?

Supposons que j'ai la classe Boss et Worker; Le patron a un travailleur et le travailleur détient une référence de patron:

Boss.h

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

Worker.h

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

En C++, Worker n'a pas besoin de Boss pour compiler, mais je ne suis pas à l'aise de voir le mot "Boss" apparaître dans Worker et le mot "Worker" apparaît dans un "Boss" en même temps. De plus, si cette conception se déplace vers une autre langue qui n'a pas de systèmes comme .h et .cpp (par exemple: Java), elle devient dépendante l'une de l'autre. Donc, mon problème est, si le code C++ a un tel modèle, même Worker n'a pas besoin de Boss pour compiler, est-ce encore une conception défectueuse qui doit être corrigée? Si oui, comment puis-je y remédier?

11
ggrr

Il n'y a rien qui soit fondamentalement défectueux à propos de cette idée. Ce que vous avez, c'est deux relations. Boss possède un ou plusieurs Workers. Et Worker a une référence non propriétaire à un Boss. L'utilisation d'un pointeur brut suggère que Worker ne fait pas propre le pointeur qu'il stocke; c'est simplement l'utiliser. Cela signifie qu'il ne contrôle pas la durée de vie de cet objet.

Il n'y a rien de mal à une telle relation en soi. Tout dépend de la façon dont il est utilisé.

Par exemple, si la référence Boss que Worker stocke est l'instance d'objet Boss réelle qui possède le Worker, alors tout va bien. Pourquoi? Parce que vraisemblablement, une instance Worker ne peut exister sans un Boss qui en est propriétaire. Et parce que le Boss le possède, cela garantit que le Worker ne survivra pas au Boss qu'il référence.

Eh bien ... un peu. Et c'est là que vous commencez à rencontrer des problèmes potentiels.

Tout d'abord, selon les règles de construction C++, Boss::worker est construit avant l'instance Boss elle-même. Maintenant, le constructeur de Boss peut passer un pointeur this au constructeur de Worker. Mais Workerne peut pas l'utiliser, puisque l'objet Boss n'a pas encore été construit. Worker peut le stocker, mais il ne peut y accéder à rien.

De même, selon les règles de destruction C++, Boss::worker sera détruit après l'instance propriétaire Boss. Le destructeur de Worker ne peut donc pas utiliser en toute sécurité le pointeur Boss, car il pointe vers un objet dont la durée de vie est terminée.

Ces limitations peuvent parfois conduire à devoir utiliser une construction en deux étapes. C'est-à-dire, rappeler Worker après que Boss a été entièrement construit, afin qu'il puisse communiquer avec lui pendant la construction. Mais même cela peut être OK, en particulier si Worker n'a pas besoin de parler à Boss dans son constructeur.

La copie devient également un problème. Ou plus précisément, les opérateurs d'affectation deviennent problématiques. Si Worker::boss est destiné à renvoyer à l'instance Boss spécifique qui le possède, alors vous ne devez jamais le copier. En effet, si tel est le cas, vous devez déclarer Worker::boss en tant que pointeur constant, de sorte que le pointeur soit défini dans le constructeur et nulle part ailleurs.

Le point de cet exemple est que l'idée que vous avez définie n'est pas, en soi, déraisonnable. Il a une signification bien définie et vous pouvez l'utiliser pour beaucoup de choses. Vous n'avez qu'à penser à ce que vous faites.

23
Nicol Bolas

Je vais essayer d'affiner un point soulevé dans réponse de gnasher729 .

Nous devons faire une distinction prudente entre:

  • Propriété - Boss possède un Worker. Si Boss est détruit, le Worker qu'il possède doit être détruit simultanément.
  • Existence indépendante (entité)
    • Un Boss et un Worker peuvent exister avec une durée de vie individuelle. Si l'un est détruit, l'autre n'est pas nécessairement détruit.
    • Cela dit, un Boss doit pouvoir communiquer avec une instance spécifique de Worker; de même, un Worker doit pouvoir communiquer revenir à un Boss spécifique.

Il n'est pas clair si la question appartient à l'un ou l'autre cas.

Si Boss et Worker peuvent exister indépendamment, alors il suffit d'implémenter un mécanisme de communication mutuelle entre eux, sans que chacun garde une référence à l'autre. Il est possible d'utiliser des pointeurs non propriétaires à cet effet, tant que l'on implémente un mécanisme pour réinitialiser les pointeurs dans chaque instance survivante chaque fois qu'une instance est supprimée.

Un exemple de code qui tente de résoudre ce problème:

// 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.

Dans cet exemple, l'extraction de la classe Boss à la demande d'un ouvrier Worker, or fetching aby ID upon request by Boss or by a fellow worker, is the responsibility of the CompanyDirectory` est demandée.

Si une instance de Worker est invalidée, il est également de la responsabilité de CompanyDirectory, en particulier ses méthodes de récupération (GetBoss, GetWorker), de lever une exception.

4
rwong

Il n'y a rien de mal à cette conception.

Java ne se soucie pas de l'ordre dans lequel compiler les choses, il fonctionne généralement comme par magie. Eh bien, j'ai trouvé quelques cas horriblement compliqués impliquant l'initialisation statique des variables statiques publiques et l'ordre de chargement des classes où j'ai rencontré des problèmes, mais c'est extrêmement rare.

Si vous avez un problème, ce sera avec la sauvegarde de ces objets. Dans votre base de données (ou mappage de base de données), vous devez autoriser la nullité d'au moins un pointeur. Dans votre cas, vous devez autoriser un utilisateur avec un boss nul ou un boss sans aucun utilisateur (ou les deux).

Si vous utilisez un ORM, tout ira bien, mais si vous écrivez votre propre façon de sérialiser ou de persister ces objets, vous devrez peut-être détecter des boucles afin de ne pas entrer dans une boucle infinie en essayant de sauvegarder votre graphique d'objet.

Cela dit, les gens font ça tout le temps. Rails a un "last_modifier" qui fait référence à l'utilisateur. Lorsque vous modifiez un utilisateur, vous devenez le "dernier modificateur" de cet utilisateur de sorte que la table se référence elle-même (auto-référentielle). Chaque fois que vous stockez un arbre dans une base de données SQL, chaque ligne a généralement un pointeur parent_id qui fait référence à une autre ligne de la même table.

0
GlenPeterson

Out Of The Tar Pit (PDF), publié en 2006, décrit la programmation relationnelle fonctionnelle. Considérez vos classes comme des tables dans une base de données relationnelle. Comment construiriez-vous cette relation?

Le patron et l'ouvrier seraient des "employés". La relation entre le patron et le travailleur est une relation un à plusieurs: chaque employé doit avoir un patron. Cette relation pourrait être implémentée sous forme de colonne dans la table des employés identifiant cela comme un attribut d'employé ou elle pourrait être représentée comme une table distincte qui peut ajouter des capacités supplémentaires (plusieurs patrons?).

Toutes les caractéristiques d'un objet n'appartiennent pas nécessairement à l'objet. Les relations externes peuvent être précieuses, flexibles et aider à réduire la redondance (et les bogues).

0
Bill Door