如何在Swift项目中不依赖第三方库实现Dependency Injection?
Hey there! Totally get where you're coming from—DI is such a game-changer for keeping code clean, modular, and testable, especially when you’re trying to avoid that messy spaghetti code and stick to SOLID principles. Let’s walk through how you can roll your own implementation in Swift without relying on any third-party libraries, tailored to fit existing project constraints.
1. Start with Constructor Injection (The Foundation)
The simplest and most widely used form of DI is constructor injection—this is where you pass dependencies directly into a class’s initializer. It’s straightforward, easy to follow, and aligns perfectly with the Dependency Inversion Principle.
Here’s a concrete example:
// First, define a protocol for your dependency (abstraction over implementation) protocol NetworkServiceProtocol { func fetchUserData(completion: @escaping (Result<User, Error>) -> Void) } // Concrete implementation of the network service class ProductionNetworkService: NetworkServiceProtocol { func fetchUserData(completion: @escaping (Result<User, Error>) -> Void) { // Actual network request logic here } } // ViewModel that depends on the network service class UserProfileViewModel { private let networkService: NetworkServiceProtocol // Inject the dependency via the initializer init(networkService: NetworkServiceProtocol) { self.networkService = networkService } func loadUserProfile() { networkService.fetchUserData { [weak self] result in // Handle the result } } } // Usage in your app: let productionNetwork = ProductionNetworkService() let viewModel = UserProfileViewModel(networkService: productionNetwork)
The best part? For testing, you can easily swap in a mock implementation of NetworkServiceProtocol without touching the ViewModel code at all.
2. Add a Dependency Container for Scalability
As your project grows, manually passing dependencies through every constructor can get tedious. A lightweight dependency container will help you manage and resolve dependencies centrally, while still keeping things flexible.
Here’s a minimal, self-contained container:
class DIContainer { // Singleton instance (adjust if your project prefers non-singleton containers) static let shared = DIContainer() // Store dependencies using a dictionary of factory closures private var dependencyFactories: [String: () -> Any] = [:] // Register a dependency with a factory closure func register<T>(type: T.Type, factory: @escaping () -> T) { let key = String(describing: T.self) dependencyFactories[key] = factory } // Resolve a registered dependency func resolve<T>() -> T { let key = String(describing: T.self) guard let factory = dependencyFactories[key], let dependency = factory() as? T else { fatalError("No dependency registered for type \(T.self)") } return dependency } } // Set up your dependencies once (e.g., in AppDelegate or SceneDelegate) DIContainer.shared.register(type: NetworkServiceProtocol.self) { ProductionNetworkService() } // Resolve dependencies wherever you need them: let viewModel = UserProfileViewModel(networkService: DIContainer.shared.resolve())
3. Handle Multiple Environments (Dev/Test/Production)
To make your container even more useful, add support for environment-specific dependencies—this is perfect for testing or using mock services in development.
enum AppEnvironment { case production case development case testing } class DIContainer { static let shared = DIContainer(environment: .production) private let environment: AppEnvironment private var dependencyFactories: [String: () -> Any] = [:] init(environment: AppEnvironment) { self.environment = environment registerBaseDependencies() } private func registerBaseDependencies() { switch environment { case .production: register(type: NetworkServiceProtocol.self) { ProductionNetworkService() } case .development: register(type: NetworkServiceProtocol.self) { DevMockNetworkService() } case .testing: register(type: NetworkServiceProtocol.self) { TestMockNetworkService() } } } // Keep the register/resolve methods from earlier... } // For testing, initialize a container with the testing environment: let testContainer = DIContainer(environment: .testing) let testViewModel = UserProfileViewModel(networkService: testContainer.resolve())
4. Key Best Practices to Stick to SOLID
- Always code to protocols, not concrete types: This keeps your code decoupled and makes swapping implementations trivial.
- Avoid overcomplicating the container: Start small—you don’t need features like scope management (singleton vs. transient) unless your project actually requires it.
- Don’t hide dependencies: Avoid using the container directly inside classes (service locator anti-pattern)—stick to constructor injection for clear dependency visibility.
- Test everything: Verify that your DI setup works by writing unit tests with mock dependencies—this is where the real value of DI shines.
This implementation is lightweight, fits into existing codebases easily, and gives you all the benefits of DI without relying on external libraries. You can extend the container as your project grows, but starting simple will keep things maintainable.
内容的提问来源于stack exchange,提问作者JJack_

