【问题标题】:How to deal with circular references?如何处理循环引用?
【发布时间】:2011-04-12 19:03:51
【问题描述】:

如果我有这两个项目:

MyCompany.ERP.Billing
MyCompany.ERP.Financial

账单财务询问/发送信息,反之亦然。两者都太大了,所以我不想把它们放在一个项目中。 Visual Studio 不允许循环引用。你会怎么处理呢?

【问题讨论】:

  • 虽然您可以使用已经提到的几个技巧,但这听起来像是一个架构问题。您如何区分帐单和财务?我觉得在这里划清界限有些困难,我的感觉是“财务”永远不应该指计费,因为它不那么抽象。是否有可能某些项目驻留在错误的项目中?究竟什么是“太大”?
  • 这只是一个例子。目的是了解如何按照良好的 OOP 实践创建 ERP,因为循环引用在这类软件中非常常见,因为它非常庞大并且分为许多模块。
  • 循环引用来自有缺陷的设计,而不是来自领域复杂性。没有双关语的意思,我不是说这就是这里的情况。 ERP 不是您可以在开发过程中开发的东西,您需要在编写一行代码之前对您要完成的工作有一个非常清晰的认识。
  • 我感觉项目之间的循环引用表明了灾难性的架构缺陷。每当我遇到这个问题时,我都不得不坐下来重新思考我的抽象。我总是在它们中发现了导致循环依赖的错误。想想法律:“你有罪!” - “这是什么意思” - “如果你被定罪,你就是有罪的!” - “但为什么我被定罪” - “因为你有罪”。现在我们有了 Kafka,而您的代码中不想使用 Kafka...
  • @Eduardo:简单地说,因为一个程序集必须在任何引用它的程序集之前完全构建和运行。类也不一样。理论上,程序集中的类可以按任意顺序构建,因为它们被视为单个单元的一部分。没有比程序集更高的实际单元(解决方案文件是 Visual Studio 概念,与生成的 IL 无关)。

标签: .net visual-studio architecture circular-reference


【解决方案1】:

从您的类中提取接口并将它们放入从BillingFinancial 项目引用的核心项目中。然后,您可以使用这些接口在程序集之间共享数据。

这只允许您在这 2 个程序集之间传递对象,但您不能从另一个程序集创建对象,因为您实际上没有开始的引用。如果您希望能够创建对象,则需要在这两个项目之外的工厂来处理对象创建。

我会将需要在BillingFinancial 之间来回共享数据的业务逻辑提取到另一个项目中。这会让事情变得更容易,并且会让您免于使用各种使可维护性成为噩梦的技巧。

【讨论】:

  • 我如何在 Billing 中创建金融对象的实例?示例:IAccountPayable a = new ???()
  • 应在共享程序集中定义应用作参数和返回值以在帐单和财务之间传递的数据传输对象以及服务接口。理想情况下,服务实现将通过依赖注入框架构建,并通过构造函数参数提供给依赖它们的类。
  • 我同意这是一种可能性,但它需要使用(智能)ServiceLayer 和 DTO。但是,如果 OP 正在实现 DomainModel,那将不起作用。但是,DomainModel 是否可行取决于很多标准,您不能总是选择它们作为 DTO,在这种情况下,整个方法将导致繁琐的代码。让我想起了“企业集成模式”带来的许多危险......
  • 此解决方案的主要缺点是接口破坏了封装:接口根据定义是公共的,您需要公开的内容比您想要的要多得多。想想不可变的类、值类型等。此外,接口表明存在不同的实现,例如IOrder - 有不同的类型顺序吗? IOrderItem 等也是如此 - 您的代码变得缺乏表现力且难以维护。
【解决方案2】:

项目规模过大应该不是问题。您可以使用名称空间和源代码的不同文件夹来保持代码结构。在这种情况下,循环引用不再是问题。

【讨论】:

  • -1。虽然该陈述准确,但它没有抓住问题的重点。
  • 我在回答“两者都太大,所以我不想把它们放在一个项目中”的问题中的陈述。我认为使用文件夹和命名空间是解决这个问题的正确方法。当需要独立部署应用程序的各个部分时,应该创建额外的程序集。
  • 大型项目的一个问题是很多人对.csproj文件进行更改,导致合并太多。
  • @PhilB:除了部署之外,创建其他项目还有其他原因。你是说如果可能的话,整个应用程序应该是一个且只有一个项目?我认为这对其他一些问题视而不见,其中最重要的是适当的 SOC。
  • +1 试图让它恢复中立。当循环引用弹出时,至少应该考虑答案(以及 cmets)。这不值得投反对票,IMO。
【解决方案3】:

提到接口的答案是正确的 - 但如果您需要能够从两个项目中创建这两种类型,您要么需要将工厂移植到另一个项目中(这也将引用接口项目,但可能是被你的两个主要项目引用)或显着改变你正在使用的结构。

这样的事情应该可以工作:

Finance: References Billing, Interfaces, Factory
Billing: References Finance, Interfaces, Factory
Factory: References Interfaces

Factory 将有一个 BillingFactory.CreateInstance() As Interfaces.IBilling 以及实现 Interfaces.IBilling 的抽象 Billing 类。

我能看到的唯一问题是,如果您在实例化一个对象时需要做一些聪明的事情,并且不希望该逻辑最终出现在一个单独的项目中 - 但由于您没有提到任何要实例化的聪明逻辑,这个应该够了

【讨论】:

  • 你的意思是这样的? MyCompany.ERP.Factories.Billing.CreateInstance(typeof(IAccountPayable))
  • 是的 - 然后工厂负责创建您的具体实例并将其返回。这可以从任何其他位置调用。实现 IAccountPayable 的抽象 AccountPayable 类的存在取决于它的复杂程度——如果它只是一个 POCO,它可以存在于简单的地方(即,而不是接口项目,有一个公共或实体项目)。如果它很复杂并且有自己的逻辑,那么您将需要稍微重组——因为您不希望所有的业务逻辑都分散在常见的项目中......
  • 我们使用:一个包含“经理”的 Project.BusinessLogic 程序集。管理器是处理 Project.Entities 程序集中的实体的类 - UI 与拥有操作实体的逻辑的管理器对话。管理者可以操纵或返回实体。在我们的案例中,实体是非常简单的 POCO。管理器还有一个 CreateInstance() 方法,它返回一个新实体。
  • 上述评论中的解决方案可能不适合您的情况 - 正如我所说,这取决于您的逻辑所在。
  • 但是,如果 ManagerA 在某个时候调用 ManagerB 而 ManagerB 调用 ManagerA,你就会遇到同样的问题。
【解决方案4】:

This solution 最终可能成为循环引用问题的解决方法。基本上,您在代码周围使用 #if 逻辑,除非存在引用,否则不会编译,并且仅在需要的程序集存在时才在项目文件中使用条件编译来定义变量。因此,在第一次从源代码下载时,或者在解决方案清理之后,您必须编译两次。随后的构建/重建只需要正常的 1 个构建。这样做的好处是您永远不必手动注释/取消注释 #define 语句。

【讨论】:

    猜你喜欢
    • 2019-02-26
    • 1970-01-01
    • 2015-03-02
    • 2014-04-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多