dry-system 依赖注入
在大型 Ruby 项目中,对象之间的依赖关系会变得越来越复杂。服务层依赖仓储层,仓储层依赖数据库连接,控制器依赖多个服务。如果这些依赖都是手动创建的,代码很快会变得难以维护。dry-system 是 dry-rb 系列中的依赖注入框架,它用容器管理对象的创建和注入,让你的代码保持清晰和可测试。
dry-system 的哲学与传统 Rails 的全局状态模型不同。它强调显式依赖、不可变配置、和惰性加载。学完这一章你会理解为什么现代 Ruby 项目越来越多地使用 dry-system 替代 Rails 内置的 autoload。
运行 hello advance dry_system 可以查看完整演示代码。
运行示例
本章节的代码可以通过 CLI 运行完整演示:
hello advance dry_system # 查看 dry-system DI 完整演示
演示包含两部分:
- 模拟容器(Section 1-6):DIContainer 展示依赖注入容器内部实现模式,理解 Ruby 如何管理对象生命周期
- 真实 Dry::Container(Section 7):Dry::Container + resolve + AutoInject + namespace,生产级用法参考
建议先运行演示代码,再看文档理解原理。
容器设置
依赖注入的核心是一个容器。容器负责管理所有组件的创建和生命周期:
require "dry/system/container"
class Application < Dry::System::Container
config.root = Pathname("/path/to/app")
# 定义组件目录
config.component_dirs.add "lib/services" do |dir|
dir.auto_register = true
end
config.component_dirs.add "lib/repositories" do |dir|
dir.auto_register = true
end
end
容器定义了从哪里加载组件和如何加载。component_dirs 声明了组件所在的目录。每个目录可以有自己的注册规则,比如 auto_register = true 表示自动按命名约定注册。
在 Hello Ruby 项目中,容器已经定义好了。你可以在 Awesome 层看到完整的容器配置。
自动注册(Auto-registration)
自动注册是 dry-system 最强大的特性。它会扫描指定目录,根据文件路径和类名的命名约定自动注册组件:
lib/services/
├── email_service.rb → class EmailService → 注册为 'email_service'
├── user_service.rb → class UserService → 注册为 'user_service'
└── payment_service.rb → class PaymentService → 注册为 'payment_service'
lib/repositories/
├── user_repo.rb → class UserRepo → 注册为 'user_repo'
└── post_repo.rb → class PostRepo → 注册为 'post_repo'
注册后的组件通过容器访问:
# 访问已注册的组件
email_svc = Application["email_service"]
user_repo = Application["user_repo"]
# 组件是单例的,容器中只有一份实例
email_svc2 = Application["email_service"]
email_svc.object_id == email_svc2.object_id # true
命名约定是干系系统自动注册的基础:
- 文件名用 snake_case,类名用 PascalCase
- 目录名映射为命名空间路径
- 组件的键名等于
snake_case(文件名),不含扩展名
自动注册不需要手动写配置。你只需要在正确的目录下放置文件,容器会自动发现并注册它。
Provider:管理外部资源
组件通常是纯 Ruby 对象,但有些资源需要外部管理,比如数据库连接、HTTP 客户端、日志配置。Provider 允许你在容器启动和关闭时执行自定义逻辑:
Application.register_provider :database do
start do
# 容器启动时执行
target.use :sequel, url: "postgres://localhost/myapp"
end
stop do
# 容器关闭时执行
target[:database]&.disconnect
end
end
Application.register_provider :logger do
start do
target.register("logger", Logger.new($stdout))
end
end
注册 Provider 后,组件可以通过容器键访问对应资源:
Application["database"] # Sequel 数据库连接
Application["logger"] # Logger 实例
Provider 与自动注册的区别在于:自动注册是基于文件发现的,Provider 是手动注册的。Provider 适合管理那些不需要对应 Ruby 文件的资源。
Import Mixin:注入依赖
注入依赖的最常见方式是使用 Import mixin。它把容器中注册的组件以方法形式注入到类中:
# 定义 Import mixin
module Import
extend Application.injector
end
# 在具体类中使用
class CreateUserService
extend Import["user_repo", "email_service", "logger"]
def call(attrs)
user = user_repo.create(attrs)
email_service.welcome(user)
logger.info("Created user: #{user.email}")
user
end
end
Import["key1", "key2"] 在类上定义了 self.key1 和 self.key2 方法,它们从容器中解析对应组件并缓存。这意味着:
- 依赖是显式声明的。看类定义就知道它依赖什么
- 依赖是从容器中解析的,不需要手动 new
- 组件是单例的,多次调用
key1返回同一实例
在实际项目中,你会把 Import 定义为项目的标准依赖注入方式:
# lib/hello/system/import.rb
module Hello
module System
module Import
extend Application.injector
end
end
end
然后所有服务类都用 extend Import[...] 声明依赖。这种模式让依赖关系透明且可测试。
在测试中使用容器
依赖注入的最大好处之一是测试。你可以用 test double 替换容器中的真实组件:
RSpec.describe CreateUserService do
let(:user_repo) { instance_double(UserRepo, create: user) }
let(:email_service) { instance_double(EmailService, welcome: true) }
let(:logger) { instance_double(Logger, info: nil) }
let(:service) do
CreateUserService.new(
container: double(
"email_service" => email_service,
"logger" => logger
)
).tap { |s| s.define_singleton_method(:user_repo) { user_repo } }
end
it "creates user and sends email" do
service.call(name: "Alice", email: "alice@example.com")
expect(email_service).to have_received(:welcome).with(user)
end
end
在测试中替换容器组件,你就不需要真正连接数据库或发送真实邮件。每个测试只验证当前类的逻辑,隔离了外部依赖。这是单元测试的核心原则。
与 Hello Ruby 项目的集成
在 Hello Ruby 的 Awesome 层,dry-system 容器是整体架构的支柱:
lib/hello/
├── system/
│ ├── container.rb → Application < Dry::System::Container
│ ├── import.rb → Application.injector
│ └── provider/
│ └── database.rb → 数据库 Provider
├── services/
│ ├── create_user.rb
│ └── send_email.rb
└── repositories/
└── user_repo.rb
CLI 命令通过 System::Import 注入服务组件:
class CreateUserCommand
extend Hello::System::Import["create_user", "logger"]
def execute(name:, email:)
result = create_user.call(name: name, email: email)
logger.info("User created: #{result.email}")
result
end
end
这种设计让命令对象本身不包含业务逻辑,只是调度各个服务。业务逻辑在服务中,服务通过依赖注入获得所需的协作组件。
本章要点
- 容器(Container)管理所有组件的创建和生命周期
- 自动注册 按文件名和类名约定自动注册组件,免去手动配置
- Provider 用于管理外部资源(数据库连接、日志等),支持 start/stop 生命周期
- Import mixin 将容器中的组件以方法形式注入到类中
- 依赖声明是显式的:
extend Import["key1", "key2"]清楚地列出依赖 - 依赖注入让测试变得简单:用 test double 替换容器中的真实组件
- 在 Hello Ruby 项目中,Awesome 层使用 dry-system 构建完整的 DI 架构
- 运行
hello advance dry_system查看完整 DI 模式演示