Introduction to clean code for Jr: Dependency Injection

I would like to say hello and welcome to everyone who loves and cherishes Jr. ❤️

Yasin Özmen
7 min readJun 23, 2023
Photo by Fotis Fotopoulos on Unsplash

Bu yazının türkçesi için bu makaleye bakabilirsiniz. ⬇️

I said in my previous article for jr.s who want to be mid, but now that they want us to go through a camel’s ditch even to become jr, I think if we want to become jr directly, we need to learn this and similar clean architecture structures and apply them to our code, otherwise we can’t even become jr, let alone mid(!) in 6 months 🥲. So we have to improve ourselves and I will start without further ado.

I think you remember my previous article with the Delegate pattern, if you don’t remember and if you ask what the delegate pattern is, you can first look here.

This is the first part of a series of 3 articles, in this article we will talk about why we use DI (Dependency Injection) and the most used form of DI.
I will talk about Constructor Injection.

DI I will make the same application in 2 parts so that the main focus in all the articles I explain is the structure, a simple application that takes photos from Unsplash and shows us.

Why do we suffer this torment?

The most important reason is TEST TEST TEST TEST.
Nobody is forcing us to do this to torture us. It is the last item of the SOLID principle, which leads us in terms of clean code understanding, and in general, it aims to reduce our dependency, reduce and eliminate our dependency on the objects, classes, etc. we use, thus allowing us to write more flexible, testable and developable code.

Constructor Injection

In this article I will talk about Constructor Injection, which is the most used form of DI, and in the other article I will talk about the other two most used ones.

View

There will just be a huge UIImageView on the screen and a button below it.
I will pin them to the screen with SnapKit again.

class Interface:UIView {
// MARK: - UI Elements
private let image: UIImageView = {...}()
private let button: UIButton = {...}()
private let activityIndicator: UIActivityIndicatorView = {...}()

// MARK: - Life Cycle
override init(frame: CGRect) {
super.init(frame: frame)
configureUI()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: - Function
func configureUI() {
backgroundColor = .systemGray5
setupImageView()
setupButton()
setupActivityIndicator()
}

// MARK: - Actions
@objc func buttonDidTapped(){
}
}

// MARK: - SnapKit Part
extension Interface {...}
This is what our app looks like

Network Layer

Now let’s write the Network layer for our application to download images, for this I use Alamofire and AlamofireImage.

First of all, let’s think about what internet related operations we are going to do.

  1. We will send our API request, parse the incoming data.
  2. We will download the image using AlamofireImage with the url in the incoming data.

Let’s create a 2-stage layer for these operations, a Service layer and a Manager layer.

We can think of our Service layer as Restaurant, our Manager layer as Order Applications and the ViewModel layer we will come to in the future as the orderer.

Now let’s get down to the core of the business again and explain it one by one.
For the service part, this is the part where it is actually done. When you request a lahmacun from the application (Manager), you usually don’t give too much detail, you just choose the restaurant at first. How the lahmacun is made is entirely the restaurant’s own business. Our restaurant prepares the food with the Service layer and Alamofire, when the food is done, it gives the food to the courier (order app) and the courier brings the food to us.

Also burrakerden has a very nice article about how the restaurant makes the food, you can find it here.

Below we see how the restaurant makes the dish.

protocol GetDataProtocol: AnyObject {
func fetch<T>(path: String, onSuccess: @escaping (T) -> Void, onError: @escaping (AFError) -> Void) where T: Codable
}

final class GetDataService:GetDataProtocol {
func fetch<T>(path: String, onSuccess: @escaping (T) -> Void, onError: @escaping (AFError) -> Void) where T: Codable {
AF.request(path, encoding: JSONEncoding.default).validate().responseDecodable(of: T.self) { (response) in
guard let model = response.value else {
print(response)
return
}
onSuccess(model)
}
}
}
protocol GetImageProtocol: AnyObject {
func getImage(url: String, onSuccess: @escaping (CGImage?)->(), onError: @escaping (String?)->())
}

final class GetImageService:GetImageProtocol {
func getImage(url: String, onSuccess: @escaping (CGImage?)->(), onError: @escaping (String?)->()) {
AF.request(url).responseImage { response in
if case .success(let image) = response.result {
onSuccess(image.cgImage)
}else {
onError("error when image dowloand")
}
}
}
}

GetDataService is already explained in Burak’s article. GetImageService is not much different, we actually give a url and if the operation is successful, it returns us cgImage in the completion handler and if the operation fails, it returns us the error message.

Let’s see how the Order application takes orders.
This is exactly where Dependency Injection comes in. As you remember, we had already decided in advance that we would order lahmacun, but we didn’t know which restaurant to order it from. The init part here will run while we are ordering and will ask us which restaurant we want. Since we want lahmacun to come from the Service layers we have created, we will give those layers at the time of init.

class GetDataManager {
private let getDataService: GetDataProtocol
private let url:String

init(getDataService: GetDataProtocol) {
self.getDataService = getDataService
self.url = UnsplashUrl.url.rawValue
}

func getJsonData(onSuccess: @escaping (UnsplashData?)->(Void), onError: @escaping (String)->(Void)) {
getDataService.fetch(path: url) { (response:UnsplashData) in
onSuccess(response)
} onError: { error in
onError(error.localizedDescription)
}
}
}
class GetImageManager {
private let getImageService: GetImageProtocol

init(getImageService: GetImageProtocol) {
self.getImageService = getImageService
}

func getImage(url: String, onSuccess: @escaping (CGImage?)->(), onError: @escaping (String?)->()) {
getImageService.getImage(url:url){ cgImage in
onSuccess(cgImage)
} onError: { error in
onError(error)
}
}
}

Let’s move quickly to the ViewModel Side so that it is well understood

protocol ViewModelProtocol: AnyObject {
func getData()
func getPhoto()
var data:UnsplashData? { get set }
}

class ViewModel:ViewModelProtocol {
// MARK: - Propertires
var data: UnsplashData?
weak var changeImageDelegate: ChangeImageProtocol?
let getDataManager = GetDataManager(getDataService: GetDataService())
let getImageManger = GetImageManager(getImageService: GetImageService())


// MARK: - Functions
func getData() {
getDataManager.getJsonData { data in
self.data = data
self.getPhoto()
} onError: { error in
print(error)
}
}

func getPhoto() {
guard let data = data else{
return
}
getImageManger.getImage(url: data.urls.regular) { image in
self.changeImageDelegate?.changeImage(image)
} onError: { error in
print(error as Any)
}
}
}

As you can see, when we add the getDataManager, it asks us for service in the init method, we currently give GetDataService, but in the future, maybe instead of doing it with Alamofire, we will want to do it with URLSession, in such a case, all we need to do is to give that service class, instead of changing our code from top to bottom, we can continue on our way by updating a small place.

If we need to explain what we are doing, when the getData function runs, it goes to the getJsonData method of the getDataManager and it returns UnsplashData when the operation is successful, we assign it to the variable that comes with the protocol and call the getPhoto method immediately afterwards.

The getPhoto method accesses the Interface through changeImageDelegete and updates the Image. Let’s see how we did that part.

protocol ChangeImageProtocol: AnyObject{
func changeImage(_ image:Image)
}

class Interface:UIView {
// MARK: - Properties
weak var buttonDelegate: InterfaceProtocol?
private let viewModel = ViewModel()
.........
}
// MARK: - Change Image
extension Interface: ChangeImageProtocol{

func changeImage(_ image: AlamofireImage.Image) {
guard let cgImage = image.cgImage else {
print("image not transform to cgImage")
return
}
self.image.image = UIImage(cgImage: cgImage)
}
}

// MARK: - SnapKit Part
extension Interface {...}
protocol InterfaceProtocol:AnyObject {
func buttonDidTapped()
}
class ViewController: UIViewController {
// MARK: - Properties
private let viewModel = ViewModel()
var interface: Interface?
weak var viewModelDelegate: ViewModelProtocol?

// MARK: - Life Cycle
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
configureUI()
delegateImp()
getFirstData()
}

// MARK: - Funcitons
func configureUI() {...}

func delegateImp() {
interface?.buttonDelegate = self
viewModel.changeImageDelegate = interface
viewModelDelegate = viewModel
}
...
}
...
This is the final version of our application

Normally our topic is complete and there is no Dependency Injection left, but I want to run an activityIndicator while the images are loading, so I’m posting the finished version on github.

If you like my article or if you don’t like it, I would be very happy if you leave feedback If you want to see my articles in this style, you can follow me here and twetter ❤️

--

--