Domanda

la mia prima volta sul sito quindi mi scuso se è stato taggato in modo errato o è stata data risposta altrove ...

Continuo a imbattermi in una situazione particolare del mio attuale progetto e mi chiedevo come voi ragazzi l'avreste affrontato. Il modello è: un genitore con una raccolta di figli e il genitore ha uno o più riferimenti a elementi particolari nella raccolta figlio, normalmente il figlio "predefinito".

Un esempio più concreto:

public class SystemMenu 
{
    public IList<MenuItem> Items { get; private set; }
    public MenuItem DefaultItem { get; set; }
}

public class MenuItem
{
    public SystemMenu Parent { get; set; }
    public string Name { get; set; }
}

Per me questo sembra un buon modo chiaro di modellare la relazione, ma causa immediatamente problemi grazie all'associazione circolare, non riesco a far valere la relazione nel DB a causa delle chiavi circolari esterne e esplode LINQ to SQL a causa dell'associazione ciclica. Anche se potessi evitarlo, chiaramente non è una grande idea.

La mia unica idea al momento è quella di avere un flag 'IsDefault' su MenuItem:

public class SystemMenu 
{
    public IList<MenuItem> Items { get; private set; }
    public MenuItem DefaultItem 
    {
        get 
        {
            return Items.Single(x => x.IsDefault);
        }
        set
        {
            DefaultItem.IsDefault = false;
            value.DefaultItem = true;
        }
    }
}

public class MenuItem
{
    public SystemMenu Parent { get; set; }
    public string Name { get; set; }
    public bool IsDefault { get; set; }
}

Qualcuno ha affrontato qualcosa di simile e potrebbe offrire qualche consiglio?

Cheers!

Modifica: grazie per le risposte finora, forse l'esempio di "Menu" non è stato brillante però, stavo cercando di pensare a qualcosa di rappresentativo, quindi non ho dovuto andare nei dettagli del nostro non-io-sé -modello di dominio esplicativo! Forse un esempio migliore sarebbe una relazione azienda / dipendente:

public class Company
{
    public string Name { get; set; }
    public IList<Employee> Employees { get; private set; }
    public Employee ContactPerson { get; set; }
}

public class Employee
{
    public Company EmployedBy { get; set; }
    public string FullName { get; set; }
}

Il Dipendente avrebbe sicuramente bisogno di un riferimento alla propria Azienda e ogni Azienda potrebbe avere un solo ContactPerson. Spero che questo renda il mio punto originale un po 'più chiaro!

È stato utile?

Soluzione 8

Grazie a tutti coloro che hanno risposto, alcuni approcci davvero interessanti! Alla fine ho dovuto fare qualcosa in grande fretta, quindi questo è quello che mi è venuto in mente:

Introdotta una terza entità chiamata WellKnownContact e corrispondente WellKnownContactType enum:

public class Company
{
    public string Name { get; set; }
    public IList<Employee> Employees { get; private set; }
    private IList<WellKnownEmployee> WellKnownEmployees { get; private set; }
    public Employee ContactPerson
    {
        get
        {
            return WellKnownEmployees.SingleOrDefault(x => x.Type == WellKnownEmployeeType.ContactPerson);
        }
        set
        {                
            if (ContactPerson != null) 
            {
                // Remove existing WellKnownContact of type ContactPerson
            }

            // Add new WellKnownContact of type ContactPerson
        }
    }
}

public class Employee
{
    public Company EmployedBy { get; set; }
    public string FullName { get; set; }
}

public class WellKnownEmployee
{
    public Company Company { get; set; }
    public Employee Employee { get; set; }
    public WellKnownEmployeeType Type { get; set; }
}

public enum WellKnownEmployeeType
{
    Uninitialised,
    ContactPerson
}

Sembra un po 'ingombrante ma aggira il problema di riferimento circolare e si mappa in modo pulito sul DB, il che consente di risparmiare cercando di ottenere LINQ to SQL per fare qualcosa di troppo intelligente! Inoltre, consente diversi tipi di "contatti noti", che sicuramente arriveranno nel prossimo sprint (quindi non proprio YAGNI!).

È interessante notare che, una volta che ho trovato l'esempio inventato di Società / Dipendente, mi è sembrato MOLTO più facile da pensare, a differenza delle entità piuttosto astratte con cui abbiamo veramente a che fare.

Altri suggerimenti

Il trucco per risolvere questo problema è rendersi conto che il genitore non ha bisogno di conoscere tutti i metodi del bambino e che il bambino non ha bisogno di conoscere tutti i metodi del genitore. Pertanto è possibile utilizzare il Principio di segregazione dell'interfaccia per disaccoppiarli.

In breve, crei un'interfaccia per il genitore che ha solo quei metodi di cui il figlio ha bisogno. Si crea anche un'interfaccia per il figlio che ha solo quei metodi di cui il genitore ha bisogno. Quindi il padre contiene un elenco delle interfacce figlio e il figlio fa riferimento nuovamente all'interfaccia padre. Lo chiamo il modello Flip Flob perché il diagramma UML ha la geometria di un infradito Eckles-Jordan (Sue me, sono un vecchio ingegnere hardware!)

  |ISystemMenu|<-+    +->|IMenuItem|
          A    1  \  / *     A
          |        \/        |
          |        /\        |
          |       /  \       |
          |      /    \      |
          |     /      \     |
    |SystemMenu|        |MenuItem|

Si noti che non c'è ciclo in questo diagramma. Non puoi iniziare da una classe e seguire le frecce fino al punto di partenza.

A volte, per ottenere la separazione giusta, devi spostare alcuni metodi. Potrebbe esserci un codice che pensavi dovesse essere nel menu di sistema che si sposta in MenuItem, ecc. Ma in generale la tecnica funziona bene.

La tua soluzione sembra abbastanza ragionevole.

Un'altra cosa a cui pensare è che i tuoi oggetti in memoria non devono corrispondere esattamente allo schema del database. Nel database puoi avere lo schema più semplice con le proprietà figlio, ma in memoria puoi ottimizzare le cose e avere il genitore con riferimenti agli oggetti figlio.

Non vedo davvero il tuo problema. Chiaramente stai usando C #, che contiene oggetti come riferimenti e non istanze. Ciò significa che è perfettamente corretto avere riferimenti incrociati o persino riferimenti personali.

in C ++ e in altre lingue in cui gli oggetti sono più compositi, allora si possono avere problemi, che in genere vengono risolti usando riferimenti o puntatori, ma C # dovrebbe andare bene.

Molto probabilmente il tuo problema è che stai cercando di seguire tutti i riferimenti in qualche modo, portando a un riferimento circolare. LINQ utilizza il caricamento lento per risolvere questo problema. Ad esempio, LINQ non caricherà la Società o il Dipendente fino a quando non lo farà riferimento. Devi solo evitare di seguire tali riferimenti oltre un livello.

Tuttavia, non puoi davvero aggiungere due tabelle come ogni altra chiave esterna, altrimenti non saresti mai in grado di eliminare alcun record, poiché l'eliminazione di un dipendente richiederebbe prima l'eliminazione dell'azienda, ma non puoi eliminare l'azienda senza eliminazione dell'impiegato. Tipicamente, in questo caso, useresti solo uno come una vera chiave esterna, l'altro sarebbe semplicemente un psuedo-FK (cioè uno che viene usato come un FK ma non ha vincoli abilitati). Devi decidere qual è la relazione più importante.

Nell'esempio dell'azienda, è probabile che tu voglia eliminare il dipendente ma non la società, quindi imposta la relazione di vincolo tra la società e il dipendente FK. Ciò ti impedisce di eliminare la società se ci sono dipendenti, ma puoi eliminare i dipendenti senza eliminare la società.

Inoltre, evitare di creare nuovi oggetti nel costruttore in queste situazioni. Ad esempio, se l'oggetto Employee crea un nuovo oggetto Company, che include un nuovo oggetto dipendente creato per il dipendente, alla fine esaurirà la memoria. Invece, passa gli oggetti già creati al costruttore o impostali dopo la costruzione, possibilmente utilizzando un metodo di inizializzazione.

Ad esempio:

Company c = GetCompany("ACME Widgets");
c.AddEmployee(new Employee("Bill"));

quindi, in AddEmployee, si imposta la società

public void AddEmployee(Employee e)
{
    Employees.Add(e);
    e.Company = this;
}

Forse un modello composito GoF autoreferenziale è un ordine qui. Un menu ha una raccolta di elementi menu foglia ed entrambi hanno un'interfaccia comune. In questo modo è possibile comporre un menu tra Menu e / o MenuItems. Lo schema ha una tabella con una chiave esterna che punta alla propria chiave primaria. Funziona anche con i menu a piedi in questo modo.

Nel codice, devi avere riferimenti in entrambi i modi per fare riferimento alle cose in entrambi i modi. Ma nel database, hai solo bisogno del riferimento in un modo per far funzionare le cose. A causa del modo in cui i join funzionano, devi solo avere la chiave esterna in una delle tue tabelle. Quando ci pensi, ogni chiave esterna nel tuo database potrebbe essere capovolta e creare e creare un riferimento circolare. Meglio scegliere solo un record, in questo caso probabilmente il bambino con una chiave esterna per il genitore, e basta.

In un senso del design guidato dal dominio, puoi scegliere di evitare relazioni bidirezionali tra entità laddove possibile. Scegli una "radice aggregata" per mantenere le relazioni e usare l'altra entità solo quando si naviga dalla radice aggregata. Cerco di evitare relazioni bidirezionali dove è possibile. A causa di YAGNI, e ti farà porre la domanda "qual è stato il primo, il pollo o l'uovo?" A volte avrai ancora bisogno di associazioni bidirezionali, quindi scegli una delle soluzioni menzionate in precedenza.

/// This is the aggregate root
public class Company
{
    public string Name { get; set; }
    public IList<Employee> Employees { get; private set; }
    public Employee ContactPerson { get; set; }
}

/// This isn't    
public class Employee
{
    public string FullName { get; set; }
}

È possibile applicare chiavi esterne nel database in cui due tabelle si riferiscono l'una all'altra. Mi vengono in mente due modi:

  1. La colonna figlio predefinita nel genitore è inizialmente nulla e viene aggiornata solo dopo aver inserito tutte le righe figlio.
  2. Rinvii il controllo dei vincoli fino al momento del commit. Ciò significa che è possibile inserire prima il genitore con un riferimento inizialmente rotto al figlio, quindi inserire il figlio. Un problema con il controllo differito dei vincoli è che si possono finire con eccezioni del database lanciate al momento del commit, che è spesso scomodo in molti framework db. Inoltre, significa che devi conoscere la chiave primaria del bambino prima di inserirla, che potrebbe essere scomoda nella tua configurazione.

Ho ipotizzato qui che la voce di menu principale risieda in una tabella e il figlio in una tabella diversa, ma la stessa soluzione funzionerebbe se fossero entrambi nella stessa tabella.

Molti DBMS supportano il controllo vincolato differito. Forse anche il tuo, anche se non menzioni il DBMS che stai utilizzando

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top