Question

C'est la première fois que je visite le site. Je vous prie de m'excuser si le balisage est incorrect ou s'il a été répondu ailleurs ...

Je continue à me heurter à une situation particulière dans mon projet actuel et je me demandais comment vous pourriez le gérer. Le modèle est le suivant: un parent avec une collection d’enfants, et le parent a une ou plusieurs références à des éléments particuliers de la collection d’enfants, normalement l’enfant 'par défaut'.

Un exemple plus concret:

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; }
}

Pour moi, cela semble être un bon moyen de modéliser la relation, mais pose des problèmes immédiatement grâce à l'association circulaire, je ne peux pas imposer la relation dans la base de données à cause des clés étrangères circulaires, et LINQ to SQL explose en raison de l'association cyclique. Même si je pouvais éviter cela, ce n’est clairement pas une bonne idée.

Mon seule idée est actuellement d'avoir un indicateur 'IsDefault' sur 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; }
}

Quelqu'un a-t-il eu affaire à quelque chose de similaire et pourrait-il offrir des conseils?

Salut!

Edit: Merci pour les réponses reçues jusqu'à présent, peut-être que l'exemple de "Menu" n'était pas brillant, j'essayais de penser à quelque chose de représentatif, donc je n'avais pas à entrer dans les détails de notre pas-si-soi modèle de domaine non explicatif! Un meilleur exemple serait peut-être une relation entreprise / employé:

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; }
}

L'employé aurait certainement besoin d'une référence à sa société et chaque société ne pouvait avoir qu'un seul contact. J'espère que cela clarifie un peu mon propos initial!

Était-ce utile?

La solution 8

Merci à tous ceux qui ont répondu, des approches vraiment intéressantes! À la fin, j’ai dû faire quelque chose de très vite, c’est ce que j’ai trouvé:

Introduction d'une troisième entité appelée WellKnownContact et l'énumération WellKnownContactType suivante:

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
}

Cela semble un peu encombrant, mais résout le problème des références circulaires et se mappe proprement sur la base de données, ce qui évite d'essayer de faire passer LINQ à SQL de façon trop intelligente! Permet également plusieurs types de "contacts bien connus" qui vont certainement arriver dans le prochain sprint (donc pas vraiment YAGNI!).

Fait intéressant, une fois que j’ai cité l’exemple artificiel entreprise / employé, il a été beaucoup plus facile de penser, contrairement aux entités plutôt abstraites avec lesquelles nous avons vraiment affaire.

Autres conseils

Pour résoudre ce problème, il faut comprendre que le parent n'a pas besoin de connaître toutes les méthodes de l'enfant et que l'enfant n'a pas besoin de connaître toutes les méthodes du parent. Par conséquent, vous pouvez utiliser le principe de séparation des interfaces pour les découpler.

En bref, vous créez une interface pour le parent qui ne comporte que les méthodes dont l'enfant a besoin. Vous créez également une interface pour l'enfant qui ne comporte que les méthodes dont le parent a besoin. Ensuite, vous avez le parent contenant une liste des interfaces enfants, et le pointeur renvoie à l'interface parent. J'appelle cela le motif Flip Flob car le diagramme UML a la géométrie d'une bascule Eckles-Jordan (Sue-moi, je suis un ancien ingénieur en matériel!)

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

Notez qu'il n'y a pas de cycle dans ce diagramme. Vous ne pouvez pas commencer à une classe et suivre les flèches vers votre point de départ.

Parfois, pour que la séparation soit parfaite, vous devez déplacer certaines méthodes. Vous pensez peut-être que le code que vous auriez dû utiliser dans le SystemMenu doit être déplacé vers MenuItem, etc. Mais en général, la technique fonctionne bien.

Votre solution semble tout à fait raisonnable.

Une autre chose à considérer est que vos objets en mémoire ne doivent pas correspondre exactement au schéma de base de données. Dans la base de données, vous pouvez avoir le schéma plus simple avec les propriétés enfants, mais en mémoire, vous pouvez optimiser les choses et avoir le parent avec des références aux objets enfants.

Je ne vois pas vraiment votre problème. Clairement, vous utilisez C #, qui conserve les objets comme références et non comme instances. Cela signifie qu’il est parfaitement correct d’avoir des références croisées, voire des références automatiques.

en C ++ et dans d'autres langages où les objets sont plus composés, vous pouvez rencontrer des problèmes, qui sont généralement résolus à l'aide de références ou de pointeurs, mais C # devrait convenir.

Il est plus que probable que votre problème réside dans le fait que vous essayez de suivre toutes les références, ce qui conduit à une référence circulaire. LINQ utilise le chargement paresseux pour résoudre ce problème. Par exemple, LINQ ne chargera pas la société ou l'employé tant que vous n'y aurez pas fait référence. Il vous suffit d'éviter de suivre de telles références au-delà d'un niveau.

Cependant, vous ne pouvez pas vraiment ajouter deux tables car chaque clé étrangère est utilisée, sinon vous ne pourriez jamais supprimer d'enregistrements, car la suppression d'un employé nécessiterait la suppression de la société en premier lieu, mais vous ne pouvez pas supprimer la société sans supprimer l'employé. En règle générale, dans ce cas, vous utiliseriez uniquement l'une comme clé étrangère réelle, l'autre serait simplement un psuedo-FK (c'est-à-dire une clé utilisée comme FK mais pour laquelle les contraintes ne sont pas activées). Vous devez décider quelle est la relation la plus importante.

Dans l'exemple d'entreprise, vous voudrez probablement supprimer l'employé mais pas l'entreprise. Par conséquent, définissez le rapport société - employé FK comme relation de contrainte. Cela vous empêche de supprimer l'entreprise s'il y a des employés, mais vous pouvez supprimer des employés sans supprimer l'entreprise.

Évitez également de créer de nouveaux objets dans le constructeur dans ces situations. Par exemple, si votre objet Employé crée un nouvel objet Société, qui inclut un nouvel objet employé créé pour l'employé, il épuisera éventuellement la mémoire. À la place, transmettez les objets déjà créés au constructeur ou définissez-les après la construction, éventuellement à l'aide d'une méthode d'initalisation.

Par exemple:

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

puis, dans AddEmployee, vous définissez la société

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

Peut-être qu'un motif composite GoF auto-référentiel est un ordre ici. Un menu a une collection de feuilles de menu, et les deux ont une interface commune. De cette façon, vous pouvez composer un menu à partir de menus et / ou d'éléments de menu. Le schéma a une table avec une clé étrangère qui pointe vers sa propre clé primaire. Fonctionne également avec les menus de marche.

Dans le code, vous devez avoir des références dans les deux sens pour faire référence aux choses dans les deux sens. Mais dans la base de données, vous n'avez besoin que de la référence, une façon de faire fonctionner les choses. En raison de la manière dont les jointures fonctionnent, il vous suffit de disposer de la clé étrangère dans l'une de vos tables. Lorsque vous y réfléchissez, chaque clé étrangère de votre base de données peut être retournée et créer une référence circulaire. Il est préférable de choisir un seul enregistrement, dans ce cas, probablement l’enfant avec une clé étrangère vers le parent, et de le faire.

En termes de conception axée sur le domaine, vous pouvez choisir d’éviter les relations bidirectionnelles entre les entités lorsque cela est possible. Choisissez une "racine agrégée". pour maintenir les relations et utiliser l'autre entité uniquement lorsque vous naviguez à partir de la racine agrégée. J'essaie d'éviter les relations bidirectionnelles là où c'est possible. À cause de YAGNI, et cela vous fera poser la question "Quel était le premier, la poule ou l’œuf?" Parfois, vous aurez toujours besoin d’associations bidirectionnelles, puis choisissez l’une des solutions mentionnées précédemment.

/// 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; }
}

Vous pouvez appliquer des clés étrangères dans la base de données lorsque deux tables se réfèrent. Deux façons me viennent à l’esprit:

  1. La colonne enfant par défaut du parent est initialement nulle et n'est mise à jour que lorsque toutes les lignes enfants ont été insérées.
  2. Vous différez la vérification de la contrainte jusqu'au moment de la validation. Cela signifie que vous pouvez d'abord insérer le parent avec une référence initialement brisée à l'enfant, puis l'insérer. Un problème avec la vérification de contrainte différée est que vous pouvez vous retrouver avec des exceptions de base de données levées au moment de la validation, ce qui est souvent gênant dans de nombreux frameworks de base de données. En outre, cela signifie que vous devez connaître la clé primaire de l’enfant avant de l’insérer, ce qui risque d’être compliqué dans votre configuration.

J'ai supposé ici que l'élément de menu parent vivait dans une table et l'enfant dans une table différente, mais que la même solution fonctionnerait si les deux étaient dans la même table.

De nombreux SGBD prennent en charge la vérification des contraintes différée. Peut-être aussi le vôtre bien que vous ne mentionniez pas le SGBD que vous utilisez

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top