Understanding Dependency Injection Concepts
When we're reading Dependency Injection term for the first time, many people, including me frequently clueless about that term means. It's more like a $25 word for a cent idea.
So, what the meaning of Dependency Injection, in object-oriented programming context?
Simple, just giving an object to another object that need that to support their functionality. Still clueless? Let's diving to a code!
Imagine that I have a class SettingsManager
.
class SettingsManager {
private let dataSource = SettingDataSource()
func getLanguage() -> Language {
return dataSource.fetch(key: "language", type: Language.self)
}
func getHostname() -> String {
return dataSource.fetch(key: "hostname", type: String.self)
}
}
It has a dependency called dataSource
. Why it called dependency? Because the SettingManager
can't live without that, on the other words, SettingManager
become useless if there's no DataSource
in it.
If we're talking about coupling, this class considered as tightly coupled with SettingDataSource
, because the dependency is initialized inside the SettingManager
class. If there's a case, I want to move out the data source, from local persistance storage to a in memory storage, or even cloud service, this class will changed a lot. Not so flexible.
That's why, dependency injection comes into the play, and also adhere to Dependency Inversion Principle.
So, let's refactor the SettingManager
class to be more flexible, and following dependency injection & dependency inversion principle.
For the first time, dependency inversion principle, as declared on SOLID principles, stated that the software components must depend to an abstraction, not a concretion. So we inverse the dataSource
dependency to an abstraction. Let's call it SettingDataSourceProtocol
.
protocol SettingDataSourceProtocol {
func set(key: String, value: Any)
func fetch<T>(key: String, type: T.Type) -> T
}
After that, SettingManager
data source dependency has been inversed thru intializer-based injection, as follows:
class SettingManager {
private let dataSource: SettingDataSourceProtocol
init(dataSource: SettingDataSourceProtocol) {
self.dataSource = dataSource
}
func getLanguage() -> Language {
return dataSource.fetch(key: "language", type: Language.self)
}
func getHostname() -> String {
return dataSource.fetch(key: "hostname", type: String.self)
}
}
So, if we want to inject, let's say CloudSettingDataSource
, or even InMemorySettingDataSource
, it will be injected easily, as long as those class need to conform the protocol/interface SettingDataSourceProtocol
.
let inMemorySettingManager = SettingManager(dataSource: InMemorySettingDataSource())
let cloudSettingManager = SettingManager(dataSource: CloudSettingDataSource())
Pretty neat! ๐
Conclusion
Dependency injection allows us to make our software flexible to plug an play the dependency, as long as the dependency depend to the abstraction, not the concrete object. It also makes our software less coupled.
Hopefully it will be useful :)