如何处理循环引用?

29

如果我有这两个项目:

MyCompany.ERP.Billing
MyCompany.ERP.Financial

计费财务请求/发送信息,反之亦然。由于两者过于庞大,因此我不想将它们放在一个项目中。 Visual Studio不允许循环引用。你会如何处理?


3
尽管您可以采用已经提到的几种技巧,但这听起来像一个架构问题。您如何区分计费和财务?我发现在这里画出界限有些困难,我的感觉是“财务”不应该与计费相关,因为它要抽象得多。可能有一些项目被错误地放置了吗?什么东西被认为是“过于庞大”? - mnemosyn
1
这只是一个例子。意图是了解如何遵循良好的面向对象编程实践创建ERP,因为在这种类型的软件中,循环引用非常普遍,因为它非常庞大并分成许多模块。 - Eduardo
3
循环引用来自设计缺陷,而不是领域复杂性。没有恶意,我并不是在说这里的情况就是这样。ERP并不是你可以边开发边进行的项目,你需要在写一行代码之前就有非常清晰的目标图像。 - devnull
4
我感觉项目之间存在循环引用是架构上的重大缺陷迹象。每当我遇到这个问题时,我就必须坐下来重新思考我的抽象概念。我总是会发现其中的错误,导致了循环依赖。可以类比法律: "你有罪!" - "这是什么意思" - "如果你被定罪,你就有罪!" - "但是我为什么被定罪" - "因为你有罪"。现在我们有卡夫卡,你不想让卡夫卡出现在你的代码里... - mnemosyn
1
@Eduardo:简单来说,因为在引用某个程序集之前,必须完全构建并运行该程序集。但对于类而言则不然。理论上,程序集中的类可以按任意顺序构建,因为它们被视为单个单元的一部分。实际上,没有比程序集更高的单元(解决方案文件是Visual Studio的概念,与生成的IL无关)。 - Adam Robinson
显示剩余6条评论
4个回答

26

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

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

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


我该如何在Billing内创建Financial Objects的实例?例如:IAccountPayable a = new ???() - Eduardo
应该在共享程序集中定义用作参数和返回值在计费和财务之间传递的数据传输对象,以及服务接口。最好通过依赖注入框架构建服务实现,并通过构造函数参数提供给依赖于它们的类。 - StriplingWarrior
我同意这是一个可能性,但它需要使用(智能)ServiceLayer和DTO。然而,如果OP正在实现DomainModel,那将行不通。但是,无论是否选择DomainModel作为前进方向都取决于许多标准,你不能总是选择它们成为DTOs,否则整个方法将会导致笨重的代码。这让我想起了“企业集成模式”带来的许多危险。 - mnemosyn
这种解决方案的主要缺点是接口破坏了封装性:接口从定义上就是公共的,你需要暴露比你想要的更多。考虑不可变类、值类型等。此外,接口表明存在不同的实现,例如 IOrder - 是否存在不同类型的订单?对于 IOrderItem 等也是如此 - 你的代码变得难以表达和维护。 - mnemosyn

5

项目过于庞大不应该成为问题。您可以使用命名空间和不同的源代码文件夹来保持代码结构化。这样,循环引用就不再是问题。


1
虽然这个语句是“准确的”,但它忽略了问题的重点。 - Adam Robinson
1
我是在回应问题中的陈述“两者都太大了,所以我不想将它们放在一个项目中。”。我认为使用文件夹和命名空间是解决这个问题的正确方法。只有在需要独立部署应用程序的部分时,才应创建额外的程序集。 - PhilB
3
大型项目的一个问题是许多人在.csproj文件中进行更改,导致合并次数过多。 - Eduardo
2
@PhilB:除了部署之外,创建其他项目的原因还有很多。你是说整个应用程序如果可能的话应该只有一个项目吗?我认为这是对许多其他问题视而不见,其中最重要的是正确的SOC。 - Adam Robinson
5
+1表示为了使其返回中性而尝试增加一点,当出现循环引用时,答案(以及评论)至少应该被考虑。在我看来,这并不值得被踩。 - xofz
显示剩余7条评论

3
提到接口的答案是正确的 - 但如果您需要能够从两个项目中创建两种类型,您将需要将工厂分配到另一个项目中(该项目也引用接口项目,但可以被您的两个主要项目引用),或者显著更改您正在使用的结构。
类似以下内容应该可以工作:
Finance: References Billing, Interfaces, Factory
Billing: References Finance, Interfaces, Factory
Factory: References Interfaces

工厂将拥有BillingFactory.CreateInstance() As Interfaces.IBilling和实现Interfaces.IBilling的抽象计费类。

我唯一能看到的问题是,如果您需要在实例化对象时执行某些聪明的操作,并且不希望将该逻辑放在单独的项目中 - 但由于您没有提到任何聪明的实例化逻辑,因此这应该足够了。


1
你的意思是像这样吗? MyCompany.ERP.Factories.Billing.CreateInstance(typeof(IAccountPayable)) - Eduardo
是的 - 然后工厂负责创建您的具体实例并返回它。这可以从任何其他位置调用。实现IAccountPayable的抽象AccountPayable类所在的位置取决于其复杂程度 - 如果它只是一个POCO,则可以放在简单的地方(即,不是接口项目,而是通用或实体项目)。如果它很复杂并且有自己的逻辑,您需要稍微重新组织 - 因为您不希望所有业务逻辑都散布在通用项目中... - Basic
我们使用一个Project.BusinessLogic程序集,其中包含“管理器”。 管理器是处理Project.Entities程序集中实体的类 - UI与持有操作实体逻辑的管理器交互。 管理器可以操作或返回实体。 在我们的情况下,实体非常简单,是POCOs。 管理器还具有CreateInstance()方法,该方法返回一个新实体。 - Basic
以上评论中的解决方案可能不适用于您的情况 - 正如我所说,这取决于您的逻辑所在的位置。 - Basic
但是如果ManagerA在某个时间点调用ManagerB,而ManagerB又调用ManagerA,你就会遇到相同的问题。 - Eduardo
真的 - 但你需要在某个地方划定界限。你不能有两个相互引用且可以相互操纵的对象 - 这是不可能编译的。你要么需要使用接口进行抽象,要么通过设计避免这个问题。我们解决这个问题的方法是拥有“服务”,它们引用多个管理器 - 即管理器负责处理特定实体,服务操纵多个实体。当然,同样的,服务也不能相互引用 - 但这对我们来说并没有成为问题... - Basic

0

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


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