多依赖组件系统(电脑与应用)循环依赖的合理性及替代模式探讨
计算机与应用的循环依赖:是否合适?还有啥更优方案?
先直接给结论:当前的循环依赖虽然能满足基本需求,但不是长期维护的最优选择,咱们来拆解问题,再看看怎么优化。
先看现状:为啥会有循环依赖?
在你的代码里,Computer实例持有所有安装的App,而每个App又拿着整个Computer的引用——双向依赖就这么形成了。短期跑起来没问题,但踩坑的地方不少:
- 耦合太死:比如
PdfReader明明只需要硬盘和屏幕,却要持有整个Computer对象,完全违反了「最少知道原则」,以后改Computer的结构,所有应用都可能受影响。 - 测试头疼:想单独测
PdfReader?不行,必须先创建一个完整的Computer实例,连带着初始化所有硬件,没法单独mock某个组件(比如模拟一个不打印的屏幕来测逻辑)。 - 扩展性差:以后加个摄像头硬件,或者把硬盘换成SSD,所有依赖
Computer的应用都得跟着调整,太麻烦。
所以这种循环依赖,能用但绝对算不上合适。
更优方案:依赖注入+接口抽象
核心思路就是让应用只拿自己需要的东西,别抱着整个计算机不放。具体怎么做?
第一步:给硬件加抽象接口
先把每个硬件的行为抽象成接口,比如DisplayDevice管显示,AudioDevice管发声,StorageDevice管存储。这样应用依赖的是接口,不是具体的Screen或HardDrive,以后换硬件只要符合接口就行。
第二步:计算机只给应用递需要的组件
安装应用的时候,计算机不用把自己整个传过去,而是把应用需要的硬件组件单独传进去——这就是依赖注入。
第三步:应用彻底和计算机解绑
应用不再持有Computer的引用,只保留自己需要的硬件接口实例,职责更单一。
优化后的代码示例
from abc import ABC, abstractmethod # 先定义硬件行为的抽象接口 class DisplayDevice(ABC): @abstractmethod def display(self, content): pass class AudioDevice(ABC): @abstractmethod def play(self, sound): pass class StorageDevice(ABC): @abstractmethod def write(self, data): pass # 具体的硬件实现 class Screen(DisplayDevice): def display(self, content): print(f'Displaying "{content}"') class Speaker(AudioDevice): def play(self, sound): print(f'Playing "{sound}"') class HardDrive(StorageDevice): def write(self, data): print(f'Writing "{data}" to disk') # 应用管理类,逻辑和原来差不多 class Applications(dict): def __setitem__(self, name, app): if name in self: raise PermissionError(f'App "{name}" is already installed!') super().__setitem__(name, app) # 计算机类:只管硬件和应用安装,不再被应用直接依赖 class Computer: def __init__(self): self.screen = Screen() self.speaker = Speaker() self.disk = HardDrive() self.apps = Applications() def install_app(self, app_class, *args, **kwargs): app = app_class(*args, **kwargs) self.apps[app.name] = app def __getattr__(self, item): try: return self.apps[item] except KeyError: raise AttributeError(f'Computer has no app named "{item}"') # 基础应用类:只依赖需要的硬件接口 class App(ABC): def __init__(self, name): self.name = name class PdfReader(App): # 直接注入需要的存储和显示设备 def __init__(self, storage: StorageDevice, display: DisplayDevice, name='pdf_reader'): super().__init__(name) self.storage = storage self.display = display self.storage.write('pdf reader data') def read_document(self, document): self.display.display(document) class VideoPlayer(App): # 注入存储、显示、音频设备 def __init__(self, storage: StorageDevice, display: DisplayDevice, audio: AudioDevice, name='video_player'): super().__init__(name) self.storage = storage self.display = display self.audio = audio self.storage.write('video player data') # 修复了原代码里写pdf数据的笔误 def play_video(self, video): self.display.display(video) self.audio.play(video) if __name__ == '__main__': home_pc = Computer() # 安装时把需要的硬件传进去 home_pc.install_app(PdfReader, home_pc.disk, home_pc.screen) home_pc.install_app(VideoPlayer, home_pc.disk, home_pc.screen, home_pc.speaker) home_pc.pdf_reader.read_document('Harry_Potter.pdf') home_pc.video_player.play_video('Pulp_Fiction.mp4')
优化后好处多多
- 耦合大幅降低:应用只关心自己需要的硬件功能,和
Computer彻底解耦,改计算机结构不影响应用。 - 测试轻松了:测
PdfReader的时候,随便写个mock的StorageDevice和DisplayDevice就行,不用初始化整个计算机。 - 扩展性拉满:以后加个触摸屏(只要实现
DisplayDevice),或者换SSD(实现StorageDevice),应用完全不用改;新增应用也只要声明需要哪些组件。
特殊情况:非要保留循环依赖?
如果因为业务需求,必须让应用能拿到整个计算机的引用(比如要动态获取硬件),那至少得用依赖倒置:让Computer实现一个HardwareProvider接口,应用依赖这个接口而不是具体的Computer类。但这只是退而求其次的方案,还是不如依赖注入灵活。
内容的提问来源于stack exchange,提问作者Alex Pilkevych
相关产品推荐
相关产品推荐

