至少有三个易变输入或输出时才适合使用 DIP 吗?是否应始终使用?
不应把“至少有三个易变输入或输出”作为采用 DIP 的门槛,也不必在所有地方预先增加抽象。判断时要看:某个具体实现发生变化,是否会迫使本应稳定的业务逻辑一起修改。
你的数据源抽象有合理依据,因为代码已经需要在在线数据和文件数据之间切换。HistoricalPricesService 目前只是转发调用,是否值得单独保留,还要看它有没有明确的业务职责。
DIP 关注什么,为什么与数量无关?
依赖倒置原则包含两个相关要求:
- 高层模块不应依赖低层模块,两者都应依赖抽象。
- 抽象不应依赖实现细节,实现细节应依赖抽象。
在这个例子中,下载价格和读取 Parquet 文件是具体的数据获取方式。使用价格数据进行分析或计算的业务逻辑,可以只依赖“提供价格数据”这一约定。
即使只有一个在线数据源,如果测试业务逻辑时需要避免真实网络请求,引入这个边界也可能有价值。即使有三个输入来源,如果业务逻辑必须分别处理它们的不同含义,统一包装成一个接口也未必能减少复杂度。
实现数量可以帮助你发现共同点,但不能决定抽象是否成立。
当前代码体现了哪些设计?
HistoricalPricesService 接收 PricesData,而不是在内部直接创建 ApiOrMockPricesData。这使它能够接受不同的数据源实现,是朝 DIP 方向进行的设计。
通过构造函数接收依赖的做法叫依赖注入。依赖注入关注对象如何获得依赖,DIP 关注业务代码与实现细节之间的依赖方向。
因此,构造函数传参本身并不能证明整个设计满足 DIP。还要看 PricesData 是否表达了业务真正需要的能力,以及它是否让业务摆脱了具体下载库、文件路径等细节。
创建具体对象的代码仍然需要存在。将它放在程序入口或组装对象的位置即可,不必消除所有对具体类的引用。
怎样判断这个抽象是否值得保留?
可以按下面的顺序检查:
- 明确调用方需要什么:是任意
DataFrame,还是具有约定列、索引和时间范围的价格数据? - 确认已有的替换需求:你已经有在线获取和文件读取两种方式,这比猜测未来可能增加数据源更有说服力。
- 检查替换后业务逻辑是否稳定:更换数据源后,业务代码是否仍能直接使用返回结果,还是需要增加大量条件分支?
- 评估维护成本:如果一个包装类始终只有一行转发代码,也没有独立职责,可以先由调用方直接接收数据源。
Python 中不一定需要通过 ABC 才能建立这种边界。抽象基类、Protocol 或有清楚约定的鸭子类型都可以使用,具体选择取决于是否需要继承约束、静态类型检查等能力。
先把数据契约定义清楚
原代码的抽象方法没有写返回类型,可以先补上:
from abc import ABC, abstractmethod
import pandas as pd
class PricesData(ABC):
@abstractmethod
def get_data(self) -> tuple[bool, pd.DataFrame]:
pass
类型注解只说明返回一个布尔值和一个 DataFrame,还不足以保证不同实现可以互换。你还需要明确:
- 布尔值表示“来自文件”“使用了兜底数据”,还是其他含义。
DataFrame应有哪些列、索引和数据范围。- 无法提供符合约定的数据时,应返回什么或抛出什么异常。
例如,文件中的数据是否覆盖 assets_tickers 请求的资产,需要由数据源保证或校验。仅仅让两个类都有 get_data() 方法,还不能保证替换后业务行为正确。
如果调用方并不关心数据是否来自兜底来源,也可以考虑不把这个布尔值暴露给业务层。
修正当前实现中的状态问题
原来的 self.is_default 存在跨调用残留的风险:某次调用下载失败后,它会变成 True;同一个实例下一次下载成功时,代码没有将它重置为 False。
如果这个标志描述的是本次返回结果,使用局部变量更直接。可以将 ApiOrMockPricesData.get_data() 替换为:
def get_data(self) -> tuple[bool, pd.DataFrame]:
data = robust_download(self.assets_tickers)
if data is not None:
return False, data
print("[WARNING] Using fallback data")
return True, pd.read_parquet("historical_prices.parquet")
这段代码沿用原有的 robust_download 和 pd,并假设下载失败时返回 None。它没有处理下载函数抛出异常、返回空表或文件读取失败的情况。这些行为需要根据实际函数约定决定,不能仅凭函数名称推断。
是否要进一步拆分在线获取和兜底逻辑?
ApiOrMockPricesData 同时负责在线获取和失败后读取文件。目前逻辑很短,保留在一个类中可以接受。
如果以后需要组合不同的主数据源、备用数据源,或在多处复用相同的兜底策略,再把它们拆成独立组件会更有价值。没有这些需求时,不必为了形式完整而增加一组类。
当前场景中,可以保留业务与数据获取方式之间的边界。是否增加服务类、兜底包装器或更多接口,应由实际职责和变化需求决定。
备注:内容仅供参考。
