避免在领域模型中出现循环引用

7

这一定是一个很常见的情景,已经有很多关于它的文章了,希望甚至有一个很好的模式。我有一个域模型,在其中一个自定义容器包含实体。例如(为简洁起见省略属性和接口):

class Entity
{
    public int Id;
    public EntityContainer ParentContainer;
}


class EntityContainer
{
    public int Id;
    public IList<Entity> Entities = new List<Entity>();

    public void AddEntity(Entity entity)
    {
        entity.ParentContainer = this;
        Entities.Add(entity);
    }
}


class Main
{
    public Main()
    {
        Entity entity1 = new Entity();
        Entity entity2 = new Entity();
        EntityContainer entityContainer = new EntityContainer();
        entityContainer.AddEntity(entity1);
        entityContainer.AddEntity(entity2);

        // Can now traverse graph easily, e.g.
        Console.WriteLine("entity1's parent container ID = " + entity1.ParentContainer.Id);
        Console.WriteLine("Container contains at least this entity ID: " + entityContainer.Entities[0].Id);

    }
}

我现在可以轻松地双向遍历我的对象图,但是却创建了一个循环引用。您是否会创建第三个类型来解除依赖关系?

提前感谢。


2
你那里的模型不允许多个父容器的反向关系,因此如果一个实体存在于多个容器中,它很可能无法以相同的方式工作。 - workmad3
你能澄清一下这是怎么回事吗?据我所知,你正在创建一个树形结构,所以我不明白循环性从哪里来。 - RobV
实体具有对实体容器的引用,而实体容器具有对实体的引用。 - ng5000
反比关系的观点很好。我需要更新,但这个例子可能对问题的目的来说还可以。 - ng5000
关于反向关系,如果我要将其添加到上面,那么我可能会用“EntityContainer”的列表替换“Entity”类中的父容器字段。 - ng5000
4个回答

5

循环引用本身并没有问题,在.NET Framework中广泛使用,例如XmlNode.OwnerDocument、Control.Parent。

如果需要沿树向上遍历,则可以使用反向引用。

在COM中,循环引用比较棘手,因为如果将容器及其所有子元素设置为null,则对象不会被正确清除,因为子元素仍然持有对父元素的引用。但是,.NET垃圾回收机制实现方式没有这个问题。


我在处理循环引用时遇到的一个问题是序列化。通常,如果对象A有B,而对象B又有A,那么它就无法进行序列化。当将这些对象放入进程外会话状态时,这尤其成为了一个问题。 - Mark Richman

2
容器需要知道内容的类型吗?如果不需要,泛型可以避免这种情况 - 即使用Container<T>,其中你可能会使用Container<Entity>。除此之外,将必要的细节推入一个接口(或基类)中,在两个程序集中都能引用是一种常见方法。
个人而言,我会尝试避免子类需要了解父类的情况。
另外,请注意,如果您选择采用抽象化(接口等)方法,则如果您使用xml序列化,其可能会产生重大影响。
编辑后:
好的,首先:如果循环引用(在程序集内部)没有问题,请不要修改它。如果有问题,则需要使用额外的类型;假设一些接口表示具体类型,即其中Entity:IEntity,而EntityContainer仅知道IEntity(或反之亦然, IEntityContainer,或两者都有)。

1
出于性能原因,子级需要了解其父级。 - ng5000
Generica不适用于我正在建模的实际问题域。这些是特定类型和关系。 - ng5000
谢谢更新 - 我将添加接口。我认为普遍共识是,循环引用其实并没有什么问题,这是这个问题的主要观点。 - ng5000

1

在我的观点中,使用循环引用对象是可以的,只要在设计这些对象时使用某种延迟加载模式。

例如:

您想要访问:Company.Employee,以及在另一种情况下:Employee.Company

这会形成一个循环引用,即Company.Employee.Company.Employee等。

如果这些属性不是延迟加载的,例如公司对象始终加载其员工属性,而员工对象始终加载其公司属性,则在引入数据源时将无法正常工作。


1

所以我认为你的类模型没有问题,但是你可以轻松地进行清理。让Entity实现IEntity接口,并让EntityContainer持有一个IList,除非你有非常特殊的原因使用IList,否则你应该考虑IEnumerable,这将使你使用EntityClass的消费者更容易。因为传递任何IEntity数组或选择IEntities的linq表达式都是可能的。


1
在List上调用Enumerable.ToList()绝对不是免费的,因为它总是会产生一个新的列表 - 因此它必须要进行复制,这是O(N)的。 - Pavel Minaev
@Pavel 不是,它被优化为仅在列表上调用时返回列表对象。 - Rune FS
1
你是完全错误的。用ILSpy打开System.Core.dll并查看Enumerable.ToList的实现。它只是简单地return new List()再加上一个参数null检查。这是非常有意的 - LINQ的设计者不希望库客户端能够将对象向下转换为库作者没有预期的内容,并可能改变内部数据。因此,即使原始对象已经是该类型,所有的To...()方法都会创建一个副本。相比之下,As...()返回原始对象。 - Pavel Minaev
@Pavel,你是对的,我记错了优化,它确实总是返回一个新列表。 - Rune FS

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接