我第一次在网站上如果标记不正确或在其他地方得到回答而道歉......
我在目前的项目中遇到了特殊的情况,我想知道你们会如何处理它。模式是:父项具有子集合,父项具有对子集合中特定项的一个或多个引用,通常是“默认”子项。
更具体的例子:
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; }
}
对我而言,这似乎是建立关系的一种很好的干净方式,但由于循环关联而立即导致问题,因为循环外键而无法强制执行DB中的关系,并且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。希望这能让我的原点更加清晰!
答案 0 :(得分:25)
解决这个问题的诀窍是要意识到父母不需要知道孩子的所有方法,并且孩子不需要知道父母的所有方法。因此,您可以使用Interface Segregation Principle对其进行解耦。
简而言之,您为父级创建了一个只包含子级所需方法的接口。您还为子级创建了一个只有父级需要的方法的接口。然后,您让父级包含子接口的列表,并让子级指向父接口。我称之为 Flip Flob Pattern 因为UML图具有Eckles-Jordan触发器的几何形状(Sue me,我是一位老硬件工程师!)
|ISystemMenu|<-+ +->|IMenuItem|
A 1 \ / * A
| \/ |
| /\ |
| / \ |
| / \ |
| / \ |
|SystemMenu| |MenuItem|
请注意,此图中没有循环。你不能从一个班级开始,然后按箭头回到起点。
有时,为了使分离恰到好处,你必须移动一些方法。可能存在您认为应该在SystemMenu中移动到MenuItem等的代码。但是通常该技术运行良好。
答案 1 :(得分:6)
你的解决方案似乎很合理。
要考虑的另一件事是内存中的对象不必与数据库模式完全匹配。在数据库中,您可以使用包含子属性的更简单的模式,但在内存中,您可以优化事物并让父项具有对子对象的引用。
答案 2 :(得分:3)
我真的没有看到你的问题。很明显,你正在使用C#,它将对象作为引用而不是实例。这意味着进行交叉引用甚至是自引用都是完美的。
在C ++和其他语言中,对象更复杂,那么你可能遇到问题,通常使用引用或指针来解决,但C#应该没问题。
很可能你的问题是你试图以某种方式跟踪所有引用,导致循环引用。 LINQ使用延迟加载来解决此问题。例如,LINQ在您引用之前不会加载公司或员工。您只需要避免进一步遵循这样的引用。
但是,你不能真正添加两个表作为彼此的外键,否则你永远无法删除任何记录,因为删除一个员工需要先删除公司,但你不能删除该公司删除员工。通常情况下,在这种情况下,您只能使用一个作为真正的外键,另一个只是一个伪FK(即,一个用作FK但没有启用约束的)。你必须决定哪个是更重要的关系。
在公司示例中,您可能希望删除员工而不是公司,因此请将公司 - &gt;员工FK删除约束关系。如果有员工,这可以防止您删除公司,但是您可以删除员工而不删除公司。
此外,避免在这些情况下在构造函数中创建新对象。例如,如果您的Employee对象创建了一个新的Company对象,其中包括为该员工创建的新员工ojbect,它最终会耗尽内存。相反,将已创建的对象传递给构造函数,或者在构造之后设置它们,可能使用初始化方法。
例如:
Company c = GetCompany("ACME Widgets");
c.AddEmployee(new Employee("Bill"));
然后,在AddEmployee中,您设置公司
public void AddEmployee(Employee e)
{
Employees.Add(e);
e.Company = this;
}
答案 3 :(得分:2)
也许自我引用的GoF Composite模式是一个订单。菜单有一个叶子MenuItems的集合,两者都有一个共同的界面。这样你就可以用Menus和/或MenuItems组成一个菜单。该模式具有一个表,该表具有指向其自己的主键的外键。也适用于步行菜单。
答案 4 :(得分:1)
在代码中,您需要引用两种方式来引用两种方式。但是在数据库中,您只需要一种方法来使事情有效。由于连接工作的方式,您只需要在其中一个表中使用外键。当您考虑它时,数据库中的每个外键都可以翻转,并创建和创建循环引用。最好只选择一条记录,在这种情况下可能是父母带有外键的孩子,只是完成了。
答案 5 :(得分: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; }
}
答案 6 :(得分:0)
您可以在数据库中强制执行外键,其中两个表相互引用。想到两种方式:
我在这里假设父菜单项位于一个表中,而子项位于不同的表中,但如果它们都在同一个表中,则相同的解决方案将起作用。
许多DBMS支持延迟约束检查。虽然您没有提到您正在使用的DBMS,但您可能也会这样做
答案 7 :(得分: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做任何太聪明的事情!还允许多种类型的“众所周知的联系人”,它们肯定会在下一个冲刺中出现(所以不是真的YAGNI!)。
有趣的是,一旦我提出了人为的公司/员工示例,它就会让我们更容易思考,与我们真正处理的相当抽象的实体形成鲜明对比。