kotlin-context-di

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
STARTER_CHARACTER = 🔌
STARTER_CHARACTER = 🔌

Manual Dependency Injection with SystemContext and TestContext

基于SystemContext和TestContext的手动依赖注入

Structure Kotlin applications using manual DI with SystemContext (production) and TestContext (test) patterns. This approach provides type-safe dependency management, full control over initialization, and excellent testability without framework overhead.
使用SystemContext(生产环境)和TestContext(测试环境)模式的手动DI来构建Kotlin应用。这种方式无需框架开销,可提供类型安全的依赖管理、对初始化过程的完全控制,以及出色的可测试性。

Core Pattern: SystemContext

核心模式:SystemContext

Create an open class that holds all application dependencies. Use interfaces to group related components, and implement them as anonymous objects inside the context.
kotlin
open class SystemContext {
    interface Repositories {
        val customerRepo: CustomerRepository
        val orderRepo: OrderRepository
    }
    
    open val repositories: Repositories by lazy {
        object : Repositories {
            override val customerRepo by lazy { CustomerRepositoryImpl(dataSource) }
            override val orderRepo by lazy { OrderRepositoryImpl(dataSource) }
        }
    }
    
    open val customerService by lazy { 
        CustomerService(repositories.customerRepo) 
    }
    
    open val orderService by lazy {
        OrderService(repositories.orderRepo, customerService)
    }
}
Key characteristics:
  • Group related dependencies using interfaces (not open classes — see rationale below)
  • Implement production wiring as anonymous objects inside the context
  • Services reference repositories and other services directly
  • Use
    lazy
    for initialization that depends on other context properties or is expensive
  • Use direct instantiation when initialization is trivial and has no dependencies
Why interfaces instead of open classes:
  • Open classes with constructor parameters force test subclasses to satisfy those parameters, even when test fakes never use them (e.g., creating a dummy
    DataSource
    just to satisfy a constructor)
  • Open classes carry implicit coupling: test subclasses inherit production defaults, which can mask test setup errors
  • Interfaces have no constructors and no inherited behavior — test implementations must explicitly provide every dependency
Why anonymous objects for production wiring:
  • Production wiring stays in one place (
    SystemContext
    )
  • Implementation details are private to the context
  • The anonymous object naturally captures
    dataSource
    and other context properties from the enclosing scope
  • No need for constructor parameters on the grouping interface
创建一个开放类来存储所有应用依赖。使用接口对相关组件进行分组,并在上下文内部以匿名对象的形式实现这些接口。
kotlin
open class SystemContext {
    interface Repositories {
        val customerRepo: CustomerRepository
        val orderRepo: OrderRepository
    }
    
    open val repositories: Repositories by lazy {
        object : Repositories {
            override val customerRepo by lazy { CustomerRepositoryImpl(dataSource) }
            override val orderRepo by lazy { OrderRepositoryImpl(dataSource) }
        }
    }
    
    open val customerService by lazy { 
        CustomerService(repositories.customerRepo) 
    }
    
    open val orderService by lazy {
        OrderService(repositories.orderRepo, customerService)
    }
}
核心特性:
  • 使用接口对相关依赖进行分组(而非开放类——详见下文原理)
  • 在上下文内部以匿名对象的形式实现生产环境的依赖连接
  • 服务直接引用仓库及其他服务
  • 对于依赖其他上下文属性或初始化成本较高的组件,使用
    lazy
    初始化
  • 当初始化过程简单且无依赖时,使用直接实例化
为何使用接口而非开放类:
  • 带构造参数的开放类会强制测试子类满足这些参数,即便测试替身永远不会用到它们(例如,仅为了满足构造函数而创建一个虚拟的
    DataSource
  • 开放类会带来隐式耦合:测试子类会继承生产环境的默认实现,这可能掩盖测试设置中的错误
  • 接口没有构造函数,也没有继承行为——测试实现必须显式提供所有依赖
为何使用匿名对象实现生产环境依赖连接:
  • 生产环境的依赖连接逻辑集中在一处(
    SystemContext
  • 实现细节对上下文外部不可见
  • 匿名对象可自然捕获封闭作用域中的
    dataSource
    及其他上下文属性
  • 分组接口无需构造参数

Test Pattern: TestContext with Typed Test Implementations

测试模式:带类型化测试实现的TestContext

Extend SystemContext and override dependency groups with typed test implementations. Use concrete types in test implementations to avoid casting.
kotlin
class SystemTestContext : SystemContext() {
    class TestRepositories : Repositories {
        override val customerRepo = CustomerRepositoryFake()   // concrete type!
        override val orderRepo = OrderRepositoryFake()         // concrete type!
    }
    
    val testRepositories = TestRepositories()
    override val repositories: Repositories get() = testRepositories
}
This exploits Kotlin's covariant return types:
TestRepositories.customerRepo
has type
CustomerRepositoryFake
(concrete), while still satisfying the interface contract
CustomerRepository
(abstract).
Two access paths exist in tests:
  • repositories.customerRepo
    — typed as
    CustomerRepository
    (used by production code)
  • testRepositories.customerRepo
    — typed as
    CustomerRepositoryFake
    (for test assertions and setup)
In tests — create a fresh context per test:
kotlin
@Test
fun testOrderCreation() {
    with(SystemTestContext()) {
        // Arrange - use service methods to set up state
        customerService.registerCustomer(Customer.valid())
        
        // Act
        val order = orderService.createOrder(customerId, items)
        
        // Assert — direct access to fake methods, no casting needed
        assertThat(testRepositories.orderRepo.getSavedOrders())
            .contains(order)
    }
}
The
with(SystemTestContext()) { ... }
pattern creates a fresh context per test, provides clean access to all services and repositories, and prevents state leakage between tests.
继承SystemContext并使用类型化测试实现覆盖依赖分组。在测试实现中使用具体类型以避免类型转换。
kotlin
class SystemTestContext : SystemContext() {
    class TestRepositories : Repositories {
        override val customerRepo = CustomerRepositoryFake()   // concrete type!
        override val orderRepo = OrderRepositoryFake()         // concrete type!
    }
    
    val testRepositories = TestRepositories()
    override val repositories: Repositories get() = testRepositories
}
这利用了Kotlin的协变返回类型特性:
TestRepositories.customerRepo
的类型是具体的
CustomerRepositoryFake
,但仍能满足抽象的
CustomerRepository
接口契约。
测试中存在两种访问路径:
  • repositories.customerRepo
    — 类型为
    CustomerRepository
    (供生产代码使用)
  • testRepositories.customerRepo
    — 类型为
    CustomerRepositoryFake
    (用于测试断言和设置)
测试中——为每个测试创建全新上下文:
kotlin
@Test
fun testOrderCreation() {
    with(SystemTestContext()) {
        // 准备阶段 - 使用服务方法设置状态
        customerService.registerCustomer(Customer.valid())
        
        // 执行操作
        val order = orderService.createOrder(customerId, items)
        
        // 断言 — 直接访问替身方法,无需类型转换
        assertThat(testRepositories.orderRepo.getSavedOrders())
            .contains(order)
    }
}
with(SystemTestContext()) { ... }
模式会为每个测试创建全新上下文,提供对所有服务和仓库的清晰访问,并防止测试间的状态泄漏。

Type Safety Benefits

类型安全优势

Compile-time checking:
  • Typos caught immediately
  • Refactoring tools work perfectly (rename, move, find usages)
  • Missing dependencies fail at compile time, not runtime
IDE support:
  • Full autocomplete for all dependencies
  • Jump to definition works seamlessly
  • No string-based lookups or reflection
Clear dependency graph:
  • Constructor parameters show exact dependencies
  • Easy to trace where any component is used
  • No hidden framework magic
编译时检查:
  • 拼写错误可立即被捕获
  • 重构工具可完美工作(重命名、移动、查找引用)
  • 缺失的依赖会在编译阶段报错,而非运行时
IDE支持:
  • 所有依赖均支持自动补全
  • 跳转至定义功能无缝工作
  • 无基于字符串的查找或反射操作
清晰的依赖关系图:
  • 构造函数参数展示了精确的依赖项
  • 可轻松追踪任意组件的使用位置
  • 无隐藏的框架魔法

Initialization Control

初始化控制

Direct instantiation (default):
kotlin
open val customerService = CustomerService(repositories.customerRepo)
Use when initialization is cheap and there are no circular dependencies.
Lazy initialization:
kotlin
open val customerService by lazy {
    CustomerService(repositories.customerRepo)
}
Use when:
  • Circular dependencies exist (A needs B, B needs A)
  • Initialization is expensive (database connections, HTTP clients)
  • Component may not be used in all test scenarios
  • The value depends on other context properties that may be overridden
When in doubt, start with direct instantiation. Add
lazy
only when needed.
直接实例化(默认方式):
kotlin
open val customerService = CustomerService(repositories.customerRepo)
适用于初始化成本低且无循环依赖的场景。
延迟初始化:
kotlin
open val customerService by lazy {
    CustomerService(repositories.customerRepo)
}
适用于以下场景:
  • 存在循环依赖(A依赖B,B依赖A)
  • 初始化成本高(数据库连接、HTTP客户端)
  • 组件可能不会在所有测试场景中被使用
  • 组件值依赖可能被覆盖的其他上下文属性
若不确定,优先使用直接实例化。仅在必要时添加
lazy

Grouping Dependencies

依赖分组

Organize dependencies by layer or concern using interfaces:
kotlin
open class SystemContext(private val config: Config) {
    // Data layer
    interface Repositories {
        val userRepo: UserRepository
        val productRepo: ProductRepository
    }
    
    // External services
    interface Clients {
        val paymentClient: PaymentClient
        val emailClient: EmailClient
    }
    
    // Infrastructure
    interface Infrastructure {
        val database: Database
        val cache: Cache
    }
    
    open val infrastructure: Infrastructure by lazy {
        object : Infrastructure {
            override val database by lazy { DatabaseImpl(config.dbUrl) }
            override val cache by lazy { RedisCache(config.redisUrl) }
        }
    }
    
    open val repositories: Repositories by lazy {
        object : Repositories {
            override val userRepo by lazy { UserRepositoryImpl(infrastructure.database) }
            override val productRepo by lazy { ProductRepositoryImpl(infrastructure.database) }
        }
    }
    
    open val clients: Clients by lazy {
        object : Clients {
            override val paymentClient by lazy { PaymentClientImpl(config.paymentApiKey) }
            override val emailClient by lazy { EmailClientImpl(config.smtpConfig) }
        }
    }
    
    // Business logic
    open val userService by lazy { UserService(repositories.userRepo) }
    open val orderService by lazy {
        OrderService(
            repositories.productRepo,
            clients.paymentClient,
            clients.emailClient
        )
    }
}
This structure:
  • Makes test overriding straightforward (override entire groups)
  • Clarifies architectural layers
  • Keeps production wiring in one place
  • Anonymous objects capture config and other context properties from the enclosing scope
使用接口按层级或关注点组织依赖:
kotlin
open class SystemContext(private val config: Config) {
    // 数据层
    interface Repositories {
        val userRepo: UserRepository
        val productRepo: ProductRepository
    }
    
    // 外部服务
    interface Clients {
        val paymentClient: PaymentClient
        val emailClient: EmailClient
    }
    
    // 基础设施
    interface Infrastructure {
        val database: Database
        val cache: Cache
    }
    
    open val infrastructure: Infrastructure by lazy {
        object : Infrastructure {
            override val database by lazy { DatabaseImpl(config.dbUrl) }
            override val cache by lazy { RedisCache(config.redisUrl) }
        }
    }
    
    open val repositories: Repositories by lazy {
        object : Repositories {
            override val userRepo by lazy { UserRepositoryImpl(infrastructure.database) }
            override val productRepo by lazy { ProductRepositoryImpl(infrastructure.database) }
        }
    }
    
    open val clients: Clients by lazy {
        object : Clients {
            override val paymentClient by lazy { PaymentClientImpl(config.paymentApiKey) }
            override val emailClient by lazy { EmailClientImpl(config.smtpConfig) }
        }
    }
    
    // 业务逻辑
    open val userService by lazy { UserService(repositories.userRepo) }
    open val orderService by lazy {
        OrderService(
            repositories.productRepo,
            clients.paymentClient,
            clients.emailClient
        )
    }
}
这种结构:
  • 使测试覆盖变得简单(可覆盖整个分组)
  • 明确了架构层级
  • 生产环境的依赖连接逻辑集中在一处
  • 匿名对象可捕获封闭作用域中的配置及其他上下文属性

Test Context: Full Example

TestContext完整示例

kotlin
class SystemTestContext : SystemContext(Config.test()) {
    class TestRepositories : Repositories {
        override val userRepo = UserRepositoryFake()        // concrete type
        override val productRepo = ProductRepositoryFake()  // concrete type
    }
    
    class TestClients : Clients {
        override val paymentClient = PaymentClientFake()    // concrete type
        override val emailClient = EmailClientFake()        // concrete type
    }
    
    val testRepositories = TestRepositories()
    override val repositories: Repositories get() = testRepositories
    
    val testClients = TestClients()
    override val clients: Clients get() = testClients
}
In tests — no casting needed:
kotlin
@Test
fun testEmailSent() {
    with(SystemTestContext()) {
        orderService.completeOrder(orderId)
        
        // Direct access to fake methods via testClients — no casting
        assertThat(testClients.emailClient.sentEmails).hasSize(1)
        assertThat(testClients.emailClient.sentEmails[0].subject)
            .contains("Order Confirmed")
    }
}

@Test
fun testPaymentFailure() {
    with(SystemTestContext()) {
        // Configure fake behavior — no casting
        testClients.paymentClient.failOnNextCharge()
        
        val result = orderService.createOrder(request)
        
        assertThat(result.status).isEqualTo(OrderStatus.PAYMENT_FAILED)
    }
}
kotlin
class SystemTestContext : SystemContext(Config.test()) {
    class TestRepositories : Repositories {
        override val userRepo = UserRepositoryFake()        // concrete type
        override val productRepo = ProductRepositoryFake()  // concrete type
    }
    
    class TestClients : Clients {
        override val paymentClient = PaymentClientFake()    // concrete type
        override val emailClient = EmailClientFake()        // concrete type
    }
    
    val testRepositories = TestRepositories()
    override val repositories: Repositories get() = testRepositories
    
    val testClients = TestClients()
    override val clients: Clients get() = testClients
}
测试中——无需类型转换:
kotlin
@Test
fun testEmailSent() {
    with(SystemTestContext()) {
        orderService.completeOrder(orderId)
        
        // 通过testClients直接访问替身方法 — 无需类型转换
        assertThat(testClients.emailClient.sentEmails).hasSize(1)
        assertThat(testClients.emailClient.sentEmails[0].subject)
            .contains("Order Confirmed")
    }
}

@Test
fun testPaymentFailure() {
    with(SystemTestContext()) {
        // 配置替身行为 — 无需类型转换
        testClients.paymentClient.failOnNextCharge()
        
        val result = orderService.createOrder(request)
        
        assertThat(result.status).isEqualTo(OrderStatus.PAYMENT_FAILED)
    }
}

Fresh Context Per Test

每个测试使用全新上下文

Create a fresh context per test when fakes are stateful (the common case):
kotlin
@Test
fun `should save order`() {
    with(SystemTestContext()) {
        // Fresh fakes with no accumulated state
        orderService.createOrder(request)
        assertThat(testRepositories.orderRepo.getSavedOrders()).hasSize(1)
    }
}

@Test
fun `should not save order when payment fails`() {
    with(SystemTestContext()) {
        // Independent from the test above
        testClients.paymentClient.failOnNextCharge()
        orderService.createOrder(request)
        assertThat(testRepositories.orderRepo.getSavedOrders()).isEmpty()
    }
}
Why: Fakes are stateful —
OrderRepositoryFake
accumulates saved orders,
EmailClientFake
accumulates sent emails. Sharing a context across tests causes state from one test to leak into the next, leading to order-dependent failures and flaky tests.
The
with(SystemTestContext()) { ... }
pattern is idiomatic, cheap (no real I/O), and prevents test pollution.
Share a context only when fakes are truly stateless or when you have explicit reset logic — this is uncommon.
当替身有状态时(常见场景),为每个测试创建全新上下文:
kotlin
@Test
fun `should save order`() {
    with(SystemTestContext()) {
        // 无累积状态的全新替身
        orderService.createOrder(request)
        assertThat(testRepositories.orderRepo.getSavedOrders()).hasSize(1)
    }
}

@Test
fun `should not save order when payment fails`() {
    with(SystemTestContext()) {
        // 与上一个测试完全独立
        testClients.paymentClient.failOnNextCharge()
        orderService.createOrder(request)
        assertThat(testRepositories.orderRepo.getSavedOrders()).isEmpty()
    }
}
原因: 替身是有状态的——
OrderRepositoryFake
会累积已保存的订单,
EmailClientFake
会累积已发送的邮件。在测试间共享上下文会导致一个测试的状态泄漏到下一个测试中,从而引发依赖测试顺序的失败和不稳定测试。
with(SystemTestContext()) { ... }
模式是Kotlin的惯用写法,成本极低(无真实I/O操作),并可防止测试污染。
仅当替身真正无状态或有明确的重置逻辑时,才共享上下文——这种情况并不常见。

Nullable-to-Non-nullable Narrowing in Tests

测试中的可空类型转非空类型

When production interfaces have nullable dependencies (because configuration may be absent), test implementations can narrow them to non-nullable:
kotlin
// Production interface — nullable because config may not exist
interface Clients {
    val authClient: AuthClient?
    val notificationClient: NotificationClient?
}

// Test implementation — non-nullable
class TestClients : Clients {
    override val authClient = AuthClientStub()            // non-nullable!
    override val notificationClient = NotificationClientStub()  // non-nullable!
}
This is valid Kotlin because non-nullable types are subtypes of nullable types. Tests never need null checks when accessing test clients, even though production code handles the nullable case. This is a significant ergonomic win — test code stays clean and focused on behavior.
当生产环境接口包含可空依赖(因为配置可能不存在)时,测试实现可将其缩小为非空类型:
kotlin
// 生产环境接口 — 可空,因为配置可能不存在
interface Clients {
    val authClient: AuthClient?
    val notificationClient: NotificationClient?
}

// 测试实现 — 非空
class TestClients : Clients {
    override val authClient = AuthClientStub()            // non-nullable!
    override val notificationClient = NotificationClientStub()  // non-nullable!
}
这在Kotlin中是合法的,因为非空类型是可空类型的子类型。测试代码访问测试客户端时无需空检查,即便生产代码需要处理可空情况。这是一个显著的易用性提升——测试代码可保持简洁,专注于业务行为。

Integration with Test Doubles

与测试替身的集成

TestContext typically contains Fakes (in-memory implementations of interfaces):
kotlin
class CustomerRepositoryFake : CustomerRepository {
    private val db = mutableMapOf<String, Customer>()
    
    override fun save(customer: Customer) {
        db[customer.id] = customer
    }
    
    override fun findById(id: String): Customer? {
        return db[id]
    }
    
    // Test-specific methods (not in interface)
    fun getSavedCustomers(): List<Customer> = db.values.toList()
    fun failOnNextSave() { /* ... */ }
}
The TestContext wires these Fakes and exposes them with concrete types:
kotlin
class SystemTestContext : SystemContext() {
    class TestRepositories : Repositories {
        override val customerRepo = CustomerRepositoryFake()  // concrete type
    }
    
    val testRepositories = TestRepositories()
    override val repositories: Repositories get() = testRepositories
}
Now
customerService
uses
CustomerRepositoryFake
automatically because it references
repositories.customerRepo
, and tests access fake-specific methods via
testRepositories.customerRepo
without casting.
TestContext通常包含Fakes(接口的内存实现):
kotlin
class CustomerRepositoryFake : CustomerRepository {
    private val db = mutableMapOf<String, Customer>()
    
    override fun save(customer: Customer) {
        db[customer.id] = customer
    }
    
    override fun findById(id: String): Customer? {
        return db[id]
    }
    
    // 测试专属方法(不在接口中)
    fun getSavedCustomers(): List<Customer> = db.values.toList()
    fun failOnNextSave() { /* ... */ }
}
TestContext会连接这些Fakes并以具体类型暴露它们:
kotlin
class SystemTestContext : SystemContext() {
    class TestRepositories : Repositories {
        override val customerRepo = CustomerRepositoryFake()  // concrete type
    }
    
    val testRepositories = TestRepositories()
    override val repositories: Repositories get() = testRepositories
}
现在
customerService
会自动使用
CustomerRepositoryFake
,因为它引用的是
repositories.customerRepo
,而测试可通过
testRepositories.customerRepo
直接访问替身的专属方法,无需类型转换。

Application Wiring

应用连接

Main entry point:
kotlin
fun main() {
    val context = SystemContext(Config.fromEnvironment())
    
    // Start application with context
    val app = Application(
        context.orderService,
        context.userService
    )
    
    app.start()
}
Web framework integration (Ktor example):
kotlin
fun Application.module() {
    val context = SystemContext(Config.fromEnvironment())
    
    routing {
        get("/orders/{id}") {
            val orderId = call.parameters["id"]!!
            val order = context.orderService.getOrder(orderId)
            call.respond(order)
        }
        
        post("/orders") {
            val request = call.receive<CreateOrderRequest>()
            val order = context.orderService.createOrder(request)
            call.respond(order)
        }
    }
}
Routes access services directly from the context. No framework-specific annotations or registrations needed.
主入口:
kotlin
fun main() {
    val context = SystemContext(Config.fromEnvironment())
    
    // 使用上下文启动应用
    val app = Application(
        context.orderService,
        context.userService
    )
    
    app.start()
}
Web框架集成(Ktor示例):
kotlin
fun Application.module() {
    val context = SystemContext(Config.fromEnvironment())
    
    routing {
        get("/orders/{id}") {
            val orderId = call.parameters["id"]!!
            val order = context.orderService.getOrder(orderId)
            call.respond(order)
        }
        
        post("/orders") {
            val request = call.receive<CreateOrderRequest>()
            val order = context.orderService.createOrder(request)
            call.respond(order)
        }
    }
}
路由可直接从上下文中访问服务。无需框架专属注解或注册操作。

Why This Pattern Works

该模式为何有效

Simplicity:
  • No annotations to learn
  • No configuration files
  • No classpath scanning or reflection
  • Plain Kotlin code
Debuggability:
  • Step through initialization in debugger
  • Set breakpoints in context creation
  • No framework magic hiding behavior
Readability:
  • Dependencies visible in one place
  • Constructor calls show exactly what's needed
  • No surprising behavior from framework lifecycle
Test control:
  • Full control over what gets loaded
  • Fast test startup (only load what you need)
  • Easy to inject test doubles
  • No special test runners or annotations
  • No casting needed to access test-specific methods
Flexibility:
  • Change initialization order easily
  • Add conditional logic (feature flags, environment checks)
  • Compose contexts (production + feature flags)
Scalability:
  • Pattern stays simple as project grows
  • More dependencies just mean more properties in context classes
  • No framework limitations or architectural constraints
简洁性:
  • 无需学习注解
  • 无需配置文件
  • 无需类路径扫描或反射
  • 纯Kotlin代码实现
可调试性:
  • 可在调试器中单步执行初始化过程
  • 可在上下文创建时设置断点
  • 无隐藏的框架魔法
可读性:
  • 所有依赖集中可见
  • 构造函数调用清晰展示所需依赖
  • 无框架生命周期带来的意外行为
测试可控性:
  • 完全控制加载内容
  • 测试启动速度快(仅加载所需内容)
  • 轻松注入测试替身
  • 无需特殊测试运行器或注解
  • 无需类型转换即可访问测试专属方法
灵活性:
  • 可轻松修改初始化顺序
  • 可添加条件逻辑(功能开关、环境检查)
  • 可组合上下文(生产环境+功能开关)
可扩展性:
  • 随着项目增长,模式仍保持简洁
  • 新增依赖仅需在上下文类中添加更多属性
  • 无框架限制或架构约束

Anti-patterns

反模式

Avoid using open classes for dependency grouping:
kotlin
// Don't do this — forces test subclasses to satisfy constructor parameters
open class Repositories(private val dataSource: DataSource) {
    open val customerRepo: CustomerRepository = CustomerRepositoryImpl(dataSource)
}
Use interfaces instead — they have no constructors and force explicit implementation.
Avoid casting to access test-specific methods:
kotlin
// Don't do this
val emailClient = clients.emailClient as EmailClientFake
assertThat(emailClient.sentEmails).hasSize(1)
Use typed test implementations with dual access (
testClients.emailClient
) instead.
Avoid making everything lazy:
kotlin
// Don't do this unless needed
open val customerService by lazy { CustomerService(...) }
open val orderService by lazy { OrderService(...) }
open val productService by lazy { ProductService(...) }
Lazy adds complexity. Use direct instantiation unless circular dependencies or expensive initialization require it.
Avoid deep context hierarchies:
kotlin
// Too complex
open class DatabaseContext : InfrastructureContext()
open class RepositoryContext : DatabaseContext()
open class ServiceContext : RepositoryContext()
open class SystemContext : ServiceContext()
Keep it flat: one SystemContext with nested interface groups for organization.
Don't mix with annotation-based DI:
kotlin
// Don't mix patterns
@Inject lateinit var customerService: CustomerService  // Framework DI
val orderService = OrderService(repositories.orderRepo)  // Manual DI
Choose one approach and stick with it.
避免使用开放类进行依赖分组:
kotlin
// 不要这样做 — 会强制测试子类满足构造参数
open class Repositories(private val dataSource: DataSource) {
    open val customerRepo: CustomerRepository = CustomerRepositoryImpl(dataSource)
}
应使用接口替代——接口没有构造函数,且强制显式实现。
避免通过类型转换访问测试专属方法:
kotlin
// 不要这样做
val emailClient = clients.emailClient as EmailClientFake
assertThat(emailClient.sentEmails).hasSize(1)
应使用带双重访问路径的类型化测试实现(
testClients.emailClient
)替代。
避免给所有组件添加lazy初始化:
kotlin
// 除非必要,否则不要这样做
open val customerService by lazy { CustomerService(...) }
open val orderService by lazy { OrderService(...) }
open val productService by lazy { ProductService(...) }
lazy
会增加复杂度。仅当存在循环依赖或初始化成本较高时,才使用
lazy
,否则优先使用直接实例化。
避免过深的上下文层级:
kotlin
// 过于复杂
open class DatabaseContext : InfrastructureContext()
open class RepositoryContext : DatabaseContext()
open class ServiceContext : RepositoryContext()
open class SystemContext : ServiceContext()
保持扁平化结构:一个SystemContext,内部使用嵌套接口分组进行组织。
不要与基于注解的DI混合使用:
kotlin
// 不要混合模式
@Inject lateinit var customerService: CustomerService  // 框架DI
val orderService = OrderService(repositories.orderRepo)  // 手动DI
选择一种方案并坚持使用。

Migration Path

迁移路径

Adding to existing project:
  1. Create SystemContext with existing components
  2. Wire main entry point to use context
  3. Gradually move initialization logic into context
  4. Create TestContext and migrate tests incrementally
From framework DI:
  1. Create parallel SystemContext alongside framework
  2. New code uses SystemContext
  3. Gradually migrate existing code
  4. Remove framework once migration complete
No big-bang rewrite required. Adopt incrementally.
向现有项目中添加该模式:
  1. 创建包含现有组件的SystemContext
  2. 修改主入口以使用该上下文
  3. 逐步将初始化逻辑迁移至上下文
  4. 创建TestContext并逐步迁移测试
从框架DI迁移:
  1. 在现有框架旁创建并行的SystemContext
  2. 新代码使用SystemContext
  3. 逐步迁移现有代码
  4. 迁移完成后移除框架
无需大规模重写。可逐步采用该模式。