This content originally appeared on Level Up Coding - Medium and was authored by Pravin Tate
As per the wikipedia: In software engineering, dependency injection is a programming technique in which an object or function receives other objects or functions that it requires, as opposed to creating them internally. Dependency injection aims to separate the concerns of constructing objects and using them, leading to loosely coupled programs. The pattern ensures that an object or function that wants to use a given service should not have to know how to construct those services. Instead, the receiving ‘client’ (object or function) is provided with its dependencies by external code (an ‘injector’), which it is not aware of. Dependency injection makes implicit dependencies explicit and helps solve the following problems (Ref: link)
Let us now observe this concept in action within an actual program before delving into its benefits. For illustration purposes, we will use a straightforward and practical example: Car Assembly Line. Please note that this example has been intentionally simplified to maintain reader engagement and emphasize the primary focus of Dependency Injection.
To manufacture a car, certain essential components are necessary. Without these components, it is impossible to assemble a car. The required components include the engine, car frame (chassis), and wheels.
Similarly, there are optional components that do not hinder the car assembly process but serve as additional features. For example, stickers — some cars have different stickers based on demand, making them optional components.
In the above example, the required dependencies are represented with solid lines, while optional components are shown with dashed lines.
Following the principle that dependencies should be based on abstractions/interfaces/protocols rather than directly on implementations, it is essential to create protocols and implementations. An interface for the engine and EngineProvider has been created with their respective implementations.
I am solely concentrating on the DI principle, therefore, I am providing only basic code here to give you a clear understanding of it. If I were to write all the code details of the CarAssemble, you might lose interest in reading lengthy code and abandon it midway. Hence, I will keep it concise and straightforward.
// Engine
protocol Engine {
func start()
func stop()
}
struct EngineImpl: Engine {
func start() {}
func stop() {}
}
protocol EngineDepartment {
func getEngine(for frame: CarFrame) -> Engine?
}
class EngineDepartmentImpl: EngineDepartment {
private var engine: Engine?
func getEngine(for frame: CarFrame) -> Engine? {
self.engine = EngineImpl()
return engine
}
}
Developed the interface for the Frame and FrameProvider along with their respective implementations.:
// Car frame
protocol CarFrame {
func weight() -> CGFloat
func dimentions() -> (CGFloat, CGFloat, CGFloat)
}
struct CarFrameImpl: CarFrame {
func weight() -> CGFloat {
100
}
func dimentions() -> (CGFloat, CGFloat, CGFloat) {
(12,12,34)
}
}
protocol CarFrameProvider {
func getCarFrame(for type: CarType) -> CarFrame?
}
class CarFrameProviderImpl: CarFrameProvider {
var frame: CarFrame?
func getCarFrame(for type: CarType) -> CarFrame? {
self.frame = CarFrameImpl()
return frame
}
}
Developed the interface for the Car and CarParts along with their respective implementations.:
// Car type
enum CarType {
case suv
case sedan
}
protocol CarParts {
var engine: Engine { get }
var frame: CarFrame { get }
init(engine: Engine, frame: CarFrame)
}
class CarPartsModel: CarParts {
let engine: Engine
let frame: CarFrame
required init(engine: Engine, frame: CarFrame) {
self.engine = engine
self.frame = frame
}
}
// Car
protocol Car {
var carType: CarType { get }
var carParts: CarParts { get set }
}
class SUVCar: Car {
var carType: CarType
var carParts: CarParts
init(carType: CarType, carParts: CarParts) {
self.carParts = carParts
self.carType = carType
}
}
class Sedan: Car {
var carType: CarType
var carParts: CarParts
init(carType: CarType, carParts: CarParts) {
self.carParts = carParts
self.carType = carType
}
}
According to the diagram, there are 2 providers responsible for supplying the engine and frame components.
Now, it is essential to establish the Car Assemble Class and incorporate these providers externally to adhere to the Dependency Injection (DI) principle.:
// Assembler
class CarAssembler {
private let frameProvider: CarFrameProvider
private let engineProvider: EngineDepartment
private var car: Car?
init(frameProvider: CarFrameProvider,
engineProvider: EngineDepartment) {
self.frameProvider = frameProvider
self.engineProvider = engineProvider
}
public func manufactureCar(of type: CarType) -> Car? {
if let car = getCarProduct(for: type) {
self.car = car
guard let car = self.car else {
return nil
}
// manufactoring
if installPartsOnTheFrame() {
return car
}
}
return nil
}
}
private extension CarAssembler {
func getCarProduct(for type: CarType) -> Car? {
if let carParts = getCarParts(for: type),
let car = getCar(for: type, carParts: carParts) {
return car
}
return nil
}
func getCar(for type: CarType, carParts: CarParts) -> Car? {
switch type {
case .suv:
return SUVCar(carType: type, carParts: carParts)
case .sedan:
return Sedan(carType: type, carParts: carParts)
}
}
}
private extension CarAssembler {
func getCarParts(for type: CarType) -> CarParts? {
if let frame = frameProvider.getCarFrame(for: type),
let engine = engineProvider.getEngine(for: frame) {
let carParts = CarPartsModel(engine: engine, frame: frame)
return carParts
}
return nil
}
func installPartsOnTheFrame() -> Bool {
if let carParts = car?.carParts {
setEngine()
setWheels()
return true
}
return false
}
func setEngine() {
// set engine on the frame
}
func setWheels() {
// set wheels on the frame
}
}
We specify the dependencies (provider) type as an interface rather than a direct class.
The purpose of this is to allow for the injection of these dependencies from external sources, enabling future support for additional engineProviders without requiring modifications to this class.
These two dependencies are essential for the assembly of the car, and we acquire them during initialization or in the constructor. The necessary components (carParts) are obtained from the provider, with the assembler being solely responsible for the car assembly process. It is important to note that the EngineProvider should not be responsible for installing the engine onto the frame, following the SR principle.
In the diagram, there is an optional StickerProvider. Instead of including it in the initialization or constructor, we can create a separate method to handle this dependency.
What advantages does Dependency Injection offer?
1. Decoupling: The CarAssembler relies on the interface rather than any specific implementation (class/struct), allowing for the flexibility to pass any implementation that meets the interface requirements.
2. Testability: By writing unit tests for each interface separately, we can ensure independent testing of each component.
3. Maintainability: This code is easy to maintain, as any issues related to the engine can be addressed specifically without having to modify the entire codebase. Responsibilities are divided based on modules.
4. Scalability: For instance, if we need to incorporate a CarTester to test the assembled car, we can easily do so by creating the tester and passing the car to them. Based on the results, we can proceed with delivering the car.
What are the disadvantages of Dependency Injection?
There are no significant disadvantages, although it may require some time to grasp and implement initially due to developers adhering to inheritance and bundling everything in a single class. Consequently, they may initially perceive it as over-engineering. However, upon implementation, they come to realise the significance of this approach in the code. While it entails creating numerous interfaces/protocols, this should not be viewed as a drawback, as the Swift language is designed to be protocol-oriented.
If you enjoyed this story, kindly explore my other stories through this link.: https://medium.com/me/stories/public
Dependency Injection was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding - Medium and was authored by Pravin Tate
Pravin Tate | Sciencx (2024-06-27T13:07:00+00:00) Dependency Injection. Retrieved from https://www.scien.cx/2024/06/27/dependency-injection/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.