[架构设计] 依赖注入优于单例模式

依赖注入 vs 单例模式

文章目录

概述

本文档解释了为什么在现代软件设计中依赖注入优于单例模式,并提供了基于 Robot项目的 Python 实践示例。

为什么要替代单例模式?

单例模式的问题

  1. 全局状态:难以追踪状态变化
  2. 测试困难:单例在测试间共享状态
  3. 紧耦合:代码直接依赖具体的单例类
  4. 并发问题:需要额外的线程安全处理
  5. 违反 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]

最佳实践

  1. 使用接口/协议:定义清晰的契约
  2. 构造函数注入 :通过 __init__ 注入依赖
  3. 避免服务定位器:不要在类内部查找依赖
  4. 单一职责:每个类只做一件事
  5. 配置与代码分离:通过配置定义依赖关系

总结

依赖注入提供了:

  • ✅ 可测试性:容易注入模拟对象
  • ✅ 灵活性:运行时决定使用哪个实现
  • ✅ 解耦:组件不知道具体实现
  • ✅ 线程安全:没有全局状态
  • ✅ 生命周期管理:容器管理对象的创建和销毁

可以把它想象成电器:

  • 单例:电器和电线焊死在一起
  • 依赖注入:电器有插头,可以插到任何合适的插座上

关键洞察:依赖注入不是为了"避免单例"------而是为了控制反转,让调用者决定使用什么实现,而不是被调用者自己决定。

相关推荐
一只大袋鼠2 小时前
并发编程(二十三):单例模式(二):静态/非静态方法:单例内存优化关键
java·单例模式·并发编程
一叶飘零_sweeeet2 小时前
volatile 关键字深度拆解:从内存屏障底层到单例模式的工业级架构设计
单例模式·volatile
一只大袋鼠3 小时前
并发编程(二十四):单例模式(三):构造方法私有:单例模式的 “第一道防线”
java·单例模式·并发编程
小邓的技术笔记3 小时前
ASP.NET Core 外部依赖调用治理实战:HttpClientFactory、Polly 与幂等边界
架构设计
mingshili3 小时前
[架构设计] pypubsub 底层实现机制与高性能替代方案
python·架构设计
硅基喵17 小时前
ASP.NET Core 外部依赖调用治理实战:HttpClientFactory、Polly 与幂等边界
asp.net core·架构设计
一只大袋鼠1 天前
并发编程(二十二):单例模式:从基础实现到 Spring Web 实战
java·spring·单例模式·并发编程
带娃的IT创业者2 天前
Weclaw 请求路由实战:一个 request_id 如何在 800 个并发连接中精准找到目标浏览器?
python·websocket·fastapi·架构设计·实时通信·openclaw·weclaw
Real-Staok2 天前
(集合)C / C++ 设计模式综合
单例模式·设计模式·代理模式