循环引用——架构问题

Circular reference — architecture question

这可能是一个非常初学者的问题,但我搜索了很多主题并没有真正找到相同的情况,虽然我确信这种情况一直发生。

我的 project/program 将跟踪建筑项目图纸的更改,并在图纸更改时向人们发送通知。

将会有很多建筑项目(工地),而每个建筑项目又会有很多图纸。每张图纸都会有几个修订版(随着它们的更改,会创建一个新修订版)。

这是我的项目Class

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

这是我的图Class

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

    }
}

这里是修订结构

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

我的问题是关于绘图中的项目参考 class 和修订结构中的绘图参考。 好像有点代码味? 它似乎也可能在未来导致序列化问题。 有更好的方法吗?

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

同样,每个修订本质上都是 "owned" 绘图或绘图的一部分。没有图纸的修订没有意义,因此它需要对其所属图纸的引用?

如有任何建议,我们将不胜感激。

一般来说,循环引用在 C# 程序和数据模型中是很正常的,所以不用担心。但在序列化期间必须对其进行特殊处理。

你所拥有的与其说是循环引用,不如说是

的两个例子

parent-child 关系,从两端可导航

是的,这很正常,可以接受table,不,这不是代码味道。是的,一些序列化工具需要你提示。例如Newtonsoft.Json 需要 ReferenceLoopHandling.Ignore 设置。

Navigability 作为一个概念在 OO 设计中并不总是被谈论,这很不幸,因为它只是你想要的概念。 (它是 UML 中的一个显式术语)。

您通常不需要两端的导航能力。 Parent-child 关系 通常仅从 parent 编码到 child。这真的很常见。例如,invoiceline class 很少需要其 parent invoice 的显式字段,因为大多数应用程序只会在检索 parent 发票后查看该行。

所以设计决定不是,

"Does a revison make sense without a drawing?"

但是

"Will I ever need to find a drawing given only a revision?"

我的猜测是您的修订就像发票行,不需要导航到它们的 parent。图纸<——>项目关系的答案对我来说并不明显。 (这是一个关于你的领域的分析问题,而不是关于编码风格的问题)。

OO 代码与 SQL 等代码之间存在显着差异。在 SQL 数据库中,必须是 revision table 保存对其 parent drawing id 的引用。在 OO 代码中,parent class nearly-always 持有对 children 的引用。 children 通常不需要对他们的 parent 的引用,因为访问 children 的唯一方法是已经拥有 parent.

是的,这是一个循环引用,是的,这是一种代码味道。此外,我确实认为这种情况下的气味是正确的,这不是一个好的 OO 设计。

免责声明

  1. 正如@Rugbrød 所说,这对于 C# 程序来说可能是正常的,我不能对此发表评论,我不是 C# 编码员。

  2. 这种设计对于非 oo 范式可能没问题,例如 "component-based" 或过程编程。

所以你可以忽略这种气味我想如果这是你的代码的上下文。

详情

主要问题是您建模的是数据,而不是行为。你想先让 "data" 正确,然后你会继续考虑你想要在其上实现的实际功能。比如展示图纸,归档等等。你还没有这些,但你心里有数吧?

面向对象的方法(诚然不是每个人都同意)是对行为建模。如果你想存档你的图纸,那么实施 Drawing.Archive()。我的意思不是设置标志,而是真正将其复制到冷藏库或其他任何地方。您的应用程序应该执行的真正业务功能。

如果你这样做,你会发现,没有任何行为是相互需要的,因为那显然是一种行为。可能发生的情况是,两种行为可能需要第三种抽象行为(有时称为依赖倒置)。

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

否则,一个Revision属于一个Drawing,属于一个Project

CurrentRevision 并不是绘图的真正 属性,它是其 'Revisions' 列表中一项的快捷方式。

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