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?
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 Worker
s. 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 Worker
ne 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.
Je vais essayer d'affiner un point soulevé dans réponse de gnasher729 .
Nous devons faire une distinction prudente entre:
Boss
possède un Worker
. Si Boss
est détruit, le Worker
qu'il possède doit être détruit simultanément.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.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 a
by 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.
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.
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).