循环引用——架构问题
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 设计。
免责声明
正如@Rugbrød 所说,这对于 C# 程序来说可能是正常的,我不能对此发表评论,我不是 C# 编码员。
这种设计对于非 oo 范式可能没问题,例如 "component-based" 或过程编程。
所以你可以忽略这种气味我想如果这是你的代码的上下文。
详情
主要问题是您建模的是数据,而不是行为。你想先让 "data" 正确,然后你会继续考虑你想要在其上实现的实际功能。比如展示图纸,归档等等。你还没有这些,但你心里有数吧?
面向对象的方法(诚然不是每个人都同意)是对行为建模。如果你想存档你的图纸,那么实施 Drawing.Archive()
。我的意思不是设置标志,而是真正将其复制到冷藏库或其他任何地方。您的应用程序应该执行的真正业务功能。
如果你这样做,你会发现,没有任何行为是相互需要的,因为那显然是一种行为。可能发生的情况是,两种行为可能需要第三种抽象行为(有时称为依赖倒置)。
我认为这里唯一的问题是Drawing.CurrentRevision
否则,一个Revision
属于一个Drawing
,属于一个Project
。
CurrentRevision
并不是绘图的真正 属性,它是其 'Revisions' 列表中一项的快捷方式。
如何将其更改为方法 GetCurrentRevision()
和 CurrentRevisionID
属性?这样很明显 GetCurrentRevision 不应该被序列化,尽管 ID 是。
这可能是一个非常初学者的问题,但我搜索了很多主题并没有真正找到相同的情况,虽然我确信这种情况一直发生。
我的 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 设计。
免责声明
正如@Rugbrød 所说,这对于 C# 程序来说可能是正常的,我不能对此发表评论,我不是 C# 编码员。
这种设计对于非 oo 范式可能没问题,例如 "component-based" 或过程编程。
所以你可以忽略这种气味我想如果这是你的代码的上下文。
详情
主要问题是您建模的是数据,而不是行为。你想先让 "data" 正确,然后你会继续考虑你想要在其上实现的实际功能。比如展示图纸,归档等等。你还没有这些,但你心里有数吧?
面向对象的方法(诚然不是每个人都同意)是对行为建模。如果你想存档你的图纸,那么实施 Drawing.Archive()
。我的意思不是设置标志,而是真正将其复制到冷藏库或其他任何地方。您的应用程序应该执行的真正业务功能。
如果你这样做,你会发现,没有任何行为是相互需要的,因为那显然是一种行为。可能发生的情况是,两种行为可能需要第三种抽象行为(有时称为依赖倒置)。
我认为这里唯一的问题是Drawing.CurrentRevision
否则,一个Revision
属于一个Drawing
,属于一个Project
。
CurrentRevision
并不是绘图的真正 属性,它是其 'Revisions' 列表中一项的快捷方式。
如何将其更改为方法 GetCurrentRevision()
和 CurrentRevisionID
属性?这样很明显 GetCurrentRevision 不应该被序列化,尽管 ID 是。