【问题标题】:Circular class references bad practice?循环类引用不好的做法?
【发布时间】:2012-04-18 17:48:27
【问题描述】:

最近有人告诉我如何以 Class 1 调用 Class 2 调用 Class 1 的形式循环引用被认为是非常糟糕的做法,我就是无法理解这一点。我的意思是,如果这些课程在 2 个不同的项目中,我完全理解问题所在,但如果它们在同一个项目中,那会很糟糕吗?在某些情况下……您究竟是如何防止这种情况发生的?

例如:我有一台服务器。客户端连接到它,客户端从套接字派生或持有它,负责网络内容以及帐户ID等一些信息。当有新内容时,该客户端调用数据包处理程序,现在数据包处理程序需要来自客户端的信息,并且必须将信息发回。我将客户端传递给数据包处理程序,因此它可以调用它的发送函数等。

担心这个的人认为这是上面提到的不好的做法,并试图根本不这样做,尽管我很少看到服务器,尤其是大型服务器,将所有数据包处理保留在客户端类中.此外,您可能会比处理程序更进一步,并调用更多类。将所有内容保留在客户端内部是一团糟。那么……真的是不好的做法吗?

如果是,人们将如何绕过它?要做到这一点,您需要在 Client 中或多或少复杂的对象集合,您可以将其传递下来,而无需再次调用 Client 的函数...

就像我说的,我无法真正解决这个“问题”。有人可以帮帮我吗?

【问题讨论】:

  • F# 让循环类引用变得非常有趣。
  • 查看“相关”问题列表。 ---------> 看起来有几个可以帮助回答这个问题。
  • 在大多数情况下,我发现我可以将此类代码重构为消费者/生产者问题,从而消除一半的耦合,从而使整体设计更加简洁。在这不起作用的情况下,我使用特定接口(无论如何我都尝试这样做);再一次,我发现这些可以最大限度地减少对核心所需方面的“循环引用”,并使程序更加清晰。

标签: c# oop dependencies


【解决方案1】:

当人们谈论循环引用时,他们通常会谈论项目/dll 引用。这些在解析引用时会出现问题,并且 Visual Studio 不允许您添加循环引用。

但您指的不是项目结构,而是架构,这里的事情有点复杂。类相互调用本身并没有什么错。事实上,这在 .NET 中的回调和事件等功能的设计中是隐含的——当您注册到一个事件时,您实际上是在调用一个类,该类稍后将使用事件处理程序回调您。

不过,这种形式的循环调用是相对解耦的。服务器没有对客户端的显式引用,而只有被调用的订阅客户端的列表。如果您没有这样做,而是让数据包处理程序,例如,持有对客户端的显式引用,那么这两个类将紧密耦合 - 数据包处理程序依赖于客户端的特定实现,反之亦然。

为什么会这样?在我看来,这违反了关注点分离原则,这是编程中最基本的概念之一。客户端应该知道如何处理客户端操作,数据包处理程序应该处理数据包操作,两者都不应该知道另一个是如何工作的,并且只能通过定义明确的特定接口进行通信。

让我们根据您的 OP 进行一个非常精简的假设情况,该情况具有循环引用: 客户端调用数据包处理程序的Send() 方法。数据包处理程序现在启动连接,然后发现它需要用户名/密码。它调用客户端上的一个方法来获取它,然后将它发送到服务器,获取响应,然后回调客户端返回它。

在这种情况下,数据包处理程序现在绑定到客户端的实现。它要求客户端有一个 GetCredentials() 方法和一个 MessageReceived 方法来回调它。现在想象一个更加解耦的场景:

客户端首先注册处理程序的 ResponseReceived 事件。现在客户端调用数据包处理程序的 Send() 方法。数据包处理程序需要身份验证,所以它失败了——它抛出一个异常或返回一个错误代码,说“无法连接”。客户端收到此响应,并再次调用,这次使用 Send(username, password) 方法。它成功,获得响应,并引发 ResponseReceived 事件,将响应发送给订阅它的任何人。

这允许数据包处理程序在其他上下文中从其他客户端重用。它允许在内部对客户端或处理程序进行更改,而对其他组件的影响较小。它使代码更简单、更容易维护。这很好。 :)

【讨论】:

    【解决方案2】:

    在某些情况下循环类引用一点也不坏。总是存在实例化问题(鸡或蛋)。永远记住,对象之间已经存在两种通信方式。您可以通过调用一个在有新消息时返回的函数来让客户端等待下一条消息。

    但这是你应该停下来思考的地方。尤其是在以下情况下(还有更多,但这些都在我的脑海中):

    • 当您通知班级时,发生了某些动作->将其设为事件。
    • 当它是一对一关系时->应该将类型合并为一个。
    • 当您的类扩展其他 -> 的功能时,继承自它而不是传递引用。

    在您的情况下,客户端 数据包处理程序。我可能不会在那里保留循环引用。我可能会为这两个类编写一个控制器,作为它们之间信息的代理。我也不会将帐户 ID 存储在客户端对象中。这有点违背我喜欢坚持的单一责任原则。我可能会得到这样的结果:

    • 客户端 -> 套接字(负责网络的东西)
    • ClientHandler(负责控制流)
      • 客户
      • 数据包处理程序
      • 其他信息
    • 数据包处理程序

    【讨论】:

      【解决方案3】:

      同一个中的类应该被认为是高度内聚的(假设你的包结构是正确的!)并且可以有更紧密的耦合。在包中,循环关系(如果需要)是可以的,但会受益于基于特定接口的设计(如之前的海报所述)。

      跨包边界,内聚自然较低,耦合应尽可能低:绝对没有循环关系。

      【讨论】:

        【解决方案4】:

        您可以尝试提取 A 在 B 上调用的功能以及 B 在 A 上调用的功能,然后将它们封装在类库 C 中,其他库可以毫无问题地调用它们。显然,C 既不能引用 A 也不能引用 B。

        【讨论】:

          猜你喜欢
          • 2023-04-04
          • 2014-01-07
          • 1970-01-01
          • 2010-09-23
          • 1970-01-01
          • 2014-02-01
          • 2013-09-04
          • 2018-10-19
          • 1970-01-01
          相关资源
          最近更新 更多