Jr.lar için temiz koda giriş: Dependency Injection

Jr.ları seven ve yaşatan herkese merhaba ve hoş geldiniz demek istiyorum. ❤️

Yasin Özmen
6 min readJun 20, 2023

Ben bir önceki yazıma mid olmak isteyen jr.lar için dedim ama artık jr olmak için bile deveye hendek atlatmamızı istedikleri için bence direk jr olmak istiyorsak bu ve bunun gibi clean architecture yapılarını öğrenmemiz ve kodumuza uygulamamız lazım yoksa bırakın 6 ayda mid olmayı(!) jr bile olamıyoruz 🥲. O yüzden kendimizi geliştirmek zorundayız ve ben daha fazla uzatmadan başlıyorum.

Bir önceki Delegate pattern ile olan yazımı hatırlıyorsunuzdur diye düşünüyorum hatırlamıyorsanız ve delegate pattern de ne olaki diyorsanız öncelikle şuraya bakabilirsiniz.

Bu 3 yazılık bir serininin ilk bölümü, bu yazımızda neden DI(Dependency Injection) kullandığımızdan ve bu DI’nin en çok kullanılan şekli olan
Constructor Injection’dan bahsedeceğim.

DI Anlattığım Yazıların hepsinde asıl odaklanılması gereken kısmın yapı olması için 2 bölümde de aynı uygulamayı yapacağım, Unsplash’dan fotoğraf çekip bize gösteren basit bir uygulama.

Neden çekiyoruz bu eziyeti ?

En önemli nedeni TEST TEST TEST.
Kimse bize eziyet olsun diye bunu yapmaya zorlamıyor. Temiz kod anlayışı açısından bize öncülük eden SOLID prensibinin de son maddesidir ve genel olarak kullandığımız nesnelerde sınıflarda vs bizim bağımlılığımızı, azaltmamızı yok etmemizi hedefler bu şekilde daha esnek, test edilebilir ve geliştirilebilir kod yazabilmemize olanak sağlar.

Constructor Injection

Bu yazımda DI’nin en çok kullanılan şekli olan Constructor Injection’dan bahsedeceğim diğer yazımda da diğer en çok kullanılan ikisine değineceğim.

View

Ekranda kocaman bir UIImageView ve altında da bir buton olacak sadece.
Bunları yine SnapKit ile ekrana sabitleyeceğim.

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 {...}
Uygulamamız böyle görünüyor

Network Katmanı

Şimdi uygulamamızın resim indirmesi için gerekli olan Network katmanını yazalım, bu iş için Alamofire ve AlamofireImage kullanıyorum.

Öncelikle internetle ilgili hangi işlemleri yapacağımızı düşünelim.

  1. API isteğimizi göndereceğiz, gelen datayı parse edeceğiz.
  2. Gelen datadaki url ile AlamofireImage kullanarak resmi indireceğiz.

Bu işlemler için 2 aşamlı bir katman oluşturalım, bir Service katmanı bir de Manager katmanı.

Servis katmanlarımızı Restoran, Manager katmanlarımızı Sipariş Uygulamaları ve ileride geleceğimiz ViewModel katmanını da sipariş veren biz olarak düşünebiliriz.

Şimdi yine işin asıl özüne inelim ve tane tane anlatalım.
Servis kısmı için asıl yapıldığı kısım. Siz uygulamadan(Manager’dan) bir lahmacun istediğiniz zaman genellikle çok fazla detay vermezsiniz sadece restoranı seçersiniz en başta. Lahmacunun nasıl yapıldığı tamamen restoranın kendi işidir. Bizim Restoranımız burda Service katmanı ve Alamofire ile yemeği hazırlıyor, yemek bittiği zaman kuryeye(sipariş uygulaması) yemeği veriyor ve kurye yemeği bize getiriyor.

Ayrıca restoranın yemeği nasıl yaptığını anlatan burrakerden'in çok güzel bir yazısı var yazıya burdan ulaşabilirsiniz.

Aşşağıda restoranın yemeği nasıl yaptığını görüyoruz.

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 zaten Burağın yazısında anlatılıyor. GetImageService de çok farklı değil aslında bir url veriyoruz ve eğer işlem başarılır olursa bize completition handlerda cgImage döndürüyor eğer işlem başarız olursa bize hata mesajını döndürüyor.

Birde Sipariş uygulaması nasıl sipariş alıyor ona bakalım.
İşte tam bu kısımda Dependency Injection Karşımıza çıkıyor. Hatırlayacağınız üzere biz lahmacun isteyeceğimize zaten önceden karar vermiştik ama hangi restorandan vereceğimizi bilmiyorduk. Buradaki init kısmı biz sipariş verirken çalışıcak ve bize hangi restoranı istediğimizi soracak. Bizde oluşturduğumuz Service katmanlarından lahmacun gelmesini istediğimiz için o katmanları vereceğiz init anında.

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)
}
}
}

Hızlıca ViewModel Tarafına da geçelim iyice anlaşılsın

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)
}
}
}

Gördüğünüz gizi getDataManagerı eklerken init metodunda bizden service istiyor biz şu anda GetDataService verdik ama ileride belki Alamofire ile yapmak yerine URLSession ile yapmak isteyeceğiz öyle bir durumda tek yapmamız gereken o service sınıfını vermek olacak kodumuzu bastan aşağıya değiştirmek yerine ufacık bir yerini güncelleyerek yolumuza devam edebiliyoruz.

Ne yaptığımızı anlatmak gerekirse eğer, getData fonksiyonu çalıştığı zaman getDataManager’ın getJsonData methoduna gidiyor ve o da bize işlem başarılı olduğunda UnsplashData döndürüyor bunu protocol ile gelen değişkene atıyoruz ve hemen arından getPhoto methodunu çağırıyoruz.

getPhoto methodu changeImageDelegete sayesinde Interface’e erişiyor ve Image’i güncelliyor. Hadi o tarafı nasıl yaptık ona da bakalım.

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
}
...
}
...
Uygulamamızın son hali bu şekilde

Normalde konumuz tamamlandı ve Dependency Injectıon ile ilgili bir işlem kalmadı ama ben resimler yüklenirken bir activityIndicator çalıştırmak istiyorum bu şekilde bitmiş halini githubda yayınlıyorum.

Eğer yazımı beğendiyseniz veyahut beğenmediyseniz feedback bırakmanız beni çok mutlu eder bu tarz yazılarımı görmek istiyorsanız beni burdan ve twetterdan takip edebilirsiniz ❤️

--

--