Se A ha B e B detiene il riferimento di A, è necessario correggere un disegno difettoso? [duplicare]

7

Supponiamo che io abbia Boss e Lavoratore di classe; Boss ha un lavoratore e un operaio detiene un riferimento a Boss:

Boss.h

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

Worker.h

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

In C ++, Worker non ha bisogno di Boss per compilare, ma non mi piace vedere la parola "Boss" apparire in Worker e la parola "Worker" appare in un "Boss" allo stesso tempo. Inoltre, se questo progetto si sposta in un altro linguaggio che non ha sistemi come .h e .cpp (ad esempio: java), diventa dipendente l'uno dall'altro. Quindi, il mio problema è che se il codice C ++ ha un tale pattern, anche Worker non richiede la compilazione di Boss, è ancora necessario correggere un design difettoso? In tal caso, come posso risolverlo?

    
posta ggrr 24.06.2016 - 05:02
fonte

5 risposte

21

Non c'è nulla che sia fondamentalmente viziato su questa idea. Quello che hai sono due relazioni. Boss possiede uno o più Worker s. E Worker ha un riferimento non proprietario a Boss . L'uso di un puntatore non elaborato suggerisce che Worker non proprio il puntatore memorizzato; lo sta semplicemente usando. Ciò significa che non controlla la durata dell'oggetto.

Non c'è nulla di sbagliato in tale rapporto di per sé. Tutto dipende da come viene utilizzato.

Ad esempio, se il riferimento a Boss che Worker memorizza è l'effettiva istanza di oggetto Boss che possiede Worker , allora tutto va bene. Perché? Perché presumibilmente, un'istanza Worker non può esistere senza un Boss che lo possiede. E poiché Boss lo possiede, questo garantisce che Worker non sopravviverà al Boss a cui fa riferimento.

Beh ... un po '. Ed è qui che inizi a entrare in potenziali problemi.

In primo luogo, secondo le regole della costruzione C ++, Boss::worker viene costruita prima dell'istanza Boss stessa. Ora, il costruttore di Boss può passare un puntatore this al costruttore di Worker . Ma Worker non può utilizzarlo , poiché l'oggetto Boss non è ancora stato creato. Worker può memorizzarlo, ma non può accedere a nulla in esso.

Allo stesso modo, con le regole della distruzione di C ++, Boss::worker sarà distrutto dopo l'istanza di possesso di Boss . Pertanto, il distruttore di Worker non può utilizzare in modo sicuro il puntatore Boss , poiché punta a un oggetto la cui durata è terminata.

Queste limitazioni possono talvolta portare alla necessità di utilizzare la costruzione a due stadi. Cioè, chiamando Worker indietro dopo che Boss è stato completamente costruito, in modo che possa comunicare con esso durante la costruzione. Ma anche questo può essere OK, in particolare se Worker non ha bisogno di parlare con Boss nel suo costruttore.

Anche la copia diventa un problema. O più al punto, gli operatori di assegnazione diventano problematici. Se Worker::boss è destinato a puntare all'istanza Boss specifica che lo possiede, allora non devi mai copiarlo. Infatti, se questo è il caso, dovresti dichiarare Worker::boss come puntatore costante, in modo che il puntatore venga impostato nel costruttore e da nessun'altra parte.

Il punto di questo esempio è che l'idea che hai definito non è, da sola, irragionevole. Ha un significato ben definito e puoi usarlo per molte cose. Devi solo pensare su cosa stai facendo.

    
risposta data 24.06.2016 - 05:26
fonte
3

Cercherò di perfezionare un punto sollevato nella risposta di gnasher729 .

Dovremmo fare un'attenta distinzione tra:

  • Proprietà - Boss possiede un Worker . Se Boss è distrutto, il Worker che possiede deve essere distrutto simultaneamente.
  • esistenza indipendente (entità)
    • Un Boss e un Worker possono esistere con una durata specifica. Se uno viene distrutto, l'altro non viene necessariamente distrutto.
    • Detto questo, un Boss deve essere in grado di comunicare con un'istanza specifica di Worker ; allo stesso modo, una Worker deve essere in grado di comunicare di nuovo a uno specifico Boss .

Non è chiaro se la domanda appartiene a entrambi i casi.

Se Boss e Worker possono esistere indipendentemente, allora è sufficiente implementare un meccanismo di comunicazione reciproca tra di loro, senza che ognuno abbia un riferimento all'altro. È possibile utilizzare puntatori non proprietari per questo scopo, purché si implementi un meccanismo per reimpostare i puntatori in ogni istanza sopravvissuta ogni volta che un'istanza viene eliminata.

Un esempio di codice che tenta di risolvere questo:

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

In questo esempio, il recupero del Boss su richiesta di una Worker, or fetching a Worker by ID upon request by Boss or by a fellow worker, is the responsibility of the CompanyDirectory 'classe.

Se un'istanza Worker è invalidata, è anche responsabilità di CompanyDirectory , in particolare i suoi metodi di recupero ( GetBoss , GetWorker ), per generare un'eccezione.

    
risposta data 24.06.2016 - 14:33
fonte
0

Non c'è niente di sbagliato in questo design.

A Java non importa quale sia l'ordine per compilare le cose, di solito funziona solo magicamente. Bene, ho trovato alcuni casi orribilmente complicati riguardanti l'inizializzazione statica di variabili statiche pubbliche e l'ordine di caricamento della classe in cui mi sono imbattuto in problemi, ma questo è estremamente raro.

Se hai un problema, lo sarà con il salvataggio di questi oggetti. Nel tuo database (o mappatura del database) devi consentire almeno un puntatore di essere nullo. Nel tuo caso, dovresti consentire a un utente con un capo nullo o un capo senza utenti (o entrambi).

Se usi un ORM, starai bene, ma se scrivi il tuo modo di serializzare o persistere questi oggetti, potresti dover rilevare dei loop in modo da non entrare in un ciclo infinito cercando di salvare il tuo oggetto grafico.

Detto questo, le persone lo fanno sempre. Rails ha un "last_modifier" che fa riferimento all'Utente. Quando si modifica un utente, si diventa "ultimo modificatore" dell'utente in modo che la tabella faccia riferimento a se stessa (autoreferenziale). Ogni volta che si memorizza una struttura in un database SQL, in genere ogni riga ha un puntatore parent_id che fa riferimento a un'altra riga nella stessa tabella.

    
risposta data 24.06.2016 - 19:57
fonte
0

Fuori dal catrame (PDF), pubblicato nel 2006 descrive la programmazione relazionale funzionale. Pensa alle tue classi come tabelle in un database relazionale. Come costruiresti questa relazione?

Capo e operaio sarebbero "Dipendenti". La relazione tra il capo e il lavoratore è una relazione uno a molti: ogni dipendente dovrebbe avere un capo. Questa relazione potrebbe essere implementata come una colonna nella tabella dei dipendenti identificandola come un attributo dipendente o potrebbe essere rappresentata come una tabella separata che potrebbe aggiungere funzionalità aggiuntive (più boss?).

Non tutte le caratteristiche di un oggetto appartengono necessariamente all'oggetto. Le relazioni esterne possono essere preziose, flessibili e aiutano a ridurre ridondanza (e bug).

    
risposta data 24.06.2016 - 23:11
fonte
-1

In alcune lingue (Objective-C, Swift, penso C # e C ++ con la giusta classe pointer, hai possedere riferimenti che mantengono un oggetto da essere deallocato, e i riferimenti circolari proprietari di solito aggrottato le sopracciglia perché impedisce agli oggetti di essere disallocati.

Nel caso di Boss e Worker è una sciocchezza. Se la persona rappresentata dall'oggetto Boss viene licenziata o muore e l'oggetto Boss dovrebbe essere distrutto come conseguenza, gli oggetti Worker non dovrebbero semplicemente sparire perché i lavoratori sono ancora lì . In pratica, se il capo lascia la società, il software che lo gestisce dovrà cambiare i puntatori boss in tutti gli oggetti worker in qualcos'altro. A un puntatore nullo o a un puntatore boss diverso.

Ora potrebbe essere un progetto molto migliore avere un posto in cui gestire le relazioni tra capi e lavoratori. Quindi l'oggetto lavoratore non ha un puntatore al suo capo, ma chiede chi è il suo capo. Funziona molto meglio nel software che nella vita reale. Ed è molto più flessibile in quanto un lavoratore può avere due o tre capi o nessuno.

    
risposta data 24.06.2016 - 13:20
fonte

Leggi altre domande sui tag