第4章:工厂模式与DI:用工厂模式解耦,对比手动DI的优劣

说到依赖注入,很多人第一反应就是Dagger、Hilt这些大杀器。

但说实话,我早期做项目时,连这些框架的影子都没见过。那时候我们怎么解耦?靠的就是工厂模式。

工厂模式,说白了就是一个“生产对象”的地方。你告诉工厂你想要什么,它给你造出来。调用方不用关心对象是怎么new出来的,也不用管它依赖了谁。

嗯,这听起来是不是跟DI的理念很像?

4.1 工厂模式如何实现解耦

先看一个简单的例子。假设我们有个网络请求模块:

// 不用工厂,直接new
class UserRepository {
    private val api = RetrofitApi()
    
    fun getUser(): User {
        return api.fetchUser()
    }
}

这样写有什么问题?你想想看,如果哪天RetrofitApi的构造函数变了,或者你想换成OkHttp,你得改所有用到它的地方。

我在项目中遇到过这种场景,一个API类改了构造参数,结果全项目十几个文件都要跟着改,那叫一个酸爽。

用工厂模式改造一下:

// 工厂类
object ApiFactory {
    fun createApi(): ApiService {
        return RetrofitApi()
    }
}

// 使用工厂
class UserRepository {
    private val api = ApiFactory.createApi()
    
    fun getUser(): User {
        return api.fetchUser()
    }
}

这样一来,如果Api的实现变了,只需要改工厂里的那一行代码。调用方完全不受影响。

核心思想:工厂模式把“创建对象”的责任集中管理,调用方只依赖工厂,不依赖具体实现类。

4.2 工厂模式 vs 手动DI:一张表说清楚

我经常被问到:“工厂模式不就是手动DI吗?它们到底有什么区别?”

好问题。咱们直接上对比:

对比维度 工厂模式 手动DI(构造注入)
对象创建 工厂内部new 外部传入,调用方不创建
依赖关系 调用方依赖工厂 调用方依赖接口/抽象
测试难度 中等(需mock工厂) 容易(直接传mock对象)
灵活性 中等(工厂决定实现) 高(调用方决定实现)
代码量 中等(需手动传参)
适用场景 简单项目、模块内部 复杂项目、需要高可测性

说白了,工厂模式是“半自动”的DI。它帮你管了创建,但没帮你管“注入”这一步。

4.3 工厂模式的实战用法

我个人习惯在以下场景用工厂模式:

  • 第三方SDK封装:比如图片加载库、数据库驱动,用工厂统一管理实现切换
  • 多环境配置:开发/测试/生产环境用不同实现,工厂根据flag返回不同对象
  • 对象创建逻辑复杂:比如需要从配置文件中读取参数,再new对象

举个例子,我之前做过一个支付模块:

// 支付工厂
class PaymentFactory {
    fun create(paymentType: PaymentType): Payment {
        return when (paymentType) {
            PaymentType.ALIPAY -> AlipayPayment()
            PaymentType.WECHAT -> WechatPayment()
            PaymentType.UNIONPAY -> UnionpayPayment()
        }
    }
}

// 使用方
class CheckoutViewModel(private val paymentFactory: PaymentFactory) {
    fun pay(type: PaymentType) {
        val payment = paymentFactory.create(type)
        payment.execute()
    }
}

你看,这里工厂模式配合构造注入一起用,效果更好。工厂负责“选哪个实现”,构造注入负责“把工厂传进来”。

小技巧:工厂模式+接口,是手动DI的黄金搭档。工厂帮你管创建,接口帮你管抽象,两者互补。

4.4 工厂模式的坑,我踩过

我曾经在一个项目里过度使用工厂模式,结果搞出了“工厂地狱”。

什么意思?就是每个模块都有自己的工厂,工厂之间还互相依赖。A工厂要调用B工厂,B工厂又要调用C工厂。最后整个依赖图比蜘蛛网还乱。

避坑指南:

  • 不要全局工厂:每个模块有自己的工厂就够了,别搞一个超级工厂管所有
  • 工厂不要依赖其他工厂:如果A工厂需要B工厂的对象,那应该用DI把B工厂注入进来,而不是在A工厂里new B工厂
  • 工厂内部不要有业务逻辑:工厂只负责创建对象,别在里面写if-else判断业务状态

警告:工厂模式不是银弹。如果你的项目已经超过10个模块,或者有复杂的生命周期管理需求,建议直接上Dagger/Hilt。别硬撑。

4.5 什么时候该用工厂,什么时候该用手动DI?

我个人的经验是:

  1. 小项目、原型阶段:直接用工厂模式,简单粗暴,快速迭代
  2. 中等项目(5-10个模块):工厂模式+手动构造注入混用,核心模块用手动DI,边缘模块用工厂
  3. 大型项目(10个模块以上):上DI框架,别犹豫。手动维护依赖关系会累死人

你想想看,如果你的项目有20个ViewModel,每个ViewModel依赖3个Repository,每个Repository又依赖2个数据源。手动DI的话,你得在Application里写60行new代码。每次加个依赖,都得改Application。这不是开发,这是体力活。

但工厂模式也有它的位置。比如在单元测试中,我经常用工厂模式来创建测试数据对象:

// 测试用的工厂
object TestDataFactory {
    fun createUser(id: Int = 1, name: String = "Test"): User {
        return User(id = id, name = name)
    }
    
    fun createOrder(amount: Double = 100.0): Order {
        return Order(
            id = UUID.randomUUID().toString(),
            totalAmount = amount,
            items = listOf(createOrderItem())
        )
    }
}

这种工厂跟DI没关系,纯粹是为了让测试代码更干净。你看,工厂模式本身就是一个好工具,不一定非要跟DI绑定。

4.6 总结一下

工厂模式和手动DI,不是非此即彼的关系。它们是互补的。

工厂模式帮你解决了“创建对象”的问题,手动DI帮你解决了“传递依赖”的问题。两者结合,就是一套轻量级的依赖管理方案。

我建议你从工厂模式开始学起,理解“解耦”的本质。等你真正理解了为什么需要解耦,再去看Dagger/Hilt,你会发现那些框架的设计思路其实很自然。

嗯,下一章我们聊聊Kotlin的特性怎么让DI变得更优雅。到时候你会发现,Kotlin的语法糖能把手动DI写得跟框架一样爽。