3.3 Code Decoupling & Abstractions(整理版)
原始笔记: Code Decoupling.md 原始教程: 3.3 Code Decoupling & Abstractions created: 2026-07-27 15:19 整理说明: 原笔记指定“整理原始教程的练习和解答”。本版本保留该边界,整理教程中的五组练习与参考解答,并只加入完成练习所需的概念说明。
内容简要概括
代码解耦通过拆分职责并减少组件间依赖,使代码更容易理解、测试和维护。五组练习从提取数据加载函数开始,逐步引入类、接口和多态,再用 JSON 数据源与 mock 证明:只要不同实现遵守同一接口,分析逻辑就不必随数据来源变化。
代码解耦、Abstraction、Encapsulation、Interface、Polymorphism、CSVDataSource、JSONDataSource、Mock、依赖注入、可测试性
目录
- 1. 练习所需概念
- 2. 练习一:分离数据加载与分析
- 3. 练习二:用类抽象数据加载
- 4. 练习三:识别数据源接口
- 5. 练习四:增加 JSON 数据源
- 6. 练习五:使用 Mock 测试
- 7. 五组练习的递进关系
1. 练习所需概念
1.1 Code Decoupling
代码解耦是把软件拆分成更小的组件,并降低组件之间的相互依赖。如果一个组件发生变化时,另一个组件不必同步修改,就说明两者的耦合较低。
松耦合通常带来三个直接好处:
- 更容易理解:一次只需要关注一个较小组件;
- 更容易测试:可以独立测试组件,或用替代实现隔离外部依赖;
- 更容易维护:修改更容易限制在单一职责范围内。
1.2 Abstraction 与 Encapsulation
Abstraction 把实现细节隐藏在接口之后,让调用者关注“它能做什么”,而不是“它内部如何完成”。
Encapsulation 把数据与操作这些数据的方法组织在同一组件中,并限制外部直接依赖内部表示。
可以简记为:
Encapsulation:隐藏内部信息;Abstraction:隐藏实现方式。
1.3 Interface 与 Polymorphism
接口描述一个组件可以执行哪些操作、需要哪些参数、返回什么结果。调用者只依赖接口,不必依赖某个具体实现。
Polymorphism 允许不同类型通过同一接口被使用。例如,CSV 数据源和 JSON 数据源可以使用不同的内部实现,但都提供 load_inflammation_data();分析函数因此可以用同一种方式处理两者。
2. 练习一:分离数据加载与分析
2.1 练习
修改 compute_data.py:
- 从
analyse_data()中提取数据加载逻辑; - 新建
load_inflammation_data(dir_path); - 让该函数读取指定目录中的炎症 CSV 文件;
- 返回由二维 NumPy 数组组成的列表;
- 让
analyse_data()调用新的加载函数。
目标是把“从文件读取数据”和“对数据执行分析”分成两个职责。
2.2 参考解答
def load_inflammation_data(dir_path):
data_file_paths = glob.glob(
os.path.join(dir_path, "inflammation*.csv")
)
if len(data_file_paths) == 0:
raise ValueError(
f"No inflammation CSV files found in path {dir_path}"
)
data = map(models.load_csv, data_file_paths)
return list(data)
analyse_data() 只负责协调加载、计算与展示:
def analyse_data(data_dir):
data = load_inflammation_data(data_dir)
means_by_day = map(models.daily_mean, data)
means_by_day_matrix = np.stack(list(means_by_day))
daily_standard_deviation = np.std(means_by_day_matrix, axis=0)
graph_data = {
"standard deviation by day": daily_standard_deviation,
}
views.visualize(graph_data)
这一修改完成了第一步职责分离,但 analyse_data() 仍然通过固定函数依赖 CSV 加载方式。
3. 练习二:用类抽象数据加载
3.1 练习
在 compute_data.py 中声明 CSVDataSource:
- 构造函数接收数据目录路径;
- 把
load_inflammation_data()变为该类的方法; - 在分析函数之外创建
CSVDataSource实例; - 把数据源对象传给
analyse_data()。
修改后的分析函数应只向数据源请求数据:
def analyse_data(data_source):
data = data_source.load_inflammation_data()
...
3.2 参考解答
class CSVDataSource:
"""Load inflammation data from CSV files in a directory."""
def __init__(self, dir_path):
self.dir_path = dir_path
def load_inflammation_data(self):
data_file_paths = glob.glob(
os.path.join(self.dir_path, "inflammation*.csv")
)
if len(data_file_paths) == 0:
raise ValueError(
f"No inflammation CSV files found in path {self.dir_path}"
)
data = map(models.load_csv, data_file_paths)
return list(data)
控制器负责创建具体数据源:
data_source = CSVDataSource(os.path.dirname(infiles[0]))
analyse_data(data_source)
分析函数接收数据源对象:
def analyse_data(data_source):
data = data_source.load_inflammation_data()
...
现在,analyse_data() 不再知道数据来自哪个目录,也不再直接依赖 CSV 文件匹配逻辑。具体加载方式被封装在 CSVDataSource 中。
4. 练习三:识别数据源接口
4.1 练习
识别 CSVDataSource 与 analyse_data() 之间的接口。需要回答:
analyse_data()必须调用哪个方法;- 该方法接收哪些参数;
- 该方法返回什么。
4.2 参考解答
两者之间的接口是:
load_inflammation_data()
参数:无
返回:list[2D NumPy array]
也就是说,传入 analyse_data() 的任意对象都应当:
- 提供
load_inflammation_data()方法; - 调用该方法时不需要额外参数;
- 返回一个列表,其中每个元素都是患者炎症数据的二维 NumPy 数组。
analyse_data() 只依赖这一行为约定,而不依赖对象一定是 CSVDataSource。这就是后续替换数据来源的基础。
5. 练习四:增加 JSON 数据源
5.1 练习
新增一个从 JSON 文件加载患者数据的类,并提供同样的 load_inflammation_data() 方法。
假设 models.py 中提供以下加载函数:
def load_json(filename):
"""Load NumPy arrays from a JSON document."""
with open(filename, "r", encoding="utf-8") as file:
data_as_json = json.load(file)
return [
np.array(entry["observations"])
for entry in data_as_json
]
运行时应根据输入文件扩展名选择 CSV 或 JSON 数据源。
5.2 参考解答
class JSONDataSource:
"""Load inflammation data from JSON files in a directory."""
def __init__(self, dir_path):
self.dir_path = dir_path
def load_inflammation_data(self):
data_file_paths = glob.glob(
os.path.join(self.dir_path, "inflammation*.json")
)
if len(data_file_paths) == 0:
raise ValueError(
f"No inflammation JSON files found in path {self.dir_path}"
)
data = map(models.load_json, data_file_paths)
return list(data)
控制器根据扩展名选择实现:
_, extension = os.path.splitext(infiles[0])
if extension == ".json":
data_source = JSONDataSource(os.path.dirname(infiles[0]))
elif extension == ".csv":
data_source = CSVDataSource(os.path.dirname(infiles[0]))
else:
raise ValueError(
f"Unsupported data file format: {extension}"
)
analyse_data(data_source)
CSVDataSource 和 JSONDataSource 的内部实现不同,但都遵守相同接口。因此,选择数据源的逻辑发生变化时,分析代码不需要修改。
6. 练习五:使用 Mock 测试
6.1 练习
在 tests/test_compute_data.py 中添加测试,用 mock 对象代替真实数据源:
- 从
unittest.mock导入Mock; - 创建一个数据源 mock;
- 配置
load_inflammation_data()的固定返回值; - 把 mock 传给
analyse_data(); - 验证分析函数可以在不读取真实文件的情况下运行。
练习重点不是验证图形是否正确,而是验证 analyse_data() 能否通过约定接口使用替代数据源。
6.2 参考解答
from unittest.mock import Mock
def test_analyse_data_mock_source():
from inflammation.compute_data import analyse_data
data_source = Mock()
mock_data = [
[0, 2, 0](#broken-link-0%2C%202%2C%200),
[0, 1, 0](#broken-link-0%2C%201%2C%200),
]
data_source.load_inflammation_data.return_value = mock_data
analyse_data(data_source)
测试不需要创建 CSV 或 JSON 文件,因为 mock 直接实现了测试所需的接口行为:
data_source.load_inflammation_data.return_value = mock_data
这种替代方式隔离了文件系统依赖,让测试只关注分析函数如何使用数据源。
7. 五组练习的递进关系
五组练习形成一条连续的设计改进路径:
从大函数提取数据加载职责
→ 用类封装具体加载方式
→ 明确分析函数依赖的最小接口
→ 用第二种实现验证多态
→ 用 Mock 验证可替换性和可测试性
最终结构可以概括为:
Controller
├── 根据输入选择 CSVDataSource 或 JSONDataSource
└── 把数据源传给 analyse_data()
analyse_data()
└── 只依赖 load_inflammation_data() 接口
Tests
└── 使用 Mock 提供同一接口的替代实现
核心不是“必须使用类”,而是让分析逻辑只依赖稳定、最小的接口,使数据加载、分析与测试能够独立变化。