如何为需要初始化的具体类实现依赖反转和接口分离?
背景
根据我的理解,SOLID面向对象编程中的依赖倒置原则和接口隔离原则告诉我们,编写程序时要关注接口,而不是内部细节。因此,我正在尝试用Python开发一个简单的股票市场数据收集器,下面是一个大致的对象图,其中main
封装了应用程序的业务逻辑、用户输入处理等。我们来看看这个图的意思:
- 粉色代表具体的功能或类,绿色代表抽象类
- 空心箭头表示子类或实现关系,实心箭头表示使用关系(遵循Robert Martin的清洁架构的约定)
所以,主函数使用一个抽象接口来获取某个股票符号的价格。这个抽象类看起来像这样:
#!/usr/bin/env python3
# encoding: utf-8
"""
Defines the abstract stock price reader
"""
from abc import ABC, abstractmethod
class StockPriceReader(ABC):
"""Defines the general tick reader interface."""
@abstractmethod
def get_price(self, symbol:str)->float:
"""
Gets the price of a stock represented by the symbol, e.g
when symbol='AAPL', it gets the Apple Inc stock price.
"""
raise NotImplementedError
TickReaderConcrete
类实现了内部细节,通过类似Bloomberg或交易所API的调用来获取实际的股票价格。进行API调用所需的凭证必须是内部细节的一部分。这里不展示代码,因为实现起来相对简单。
困惑
根据上面的简单类依赖图,书中(清洁架构)似乎暗示(我在这里强调):
主块甚至不应该知道存在TickReaderConcrete。
至少,这是我对书中内容的理解,因为从main
到TickReaderConcrete
之间没有箭头,如果我错了请纠正我。
但是当我编写main.py
时,我无法假装TickReaderConcrete
不存在,换句话说,似乎main
不得不知道TickReaderConcrete
的存在,当代码看起来像这样:
#!/usr/bin/env python3
# encoding: utf-8
"""
The main function to invoke the stockprice reader
"""
from tickreader import TickReaderConcrete
...
if __name__ == '__main__':
# This line gives rise to the alternative class diagram below
reader=TickReaderConcrete(...)
# After initialised, we can use the interface permitted by the abstract base class
reader.get_price(symbol='IBM')
问题
那么,如何确保main
不知道具体读取器的存在?如果main
根本不导入具体读取器,它甚至无法实例化具体读取器对象,而抽象读取器也无法初始化。那么,基本上如何组织代码以正确实现上面的对象图呢?
稍微改写的问题
即使抽象基类暴露了必要的公共方法,至少在初始化时也需要知道具体子类的存在。具体子类能否隐藏在抽象基类后面?看看替代对象图,这就是上面代码片段所实现的。如何去掉那条虚线?
1 个回答
我读《清晰架构》已经有些年头了,但我想提出一种不同的理解。依赖倒置原则(DIP)确实是说,抽象不应该依赖于具体的实现,而是应该反过来。
定义一个高层次的接口或抽象基类,然后用一个具体的类来实现它,这个具体类包含所有的细节,这是处理这类问题的标准方法。
DIP的意思是,你的代码几乎都应该是围绕这个高层次的抽象来写的。这就是说,如果你有一些业务逻辑需要调用get_price
方法,那么它应该使用一个StockPriceReader
的抽象对象来进行交互。
不过,这条规则有一个例外,因为你需要在某个地方组合对象图,通常是在__main__
方法里。在这里,你会创建一个具体类的实例,然后把它传递给所有期望抽象类的其他代码。