【问题标题】:Circular reference — architecture question循环参考——架构问题
【发布时间】:2018-11-02 11:43:18
【问题描述】:

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

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

会有很多建设项目(工地),每个项目中都会有很多图纸。每张图纸都会有几个修订版(当它们发生更改时,会创建一个新修订版)。

这是我的项目类

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

    }
}

这是修订结构

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

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

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

同样,每个修订版本质上都是由图纸“拥有”或属于图纸的一部分。没有图纸,修订就没有意义,所以它需要参考它所属的图纸?

任何建议将不胜感激。

【问题讨论】:

  • 循环引用非常常见,例如在 EF Code First 中。这就是设置外键的方式。继续前进,如果您发现异常或错误发生并卡住 - 我们可以提供帮助
  • 这可能更适合在software engineering上提问
  • 为什么需要在 Drawing 中引用 Project ?并在 Revision 中引用 Drawing ?这些字段是否曾经使用过?
  • @Spotted 我猜会涉及到对象关系映射,这些映射将用作外键作为导航属性。如果不是,那么就像您提到的那样,这些看起来有点多余。
  • 主要的气味是你正在创建你的类之间的相互依赖关系,这会使测试变得更加困难。您可能想为 DeleteDrawing 编写一个测试,但读取签名的方式会强制这样的测试创建一个真实的 Drawing 对象。这样做可能需要您创建 15 项其他内容,然后设置各种属性,然后才能将其删除。通过使用接口来避免这种情况。然后你可以使用像 Moq 这样的东西来真正简化这些测试用例。

标签: c# oop circular-reference


【解决方案1】:

你所拥有的不是循环引用,而是两个示例

父子关系,可从两端导航

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

可导航性作为一个概念在 OO 设计中并不总是被谈论,这很不幸,因为它只是你想要的概念。 (这是 UML 中的一个明确术语)。

您通常不需要从两端导航。 父子关系通常只在父子之间进行编码。这真的很常见。例如,invoiceline 类很少需要其父级 invoice 的显式字段,因为大多数应用程序只会在检索父级发票后查看该行。

所以设计决定不是,

“没有图纸的修订有意义吗?”

但是

“我是否需要找到只给定修订版的图纸?”

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

OO 代码和例如 SQL 之间存在显着差异。在 SQL 数据库中,它必须是 revision 表,它包含对其父 drawing id 的引用。在 OO 代码中,父类几乎总是持有对子类的引用。孩子通常不需要引用他们的父母,因为您访问孩子的唯一方法是已经拥有父母。

【讨论】:

  • 谢谢克里斯,您的回答非常反映了我正在尝试做的事情。我认为能够从绘图导航回父项目非常重要。现在修订仍然没有意义,没有图纸就不能存在,但它可能不需要反向导航,我可能只会在我有一个正在运行的程序时回答这个问题。我想我现在会放弃引用,而不是使用采用 IRevision 结构的 Revise 方法,而是将修订创建逻辑移到绘图类中,以便修订永远不会存在于绘图之外
【解决方案2】:

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

【讨论】:

    【解决方案3】:

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

    免责声明

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

    2. 这种设计可能适用于非 oo 范例,例如“基于组件”或过程式编程。

    所以你可以忽略这种气味,我猜这是你代码的上下文。

    详情

    主要问题是您在建模数据,而不是行为。您首先要拥有正确的“数据”,然后您将继续考虑要在此基础上实现的实际功能。比如显示图纸、存档等。你还没有这些,但你已经想到了,对吧?

    OO 方法(诚然并非所有人都同意)是对行为进行建模。如果您希望您的图纸存档,请实施Drawing.Archive()。我的意思不是设置一个标志,我的意思是真的把它复制到冷库或其他什么地方。您的应用程序应该执行的真正业务功能。

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

    【讨论】:

    • 谢谢罗伯特。是的,在这个阶段它主要是建模数据,很快我将添加行为,这可能(很可能)影响数据建模的方式。我可以看到首先对行为进行建模的好处,这也可能会自动回答一些数据建模问题。
    【解决方案4】:

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

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

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

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

    【讨论】:

    • 我希望能够使用属性 drawing.CurrentRevision 来访问当前版本。可能发生的情况是您将获得修订版“A”,然后是修订版“B”,然后是修订版“C”。然后客户决定他们要坚持使用修订版“B”。我希望修订版“C”和所有相关信息留在那里作为历史记录,但我确实需要一个属性,所以我知道哪个是实际的当前修订版。在程序本身中,我试图避免使用 ID 来链接对象,因为在将对象提交到数据库之前,ID 可能不可用
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-01-13
    • 1970-01-01
    • 2019-08-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多