• 如何不用第三方框架管理项目中各种服务和组件之间的依赖关系?
  • 发布于 1周前
  • 49 热度
    0 评论
前言
最近在重构一个项目的时候,我遇到了一个很常见的问题:如何管理项目中各种服务和组件之间的依赖关系?虽然 iOS 生态圈有 Swinject、Resolver 这些成熟的依赖注入框架,但我总觉得它们对于中小型项目来说有点"杀鸡用牛刀"的感觉。更重要的是,直接使用第三方框架而不了解其工作原理,总让人有点不踏实。所以我花了点时间,琢磨出了一个简单但实用的轻量级依赖注入容器。今天就和大家分享一下,希望能帮到同样想了解依赖注入原理,或者不想引入第三方库的朋友们。

依赖注入容器的核心需求
先问自己一个问题:我们为什么需要依赖注入?不就是为了解耦嘛!
当我在设计这个简易版的 DI 容器时,我列出了几个核心功能:
1.要能注册依赖:把一个协议和它的具体实现关联起来
2.要能按需解析:需要用的时候才创建实例,而不是一股脑全创建
3.要能管理生命周期:有些东西需要单例(比如网络服务),有些则每次都要新建

4.最好能简单易用:最好能像 SwiftUI 的 @State 那样方便

说干就干,我先设计了两个核心组件:
1. DIContainer - 负责注册和解析依赖

2. @Injected - 一个属性包装器,让注入过程更加优雅


从零开始实现 DIContainer
先来搞定最基础版本,不要想太多,就实现注册和解析两个核心功能:
public class DIContainer {
    privatevar factories: [String: Any] = [:]
    
    func register<T>(_ type: T.Type, factory: @escaping () -> T) {
        let key = String(describing: type)
        factories[key] = factory
    }
    
    func resolve<T>(_ type: T.Type) -> T? {
        let key = String(describing: type)
        guardlet factory = factories[key] as? () -> Telse { returnnil }
        let instance = factory()
        return instance
    }
}
看起来是不是超级简单?我们就是用了个字典,key 是类型描述,value 是创建这个类型实例的工厂闭包。讲真,第一次实现的时候我还考虑过用更复杂的方案,比如用泛型参数作为 key,但发现 Swift 的类型系统在这方面有些局限,所以还是用了最朴素的字符串方式。

使用起来也是相当直观:
let container = DIContainer()
container.register(NetworkServiceProtocol.self, factory: { NetworkService() })
// 堆代码 duidaima.com
// 需要用的时候
let networkService = container.resolve(NetworkServiceProtocol.self)
不过等等,这样每次调用 resolve() 都会创建一个新实例。对于网络服务这种常用组件,我们肯定不想每次都创建新的,更希望它是个单例。怎么办呢?

给依赖加上生命周期管理
依赖注入里有个重要概念叫"生命周期"。简单来说就是,你希望这个依赖是一直存在的单例,还是每次都临时创建?曾经我在一个项目里因为没管理好这个问题,导致每次页面切换都重新创建了网络服务,连接数激增差点把服务器搞崩溃...
我用个简单枚举来表示:
public enum DependencyLifetime {
    case singleton  // 单例,只创建一次
    case transient  // 瞬时,每次解析都创建新的
}
然后给我们的容器加上生命周期支持:
public class DIContainer {
    staticlet shared = DIContainer()  // 容器本身是个单例
    
    privatevar factories: [String: Any] = [:]
    privatevar singletons: [String: Any] = [:]  // 存放单例实例
    privatevar lifetimes: [String: DependencyLifetime] = [:]
    
    privateinit() {}  // 私有初始化器,防止直接创建
    
    func register<T>(_ type: T.Type, factory: @escaping () -> T, lifetime: DependencyLifetime) {
        let key = String(describing: type)
        factories[key] = factory
        lifetimes[key] = lifetime
    }
    
    func resolve<T>(_ type: T.Type) -> T? {
        let key = String(describing: type)
        guardlet lifetime = lifetimes[key] else { returnnil }
        
        switch lifetime {
        case .transient:
            // 临时创建,直接调用工厂方法
            guardlet factory = factories[key] as? () -> Telse { returnnil }
            return factory()
        case .singleton:
            // 单例模式,先看缓存里有没有
            iflet instance = singletons[key] as? T {
                return instance
            }
            // 没有就创建一个并缓存起来
            guardlet factory = factories[key] as? () -> Telse { returnnil }
            let instance = factory()
            singletons[key] = instance
            return instance
        }
    }
}
实现起来也不复杂,就是加了个字典来缓存单例实例,解析时根据生命周期决定是返回缓存的还是创建新的。
现在,我们可以这样注册一个单例:
container.register(NetworkServiceProtocol.self, factory: { NetworkService() }, lifetime: .singleton)
用属性包装器让注入更优雅
到目前为止我们的容器已经可以工作了,但使用起来还不够优雅。每次需要依赖的时候都要调用 resolve() 方法,而且还要处理可能为 nil 的情况。记得我第一次在项目中使用的时候,到处都是 container.resolve(XXX.self)! 这样的代码,看着就很难受。于是我想到了 Swift 的属性包装器,这不就是为了解决这种问题设计的吗?
@propertyWrapper
publicstruct Injected<T> {
    privatevar dependency: T
    
    publicinit() {
        guardlet dependency = DIContainer.shared.resolve(T.self) else {
            fatalError("哎呀,找不到类型为 \(T.self) 的依赖,是不是忘记注册了?")
        }
        self.dependency = dependency
    }
    
    publicvar wrappedValue: T {
        return dependency
    }
}
有了这个属性包装器,注入依赖的代码就变得超级简洁了:
class UserService {
    @Injected var networkService: NetworkServiceProtocol
    
    // 直接用 networkService,不用管它是怎么来的
}
是不是感觉清爽多了?属性包装器在访问时自动帮我们从容器中解析依赖,完全不需要手动调用 resolve()。这就是 Swift 语言的魅力所在,能让我们写出如此优雅的代码。

实战演示:怎么在真实项目中应用
光说不练假把式,来看看怎么在真实项目中使用我们的 DI 容器。上个月我在一个项目里就用这种方式彻底重构了之前充满单例模式的代码,效果相当不错。

首先,定义我们的协议和实现类:
protocol NetworkServiceProtocol {
    func fetchData() -> String
}

class NetworkService: NetworkServiceProtocol {
    func fetchData() -> String {
        return"真实网络数据"
    }
}

// 顺便定义一个 Mock 版本,方便单元测试
class MockNetworkService: NetworkServiceProtocol {
    func fetchData() -> String {
        return"模拟网络数据"
    }
}
class UserService {
    @Injectedvar networkService: NetworkServiceProtocol
    
    func loadProfile() {
        let data = networkService.fetchData()
        print(data)
    }
}
然后,在 App 启动时初始化容器:
@main
struct ExampleApp: App {
    
    init() {
        DIContainer.setup()
    }
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

extension DIContainer {
    staticfunc setup() {
        let container = DIContainer.shared
        // 注册服务,生产环境用 NetworkService
        container.register(NetworkServiceProtocol.self, factory: { NetworkService() }, lifetime: .singleton)
        // 其他服务
        container.register(UserService.self, factory: { UserService() }, lifetime: .singleton)
        // 需要测试时,只需把上面注册改为 MockNetworkService 即可无缝切换
    }
}
最后,在视图模型中使用这些服务:
@Observable
class UserViewModel {
    // 这里有个小坑,要记得加 @ObservationIgnored
    @ObservationIgnored @Injected var userService: UserService
    
    // 你的业务逻辑...
}
提个醒: 在 SwiftUI 的 Observable 里使用依赖注入时,记得加 @ObservationIgnored,否则 SwiftUI 会把注入的依赖也当作状态追踪,可能导致不必要的视图刷新。我就因为忘了加这个,导致切换页面时出现了奇怪的性能问题,debug 了半天才发现。

还能做哪些优化?
老实说,这个实现还有很多可以完善的地方:
更智能的类型键:目前用 String(describing:) 生成的键可能处理不了复杂的泛型和协议
加点锁:多线程环境下可能会有竞态条件,应该加个锁或用 actor 来保证线程安全
优化错误处理:现在遇到错误直接 fatalError,有点粗暴,可以改成抛出更友好的错误
支持更多生命周期:除了单例和瞬时外,可以加上范围绑定(比如只在某个 Scene 内共享)
处理循环依赖:A 依赖 B,B 依赖 A,这种情况目前会死循环

支持懒加载:有些依赖可能很重,可以等真正需要时才初始化


不过嘛,这些优化可以等实际需求出现时再加。记住软件开发的 YAGNI 原则(You Aren't Gonna Need It):别为了可能永远用不到的功能过度设计。

写在最后
说实话,我最喜欢这种轻量级框架,短小精悍但又能解决实际问题。比起引入一个动辄几千行代码的第三方库,30 行代码实现的自己的框架显然更易于理解和维护。当然,如果你的项目足够复杂,有更高级的依赖注入需求,那么专业的框架如 Swinject 肯定能提供更多功能。但对于大多数中小型项目,这个简易版已经够用了。

以前我总觉得"依赖注入"是个高大上的概念,需要复杂的框架才能实现。现在看来,其核心思想其实很简单,用几十行 Swift 代码就能搞定。希望这篇文章能帮助你理解依赖注入的本质,并在实际项目中灵活运用。

有没有人和我一样,喜欢用自己实现的轻量级框架,而不是直接引入第三方库?欢迎在评论区分享你的看法和经验~
用户评论