Как вы нарушаете круговые ассоциации между сущностями?

мой первый раз на сайте так извиняюсь, если он неправильно помечен или получил ответ в другом месте...

Я постоянно сталкиваюсь с конкретной ситуацией в моем текущем проекте, и мне было интересно, как вы, ребята, справитесь с этим. Шаблон: родительский элемент с коллекцией дочерних элементов, а родитель имеет одну или несколько ссылок на определенные элементы в дочерней коллекции, обычно это "дочерний элемент по умолчанию".

Более конкретный пример:

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

Мне кажется, что это хороший чистый способ моделирования отношений, но сразу вызывает проблемы благодаря круговой ассоциации, я не могу принуждать отношения в БД из-за круговых внешних ключей, а LINQ to SQL взрывается из-за циклической ассоциации. Даже если бы я мог обманывать это, это явно не отличная идея.

Моя единственная идея в настоящее время состоит в том, чтобы в меню MenuItem был флаг IsDefault:

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

Кто-нибудь имел дело с чем-то похожим и мог предложить некоторые советы?

Ура!

Изменить: спасибо за ответы до сих пор, возможно, пример "Меню" не был блестящим, хотя я пытался подумать о чем-то представительном, поэтому мне не пришлось вникать в специфику нашего не-себя -Экспланативная модель домена! Возможно, лучшим примером может быть отношение Компания/Сотрудник:

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

Сотруднику определенно нужна ссылка на их компанию, и каждая компания может иметь только одного ContactPerson. Надеюсь, что мой исходный пункт станет более понятным!

Ответы

Ответ 1

Спасибо всем, кто ответил, некоторые действительно интересные подходы! В конце концов, мне пришлось кое-что сделать в большой спешке, так что это то, что я придумал:

Введен третий объект под названием WellKnownContact и соответствующий WellKnownContactType перечисление:

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
}

Он чувствует себя немного громоздким, но обходит круглую справочную проблему и четко отображает на БД, который сохраняет попытку заставить LINQ to SQL делать что-то слишком умное! Также допускается несколько типов "хорошо известных контактов", которые определенно входят в следующий спринт (так что не YAGNI!).

Интересно, что когда я придумал надуманный пример Company/Employee, ему было намного легче думать, в отличие от довольно абстрактных объектов, с которыми мы действительно имеем дело.

Ответ 2

Трюк для решения этого заключается в том, чтобы понять, что родитель не должен знать обо всех методах этого ребенка и что ребенку не нужно знать все методы родителя. Поэтому вы можете использовать Принцип разделения сегментов, чтобы разделить их.

Короче говоря, вы создаете интерфейс для родителя, который имеет только те методы, которые необходимы ребенку. Вы также создаете интерфейс для ребенка, который имеет только те методы, которые необходимы родителям. Затем у вас есть родительский список списка дочерних интерфейсов, и у вас есть дочерний объект обратно к родительскому интерфейсу. Я называю этот шаблон Flip Flob, потому что диаграмма UML имеет геометрию триггера Eckles-Jordan (Sue me, я старый инженер-технолог!)

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

Обратите внимание, что на этой диаграмме нет цикла. Вы не можете начать с одного класса и следовать указателям на исходную точку.

Иногда, чтобы добиться разделения, нужно переместить некоторые методы. Может быть, код, который, по вашему мнению, должен был быть в SystemMenu, который вы переходите в MenuItem и т.д. Но в целом техника работает хорошо.

Ответ 3

Ваше решение кажется вполне разумным.

Еще одна вещь, о которой стоит подумать, состоит в том, что ваши объекты в памяти не должны точно соответствовать схеме базы данных. В базе данных вы можете иметь более простую схему с дочерними свойствами, но в памяти вы можете оптимизировать вещи и иметь родителя со ссылками на дочерние объекты.

Ответ 4

Я действительно не вижу твоей проблемы. Ясно, что вы используете С#, который содержит объекты как ссылки, а не экземпляры. Это означает, что он идеально подходит для перекрестных ссылок или даже для саморегуляции.

в С++ и других языках, где объекты более сложны, тогда у вас могут быть проблемы, которые обычно решаются с использованием ссылок или указателей, но С# должно быть в порядке.

Скорее всего, ваша проблема заключается в том, что вы пытаетесь каким-то образом следовать всем ссылкам, что приводит к циклической ссылке. LINQ использует ленивую загрузку для решения этой проблемы. Например, LINQ не будет загружать компанию или сотрудника, пока вы не обратитесь к ней. Вам просто нужно избегать следующих ссылок ниже одного уровня.

Однако вы не можете добавить две таблицы в качестве внешних ключей друг друга, иначе вы никогда не сможете удалить какую-либо запись, так как удаление сотрудника потребует удаления компании сначала, но вы не можете удалить компанию без удаление сотрудника. Как правило, в этом случае вы использовали бы только один из них как реальный внешний ключ, другой - просто psuedo-FK (то есть тот, который используется как FK, но не имеет ограничений). Вы должны решить, какая из них важнее.

В примере с корпорацией вы, скорее всего, захотите удалить сотрудника, но не компанию, поэтому сделайте компанию- > сотрудником FK ограничение отношения. Это не позволяет удалить компанию, если есть сотрудники, но вы можете удалить сотрудников без удаления компании.

Кроме того, избегайте создания новых объектов в конструкторе в этих ситуациях. Например, если ваш объект Employee создает новый объект компании, который включает в себя нового сотрудника ojbect, созданного для сотрудника, он в конечном итоге исчерпает память. Вместо этого передайте объекты, уже созданные конструктору, или установите их после построения, возможно, используя метод инициализации.

Например:

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

то в AddEmployee вы устанавливаете компанию

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

Ответ 5

Может быть, самореферентный шаблон GoF Composite - это порядок здесь. Меню имеет коллекцию листьев MenuItems, и оба имеют общий интерфейс. Таким образом вы можете составить меню из меню и/или меню. В схеме есть таблица с внешним ключом, которая указывает на собственный первичный ключ. Также работает с пешеходными меню.

Ответ 6

В коде вам нужно иметь ссылки в обоих направлениях, чтобы ссылаться на вещи в обоих направлениях. Но в базе данных вам нужна только одна ссылка, чтобы заставить все работать. Из-за того, как работает соединение, вам нужно иметь только внешний ключ в одной из ваших таблиц. Когда вы думаете об этом, каждый внешний ключ в вашей базе данных можно перевернуть, создать и создать круговую ссылку. Лучше всего выбрать одну запись, в этом случае, возможно, ребенок с внешним ключом для родителя, и просто нужно сделать.

Ответ 7

В разумном смысле, ориентированном на домен, вы можете избежать двунаправленных отношений между сущностями, где это возможно. Выберите один "совокупный корень", чтобы сохранить отношения, и использовать другой объект только при навигации из корня агрегата. Я стараюсь избегать двунаправленных отношений, где это возможно. Из-за YAGNI, и это заставит вас задать вопрос "что было первым, курица или яйцо?" Иногда вам понадобятся двунаправленные ассоциации, затем выберите одно из решений, упомянутых ранее.

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

Ответ 8

Вы можете применять внешние ключи в базе данных, где две таблицы относятся друг к другу. Приходят на ум два способа:

  • По умолчанию дочерний столбец в родительском объекте имеет значение null и обновляется только после того, как все дочерние строки были вставлены.
  • Вы откладываете проверку ограничений до момента фиксации. Это означает, что вы можете сначала вставить родителя с изначально сломанной ссылкой на дочерний элемент, а затем вставить его. Одна из проблем с проверкой отложенных ограничений заключается в том, что вы можете исключить исключения из базы данных в момент фиксации, что часто неудобно во многих фреймах db. Кроме того, это означает, что вам нужно знать первичный ключ для ребенка, прежде чем вставлять его, что может быть неудобно в вашей настройке.

Я предположил, что элемент родительского меню живет в одной таблице, а дочерний элемент - в другой таблице, но одно и то же решение будет работать, если они находятся в одной таблице.

Многие СУБД поддерживают отложенную проверку ограничений. Возможно, вы тоже, хотя не упоминаете, какую СУБД вы используете