🧩 第十章 面向对象设计:拟人化和职责分配
📄 一句话概括

循环依赖的可怕在于环里的任何一个都不能单独拿走——实测 5 个类 5 条依赖里检测出 1 个环:Item → Price → Item。

📝 本节和前面几节的关系

10.4~10.5 讲的是【依赖多少】,这一节讲依赖的形状——有环就麻烦。

一、循环依赖长什么样

A 用 B、B 用 C、C 又用 A——这就是环。 最小的是两两互用:Item 需要 Price 算价,Price 需要 Item 取类型。 实测 5 个类 5 条依赖里,正好 1 个这样的环。

二、为什么环这么危险

环里的任何一个都不能单独拿走——你抽 Item,Price 就编译不过。 它们被绑成一个整体,而这个整体会越来越大。 改的时候也不安全:改 Item 可能影响 Price,改 Price 又影响 Item,测都测不清。

三、三种破环办法

上移:把互相需要的那部分抽到一个新的类,两边都依赖它。 下移:把其中一个的职责再拆,让依赖变成单向。 只依赖接口:一方只看抽象,实现由外部注入——环就断在抽象这一层。

四、怎么发现环

人眼看三个以上的环很难,所以用工具:导入时报警、静态扫描、构建期检查。 简单办法:数一数有没有哪个模块【既被别人 import 又 import 别人】。 最好的办法是让它不可能出现:分层,只允许上层依赖下层。

五、动手 Demo:设计目标之三:避免依赖关系发生循环

用邻接表描述 5 个类的依赖。 用三色 DFS 检测环,并打印环的路径。

edges = {"Order": ["Customer", "Item"],
         "Customer": ["Address"],
         "Item": ["Price"],
         "Price": ["Item"],
         "Address": []}

cycles, color, stack = [], {}, []
def dfs(n):
    color[n] = 1
    stack.append(n)
    for m in edges.get(n, []):
        if color.get(m) == 1:
            cycles.append(stack[stack.index(m):] + [m])
        elif color.get(m, 0) == 0:
            dfs(m)
    stack.pop()
    color[n] = 2

for n in edges:
    if color.get(n, 0) == 0:
        dfs(n)
print(len(edges), sum(len(v) for v in edges.values()), len(cycles))
# 5 5 1

实跑输出:

--------------------------------------------------------------------
设计目标之三:避免依赖关系发生循环
--------------------------------------------------------------------

  循环依赖的表现是:**编译/导入时报错,或者改哪个都不敢动。**实测一个依赖图:

  类           依赖谁
  ----------------------------
  Order        Customer/Item
  Customer     Address
  Item         Price
  Price        Item
  Address      —

      类                          5 个
      依赖边                      5 条
      检测到环                    1 个
      环的路径                    Item → Price → Item

  **环只有 1 个:Item 与 Price 互相依赖。**
  **破环的办法**:把 Price 需要的那部分抽成第三方,**或者让一方只看接口。**
  **为什么环这么危险**:环里的任何一个都【不能单独拿走】——**它们被绑成一个整体。**

实测:5 个类、5 条依赖、1 个环(Item → Price → Item)。 环里的两个类不能单独拿走——它们被绑成一个整体。 破环:上移、下移、或者只依赖接口。

30 秒实验

  1. 画一张你项目的依赖图,找出所有互相 import 的地方。
  2. 挑一个环,试着用【只依赖接口】断开它。

常见误区

  1. 以为循环依赖只是编译慢。它让环里的东西无法单独拿走。
  2. 靠人眼找环。三个以上的环人眼看不出,要靠工具。
自测三问(点开对答案) 1. 循环依赖为什么危险? —— 环内任一模块都不能单独拿走,它们被绑成一个整体,改一处影响不清。 2. 实测检测结果如何? —— 5 个类 5 条依赖里 1 个环:Item → Price → Item。 3. 三种破环办法? —— 上移(抽新类)、下移(再拆职责)、只依赖接口(由外部注入实现)。

相关笔记