循环引用——架构问题

7
这可能是一个非常基础的问题,但我已经搜索了很多主题,却没有找到相同的情况,尽管我确信这种情况经常发生。
我的项目/程序将跟踪建筑项目图纸的更改,并在图纸更改时向相关人员发送通知。
将有许多建筑项目(工地),每个项目又会有许多图纸。每个图纸都会有几个版本(随着变化,新版本将被创建)。
这是我的项目类。
public class Project
{
    private readonly List<Drawing> _drawings = new List<Drawing>(30);
    private readonly List<Person> _autoRecepients = new List<Person>(30);

    public int ID { get; private set; }
    public string ProjectNumber { get; private set; }
    public string Name { get; private set; }
    public bool Archived { get; private set; }
    public List<Person> AutoRecepients { get { return _autoRecepients; } }


    public Project(int id, string projectNumber, string name)
    {
        if (id < 1) { id = -1; }

        ID = id;
        ProjectNumber = projectNumber;
        Name = name;
    }


    public bool AddDrawing(Drawing drawing)
    {
        if (drawing == null) return false;
        if (_drawings.Contains(drawing)) { return true; }

        _drawings.Add(drawing);

        return _drawings.Contains(drawing);
    }


    public void Archive()
    {
        Archived = true;
    }

    public bool DeleteDrawing(Drawing drawing)
    {
        return _drawings.Remove(drawing);
    }

    public IEnumerable<Drawing> ListDrawings()
    {
        return _drawings.AsReadOnly();
    }

    public override string ToString()
    {
        return string.Format("{0} {1}", ProjectNumber, Name);
    }
}

这是我的绘图课程。
public class Drawing : IDrawing
{
    private List<IRevision> _revisions = new List<IRevision>(5);
    private List<IssueRecord> _issueRecords = new List<IssueRecord>(30);
    private IRevision _currentRevision;

    public int ID { get; private set; }
    public string Name { get; private set; }
    public string Description { get; set; }
    public Project Project { get; private set; }
    public IRevision CurrentRevision { get { return _currentRevision; } }


    public Drawing(int id, string name, string description, Project project)
    {
        // To be implemented
    }


    /// <summary>
    /// Automatically issue the current revision to all Auto Recepients
    /// </summary>
    public void AutoIssue(DateTime date)
    {
        AutoIssue(date, _currentRevision);
    }

    /// <summary>
    /// Automatically issue a particular revision to all Auto Recepients
    /// </summary>
    public void AutoIssue(DateTime date, IRevision revision)
    {

    }

    public void IssueTo(Person person, DateTime date, IRevision revision)
    {
        _issueRecords.Add(new IssueRecord(date, this, revision, person));

        throw new NotImplementedException();
    }


    public void IssueTo(Person person, DateTime date)
    {
        IssueTo(person, date, _currentRevision);
    }        

    public void IssueTo(IEnumerable<Person> people, DateTime date)
    {
        IssueTo(people, date, _currentRevision);
    }

    public void IssueTo(IEnumerable<Person> people, DateTime date, IRevision revision)
    {
        foreach (var person in people)
        {
            IssueTo(person, date, revision);
        }

    }

    public void Rename(string name)
    {
        if (string.IsNullOrWhiteSpace(name)) { return; }

        Name = name;
    }

    public void Revise(IRevision revision)
    {
        if (revision.Name == null ) return;

        _revisions.Add(revision);
        _currentRevision = revision;
    }

    public struct IssueRecord
    {
        public int ID { get; private set; }
        public DateTime Date { get; private set; }
        public IDrawing Drawing { get; private set; }
        public IRevision Revision { get; private set; }
        public Person Person { get; private set; }

        public IssueRecord(int id, DateTime date, IDrawing drawing, IRevision revision, Person person)
        {
            if (id < 1) { id = -1; }

            ID = id;
            Date = date;
            Drawing = drawing;
            Revision = revision;
            Person = person;
        }

    }
}

以下是修订(Revision)结构体:

public struct Revision : IRevision
{        
    public int ID { get; private set; }
    public string Name { get; }
    public DateTime Date { get; set; }
    public IDrawing Drawing { get; }
    public IDrawingFile DrawingFile { get; private set; }

    public Revision(int id, string name, IDrawing drawing, DateTime date, IDrawingFile drawingFile)
    {
        if (name == null) { throw new ArgumentNullException("name", "Cannot create a revision with a null name"); }
        if (drawing == null) { throw new ArgumentNullException("drawing", "Cannot create a revision with a null drawing"); }
        if (id < 1) { id = -1; }

        ID = id;
        Name = name;
        Drawing = drawing;
        Date = date;
        DrawingFile = drawingFile;
    }

    public Revision(string name, IDrawing drawing, DateTime date, IDrawingFile drawingFile)
        : this(-1, name, drawing, date, drawingFile)
    {

    }

    public Revision(string name, IDrawing drawing)
        : this(-1, name, drawing, DateTime.Today, null)
    {

    }

    public void ChangeID(int id)
    {
        if (id < 1) { id = -1; }

        ID = id;
    }

    public void SetDrawingFile(IDrawingFile drawingFile)
    {
        DrawingFile = drawingFile;
    }
}

我的问题与绘图课程中的项目参考和修订结构中的绘图参考有关。

这似乎有点像代码异味?

这也似乎可能会在将来导致序列化方面的问题。

有更好的方法来做这个吗?

似乎有必要让一个绘图对象知道它属于哪个项目,这样如果我正在处理单个绘图对象,我就可以知道它们属于哪个项目。

同样地,每个修订本实质上是由一幅图拥有或是其部分组成的。 没有绘图,修订版就没有意义,因此它需要引用它所属的绘图?

任何建议都将不胜感激。


1
循环引用非常常见,例如在EF Code First中。这是设置外键的方式。继续推进,如果看到异常或错误发生并卡住了 - 我们可以提供帮助。 - jazb
3
这可能更适合在软件工程上提问。 - A Friend
为什么在“绘图”中需要对“项目”进行引用?还有在“修订”中需要对“绘图”进行引用吗?这些字段是否被使用过? - Spotted
@AFriend 当提及其他网站时,指出不赞成跨贴通常是有帮助的。 - gnat
1
主要问题是你正在创建类之间的互相依赖,在测试方面可能会更加困难。也许你想为DeleteDrawing编写一个测试,但是该方法签名的方式会强制该测试创建一个真正的Drawing对象。这样做可能需要您创建其他15个事物,然后设置各种属性,才能使其可删除。通过使用接口来避免这种情况。然后,您可以使用类似Moq的工具来真正简化这些测试用例。 - Adam G
显示剩余4条评论
4个回答

5
您所拥有的并不是循环引用,而是一种可以从两端进行导航的父子关系。这是正常且可接受的,不是代码异味。是的,某些序列化工具需要您提供提示,例如 Newtonsoft.Json 将需要设置 ReferenceLoopHandling.Ignore
“导航性”作为一个概念在 OO 设计中并不总是被谈论,这是不幸的,因为它正是您想要的概念。(在 UML 中,这是一个明确的术语)。
通常情况下,您并不需要从两端进行导航。通常只从父类到子类编码“父子关系”,这是非常普遍的。例如,一个发票行类很少需要一个明确的字段用于其父发票,因为大多数应用程序在检索父发票后只查看该行。
因此,设计决策并不是“没有图纸是否有修订意义?”,而是“我是否需要仅通过修订就能找到图纸?”我猜您的修订版本就像发票行一样,不需要导航到其父级。对于图纸与项目之间的关系,我的答案并不确定。(这是关于您领域的分析问题,而不是关于编码风格的问题)。
这里 OO 代码和 SQL 之间存在显着的差异。在 SQL 数据库中,必须是“修订”表持有其父级“图纸”ID 的引用。在 OO 代码中,父类几乎总是持有对子类的引用。通常情况下,子类不需要对其父类进行引用,因为您访问子类的唯一方式是已经拥有了父类。

1
谢谢Chris,你的回答非常符合我想做的事情。 我认为能够从图纸返回到父项目将是重要的。 现在,修订仍然没有意义,并且没有图纸就不存在,但它可能不需要反向导航,只有当我有一个运行的程序时才会回答。我想我现在会放弃参考,而不是拥有一个接受IRevision结构的Revise方法,我将把修订创建逻辑移动到绘图类中,这样修订将永远不会存在于绘图之外。 - moiv

2

在C#程序和数据模型中,循环引用是非常普遍的,所以不要担心它们。但是在序列化期间,必须对它们进行特殊处理。


1

是的,这是一个循环引用,也是一种代码异味。此外,我认为在这种情况下,这种异味是正确的,这不是一个良好的面向对象设计。

免责声明

  1. 正如@Rugbrød所说,这可能是C#程序的常态,我无法评论,因为我不是C#编码人员。

  2. 这种设计可能适用于非面向对象范例,例如“基于组件”或过程性编程。

如果这是您的代码上下文,那么您可以忽略这种异味。

细节

主要问题是您正在对数据进行建模,而不是行为。您首先想要正确的“数据”,然后再考虑要在其上实现的实际功能。像显示图纸、存档等。您还没有这些功能,但这已经在您的脑海中了,对吧?

The OO方法(尽管并非所有人都同意)是建模行为。如果您希望存档您的绘图,则实现Drawing.Archive()。我不是指设置标志,而是真正将其复制到冷存储或其他地方。这是您的应用程序应该执行的真正业务功能。
如果您这样做,那么您会发现,没有相互需要的行为,因为那显然是一个行为。可能发生的情况是,两个行为需要第三个抽象行为(有时称为依赖反转)。

谢谢Robert。是的,目前主要是建模数据,很快我将添加行为,这可能会影响数据建模方式(很有可能)。我也可以看到先建模行为的好处,这可能会自动回答一些数据建模问题。 - moiv

0

我认为这里唯一的问题是Drawing.CurrentRevision。

除此之外,一个Revision属于一个Drawing,而一个Drawing又属于一个Project

CurrentRevision并不是Drawing的属性,它只是'Revisions'列表中的一个快捷方式。

将其更改为方法GetCurrentRevision()和属性CurrentRevisionID如何?这样就很明显了,GetCurrentRevision不应该被序列化,但ID应该。


我想要使用属性drawing.CurrentRevision来访问当前版本。 可能会出现这样的情况:你得到了版本“A”,然后是版本“B”,接着是版本“C”。然后客户决定要坚持使用版本“B”。 我希望版本“C”和所有相关信息都能保留下来作为历史记录,但我确实需要一个属性来知道哪个是实际的当前版本。 在程序本身中,我试图避免使用ID来链接对象,因为ID可能在对象提交到数据库之前不可用。 - moiv

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