依赖注入 vs 单例模式
文章目录
- [依赖注入 vs 单例模式](#依赖注入 vs 单例模式)
概述
本文档解释了为什么在现代软件设计中依赖注入优于单例模式,并提供了基于 Robot项目的 Python 实践示例。
为什么要替代单例模式?
单例模式的问题
- 全局状态:难以追踪状态变化
- 测试困难:单例在测试间共享状态
- 紧耦合:代码直接依赖具体的单例类
- 并发问题:需要额外的线程安全处理
- 违反 SOLID:违反依赖倒置原则(DIP)
概念对比
单例模式:就像村里的公用电话亭
单例模式就像一个村子只有一部公用电话:
- 谁想打电话都得去那个固定的电话亭
- 如果有人在用,其他人只能等待
- 电话坏了,全村都打不了电话
- 想换个新电话?对不起,整个系统都得改
依赖注入:就像每家都有电话接口
依赖注入就像每家都预留了电话线接口:
- 你可以插任何型号的电话(只要接口匹配)
- 可以用座机、也可以用手机
- 一家的电话坏了不影响其他家
- 测试时可以插个"假电话"模拟
架构图对比
单例模式架构
bash
┌─────────────────────────────────────────────────────────┐
│ 应用程序空间 │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐│
│ │ 模块 A │ │ 模块 B │ │ 模块 C ││
│ │ │ │ │ │ ││
│ │ Robot.get() │ │ Robot.get() │ │ Robot.get() ││
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘│
│ │ │ │ │
│ └──────────────────┼───────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────┐ │
│ │Robot 单例 │ │
│ │(全局唯一实例) │ │
│ │ │ │
│ │_instance=xxx │◄───── 全局状态 │
│ └───────────────┘ │
└─────────────────────────────────────────────────────────┘
问题:
- 所有模块都直接依赖具体的 Robot 类
- 测试时无法替换成 MockRobot
- Robot 是全局状态,测试会相互影响
依赖注入架构
bash
┌─────────────────────────────────────────────────────────┐
│ 应用程序空间 │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐│
│ │ 模块 A │ │ 模块 B │ │ 模块 C ││
│ │ │ │ │ │ ││
│ │需要: │ │需要: │ │需要: ││
│ │RobotInterface│ │RobotInterface│ │AudioInterface││
│ └──────▲──────┘ └──────▲──────┘ └──────▲──────┘│
│ │ │ │ │
│ │(注入) │(注入) │(注入) │
│ ┌──────┴───────────────────┴──────────────────┴──────┐│
│ │ 依赖注入容器 ││
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ ││
│ │ │真实 Robot │ │真实 Audio │ │其他服务... │ ││
│ │ └────────────┘ └────────────┘ └────────────┘ ││
│ │ 测试时可以替换为: ││
│ │ ┌────────────┐ ┌────────────┐ ││
│ │ │Mock Robot │ │Mock Audio │ ││
│ │ └────────────┘ └────────────┘ ││
│ └─────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────┘
优势:
- 模块依赖接口,不依赖具体实现
- 容易替换实现(测试、升级等)
- 每个测试可以有独立的实例
实现指南
步骤 1:定义抽象接口
python
from abc import ABC, abstractmethod
from typing import Protocol
class RobotInterface(Protocol):
"""机器人接口,用于依赖注入"""
@property
def is_running(self) -> bool:
"""检查机器人是否正在执行动作"""
...
@property
def current_action_name(self) -> str | None:
"""获取当前动作名称"""
...
def connect(self) -> bool:
"""连接机器人"""
...
def disconnect(self) -> None:
"""断开机器人连接"""
...
def execute_action(self, action_name: str, params: dict | None = None) -> bool:
"""执行机器人动作"""
...
步骤 2:重构 Robot 类(移除单例)
python
class Robot:
"""机器人控制器 - 不再是单例"""
def __init__(
self,
robot_config: RobotConfig,
state_monitor: RobotStateMonitor | None = None,
client_factory: Callable[[], RobotClient] | None = None
):
"""使用注入的依赖初始化机器人控制器
参数:
robot_config: 机器人配置
state_monitor: 状态监控器实例(可选,如果为 None 将创建)
client_factory: 创建 RobotClient 的工厂函数
"""
self.robot_ip = robot_config.robot_ip
self.port = robot_config.robot_port
self.robot_accid = robot_config.robot_accid
# 注入的依赖
self._monitor = state_monitor
self._client_factory = client_factory or self._default_client_factory
# 状态
self._connected = False
self._is_running = False
self._current_action_name: str | None = None
# 订阅 pubsub 主题
self._subscribe_topics()
步骤 3:创建依赖容器
python
# src/di/container.py
from dataclasses import dataclass
from typing import Optional
@dataclass
class ServiceContainer:
"""应用程序服务的依赖注入容器"""
# 核心服务
robot: RobotInterface
audio_player: AudioPlayerInterface
wakeup_manager: WakeupManagerInterface
agent_pipeline: AgentPipelineInterface
# 配置
robot_config: RobotConfig
audio_config: AudioConfig
platform_config: PlatformConfig
# 可选服务
task_queue: Optional[TaskQueueInterface] = None
voice_subscriber: Optional[VoiceEventSubscriberInterface] = None
class ServiceFactory:
"""用于创建和连接服务的工厂"""
@staticmethod
def create_container(config: Config) -> ServiceContainer:
"""创建完全连接的服务容器"""
# 创建机器人
robot = Robot(
robot_config=config.robot,
state_monitor=RobotStateMonitor(),
)
# 创建其他服务...
return ServiceContainer(
robot=robot,
audio_player=audio_player,
# ... 其他服务
)
步骤 4:在应用程序入口使用
python
# main.py
from src.di.container import ServiceFactory
from config.config import load_config
def main():
"""应用程序入口点"""
# 加载配置
config = load_config()
# 创建包含所有依赖的服务容器
container = ServiceFactory.create_container(config)
# 使用注入的依赖启动应用程序
app = Application(container)
app.run()
测试优势
使用单例(有问题)
python
# ❌ 单例模式
class Database:
_instance = None
def __new__(cls):
if not cls._instance:
cls._instance = super().__new__(cls)
cls._instance.data = {}
return cls._instance
def test_save_user():
db = Database()
db.data = {} # 清空数据准备测试
save_user("Alice")
assert "Alice" in db.data
def test_another_feature():
db = Database()
# 糟糕!上一个测试的数据还在!
assert db.data == {} # 失败!因为是同一个实例
使用依赖注入(干净)
python
# ✅ 依赖注入
class Database:
def __init__(self):
self.data = {}
class UserService:
def __init__(self, db: Database):
self.db = db # 注入依赖
def save_user(self, name):
self.db.data[name] = True
def test_save_user():
# 每个测试都有独立的数据库实例
db = Database()
service = UserService(db)
service.save_user("Alice")
assert "Alice" in db.data
def test_another_feature():
# 全新的实例,没有数据污染
db = Database()
assert db.data == {} # 成功!
高级 DI 框架
对于复杂项目,可以考虑使用专门的 DI 框架:
python
# 使用 dependency-injector 库
from dependency_injector import containers, providers
class Container(containers.DeclarativeContainer):
"""应用程序的 IoC 容器"""
config = providers.Configuration()
robot = providers.Singleton(
Robot,
robot_config=config.robot,
)
audio_player = providers.Singleton(
AudioPlayer,
audio_config=config.audio,
)
使用 DI 管理全局状态
当确实需要全局状态时,通过 DI 容器管理它:
python
class ServiceContainer:
"""管理服务生命周期的 DI 容器"""
def __init__(self):
self._singletons = {} # 存储单例服务
self._factories = {} # 存储工厂函数
def singleton(self, name: str, factory: Callable):
"""注册单例服务 - 全局但受控"""
if name not in self._singletons:
self._singletons[name] = factory()
return self._singletons[name]
最佳实践
- 使用接口/协议:定义清晰的契约
- 构造函数注入 :通过
__init__注入依赖 - 避免服务定位器:不要在类内部查找依赖
- 单一职责:每个类只做一件事
- 配置与代码分离:通过配置定义依赖关系
总结
依赖注入提供了:
- ✅ 可测试性:容易注入模拟对象
- ✅ 灵活性:运行时决定使用哪个实现
- ✅ 解耦:组件不知道具体实现
- ✅ 线程安全:没有全局状态
- ✅ 生命周期管理:容器管理对象的创建和销毁
可以把它想象成电器:
- 单例:电器和电线焊死在一起
- 依赖注入:电器有插头,可以插到任何合适的插座上
关键洞察:依赖注入不是为了"避免单例"------而是为了控制反转,让调用者决定使用什么实现,而不是被调用者自己决定。