13

我第一次在该网站上,如果它被错误地标记或在其他地方得到回答,我深表歉意......

我在当前项目中不断遇到特殊情况,我想知道你们将如何处理它。模式是:具有子集合的父级,并且父级具有对子集合中特定项目的一个或多个引用,通常是“默认”子级。

一个更具体的例子:

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

Employee 肯定需要引用他们的 Company,并且每个 Company 只能有一个 ContactPerson。希望这能让我的原点更清楚一点!

4

8 回答 8

25

解决这个问题的诀窍是意识到父母不需要知道孩子的所有方法,孩子也不需要知道父母的所有方法。因此,您可以使用接口隔离原则将它们解耦。

简而言之,您为父级创建了一个接口,该接口只有子级需要的那些方法。您还为子级创建了一个接口,该接口仅具有父级需要的那些方法。然后你让父接口包含一个子接口列表,你让子接口指向父接口。我将其称为触发器模式,因为 UML 图具有 Eckles-Jordan 触发器的几何形状(请注意,我是一名老硬件工程师!)

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

请注意,此图中没有循环。你不能从一堂课开始,然后按照箭头回到你的起点。

有时,为了使分离恰到好处,您必须移动一些方法。可能有您认为应该在 SystemMenu 中移动到 MenuItem 等的代码。但总的来说,该技术效果很好。

于 2009-03-07T17:16:30.563 回答
5

您的解决方案似乎很合理。

要考虑的另一件事是内存中的对象不必与数据库模式完全匹配。在数据库中,您可以使用具有子属性的更简单的模式,但在内存中,您可以优化事物并让父对象具有对子对象的引用。

于 2009-03-05T18:42:57.567 回答
3

我真的没有看到你的问题。显然,您使用的是 C#,它将对象作为引用而不是实例。这意味着具有交叉引用甚至自引用是完全可以的。

在 C++ 和其他对象组合更多的语言中,您可能会遇到问题,通常使用引用或指针解决这些问题,但 C# 应该没问题。

您的问题很可能是您试图以某种方式遵循所有引用,从而导致循环引用。LINQ 使用延迟加载来解决这个问题。例如,在您引用 Company 或 Employee 之前,LINQ 不会加载它。您只需要避免遵循这些参考超过一个级别。

但是,您不能真正将两个表添加为彼此的外键,否则您将永远无法删除任何记录,因为删除员工需要先删除公司,但您不能在不删除员工的情况下删除公司. 通常,在这种情况下,您只会将一个用作真正的外键,另一个将只是一个伪 FK(即,用作 FK 但未启用约束的一个)。你必须决定哪个是更重要的关系。

在公司示例中,您可能希望删除员工而不是公司,因此将 company->employee FK 设置为约束关系。如果有员工,这可以防止您删除公司,但您可以在不删除公司的情况下删除员工。

此外,避免在这些情况下在构造函数中创建新对象。例如,如果您的 Employee 对象创建了一个新的 Company 对象,其中包括为该员工创建的新员工对象,它最终会耗尽内存。相反,将已经创建的对象传递给构造函数,或者在构造之后设置它们,可能通过使用初始化方法。

例如:

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

然后,在 AddEmployee 中,设置公司

public void AddEmployee(Employee e)
{
    Employees.Add(e);
    e.Company = this;
}
于 2009-03-07T17:41:46.043 回答
2

也许自我引用的 GoF 复合模式是这里的命令。一个 Menu 有一个叶子 MenuItem 的集合,它们都有一个共同的接口。这样你就可以用菜单和/或菜单项组成一个菜单。该模式有一个带有外键的表,该外键指向它自己的主键。也适用于步行菜单。

于 2009-03-05T18:45:55.610 回答
1

在代码中,您需要同时引用两种方式来引用事物。但是在数据库中,你只需要引用一种方式就可以了。由于连接的工作方式,您只需要在其中一个表中拥有外键。仔细想想,数据库中的每个外键都可以翻转,并创建和创建循环引用。最好只选择一个记录,在这种情况下,可能是孩子与父母的外键,然后就完成了。

于 2009-03-07T17:56:11.950 回答
1

在领域驱动的设计意义上,您可以选择尽可能避免实体之间的双向关系。选择一个“聚合根”来保存关系,仅在从聚合根导航时使用另一个实体。我尽量避免双向关系。因为 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; }
}
于 2009-03-08T12:13:26.480 回答
0

您可以在两个表相互引用的数据库中强制执行外键。想到了两种方法:

  1. 父项中的默认子列最初为 null,并且仅在插入所有子行后才更新。
  2. 您将约束检查推迟到提交时间。这意味着您可以先插入对子项的最初引用中断的父项,然后再插入子项。延迟约束检查的一个问题是,您最终可能会在提交时抛出数据库异常,这在许多数据库框架中通常很不方便。此外,这意味着您需要在插入子项之前知道子项的主键,这在您的设置中可能会很尴尬。

我在这里假设父菜单项位于一个表中,而子项位于另一个表中,但如果它们都位于同一个表中,则相同的解决方案将起作用。

许多 DBMS 支持延迟约束检查。尽管您没有提及您正在使用哪个 DBMS,但您的可能也是如此

于 2009-03-08T12:26:29.240 回答
0

感谢所有回答的人,一些非常有趣的方法!最后我不得不匆忙完成一些事情,所以这就是我想出的:

引入了第三个实体,称为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 做任何太聪明的事情!还允许多种类型的“知名联系人”,这肯定会在下一个 sprint 中出现(所以不是真正的 YAGNI!)。

有趣的是,一旦我想出了人为的公司/员工示例,它就更容易思考,与我们真正处理的相当抽象的实体形成对比。

于 2009-03-09T14:15:53.947 回答