Swift dilinde SOLID Prensipleri ve Projede Kullanımı

Zafer Çalışkan
Appcent
Published in
10 min readMar 8, 2024

Yazılım geliştirme, günümüzde hızla değişen ve büyüyen bir alan haline gelmiştir. Büyük ve karmaşık yazılım projeleriyle uğraşırken, geliştiricilerin karşılaştığı zorluklar da artmaktadır. Kodun sürdürülebilirliği, esnekliği ve kalitesi, yazılım geliştirme sürecinde önemli bir rol oynamaktadır. İşte bu noktada, SOLID prensipleri devreye giriyor.

Bu makalede, SOLID prensiplerini ayrıntılı olarak inceleyeceğiz ve her birinin nasıl uygulanacağını ve neden önemli olduğunu ele alacağız. Her bir prensibin temel kavramlarını özetleyecek, gerçek dünya örnekleriyle destekleyececeğiz.

Hazırsanız başlayalım.

Kötü tasarım (Bad design) nedir?

Yazılım geliştirme sürecinde karşılaşılan en yaygın sorunlardan biri kötü tasarımdır. Kötü tasarım, Rigidity (Katılık), Fragility (Kırılganlık) ve Immobility (Hareketsizlik) gibi belirli problemleri içerir.

Rigidity durumunda, sistemin herhangi bir parçasındaki bir değişiklik diğer birçok parçayı da etkileyebilir ve değişikliğin maliyeti tahmin edilemez hale gelir.

Fragility ise, bir değişikliğin beklenmedik parçaları projeyi kırma eğilimindedir, bu da güvenilirlikte azalma ve bakım maliyetlerinde artışa neden olabilir.

Immobility durumunda ise, tasarımın bir parçası diğerlerine bağımlıdır ve bu nedenle ayırmak veya yeniden kullanmak zor olabilir.

Bu tür kötü tasarım problemleriyle başa çıkmak için, yazılım geliştirme topluluğu SOLID prensiplerini geliştirmiştir. SOLID prensipleri, yazılım tasarımında temel ilkeleri sunar ve kodun daha sürdürülebilir, esnek ve okunabilir olmasını sağlar.

Single Responsibility Principle

Single Responsibility Principle (SRP), bir sınıfın tek bir işten ve sorumluluktan sorumlu olması gerektiğini belirtir. Bu prensip, kodun daha basit, daha temiz ve daha test edilebilir olmasını sağlar. Bir sınıfın tek bir sorumluluğa sahip olması, kodun anlaşılmasını kolaylaştırır ve sınıfın daha modüler hale gelmesini sağlar.

Sol taraf prensip ihlalini göstermektedir. Sağ taraf ise optimal çözümü göstermektedir

Sol tarafta, GET DATA, PARSE DATA ve SAVE DATA kodlarının Class içerisinde tanımlandığını görüyoruz. Bu senaryoda GET DATA kodunu test etmek istersek tüm sınıf için test yazmamız gerekecektir. Ayrıca Class üç farklı sorumluluğa sahiptir. Bu da SRP’yi ihlal etmektedir.

Sağ tarafta, GET DATA, PARSE DATA ve SAVE DATA kodlarının Class dışında tanımlandığını görüyoruz. Class içerisinde sadece ilgili kodları çağırarak kullanıyoruz. Eğer GET DATA kodunu test etmek istersek artık tüm sınıf yerine sadece GET DATA’nın bulunduğu kısma test yazmamız yeterli olacaktır.

Open-Closed Principle

Open-Closed Principle (OCP), bir modülün davranışının genişletilebilir olması ancak kaynak kodunun değiştirilemez olması gerektiğini belirtir. Bu ilke, kodun değişikliklere kapalı olmasını sağlar ancak yeni gereksinimler veya farklı davranışlar eklenerek modülün genişletilebilir olmasını sağlar.

İki temel özelliği vardır:

  1. Open for Extension (Genişletmeye Açık): Bir modülün davranışı, yeni gereksinimler veya farklı davranışlar eklenerek genişletilebilir olmalıdır. Bu, uygulamanın değişen ihtiyaçlarına uyum sağlamayı ve yeni özelliklerin kolayca eklenmesini sağlar.
  2. Closed for Modification (Değişikliklere Kapalı): Bir modülün kaynak kodu değiştirilemez olmalıdır. Mevcut kodun değiştirilmesi yerine, yeni kod ekleyerek modülün davranışını genişletmek gerekir. Bu, mevcut kodun beklenmedik şekilde etkilenmemesini sağlar.

Bu prensibin uygulanması, kodun daha az bağımlılığa sahip olmasını sağlar. Bir modülün davranışı değiştiğinde, sadece bu modülün kendisi etkilenir ve diğer modüllerde değişiklik yapılmasına gerek kalmaz. Bu, kodun daha sürdürülebilir ve bakımının daha kolay olmasını sağlar.

Sol taraf prensip ihlalini göstermektedir. Sağ taraf ise optimal çözümü göstermektedir

Sol tarafta, butona basıldığında printColor fonksiyonu çalışmakta ve parametre olarak Square tipinde bir enum almaktadır. Daha sonra switch — case ile square’in hangi kareye denk geldiğini öğrenip çıktı olarak rengini yazmaktadır. Eğer Square enum’ına yeni bir case eklersek mecburen printColor fonksiyonunu düzenlemek zorundayız. Bu da yeniden test yazmamız ve akışı incelememizi gerektirecektir. Peki Square enum’ını 50 farklı yerde kullandığımızı düşünelim. Yeni değişikliği eklemek için ne kadar efor harcayacağız?

Sağ tarafta, Mavi, Yeşil, Kırmızı karelerin bulunduğu bir liste oluşturduk. Butona basıldığında printColor fonksiyonu çalışmakta ve parametre olarak basılan karenin indexini vermekteyiz. Ardından ilgili kare içerisindeki printColor fonksiyonunu çalıştırarak çıktı olarak rengi yazmaktayız. Yeni bir kare eklemek istediğimizde listeye oluşturulan kareyi eklememiz yeterli olacaktır.

Liskov Substitution Principle

Liskov Yerine Koyma Prensibi (LSP), bir alt sınıfın, üst sınıfın tüm özelliklerini kullanabilmesi ve yerine geçebilmesi gerektiğini belirtir. Yani, alt sınıflar, üst sınıfların davranışlarını değiştirmeden kullanabilmelidir.

Bu prensibin ihlali, genellikle Open-Closed Principle (Açık/Kapalı Prensibi) ihlaliyle sonuçlanır. Yani, alt sınıflar, üst sınıfın davranışlarını değiştirir veya geçersiz kılar. Bu durum, kodda beklenmedik hatalara ve tutarsızlıklara yol açabilir.

Sol taraf prensip ihlalini göstermektedir. Sağ taraf ise optimal çözümü göstermektedir

Sol tarafta, iki adet Error protocolünü conform yapan enum bulunuyor. Random sınıfı içerisinde getValue fonksiyonuna 10 sayısından büyük bir değer verildiğinde “ok” yazmakta, küçük değer verildiğinde ise RandomError.bigNumber throw etmektedir. Random sınıfından türetilen DifferentRandom sınıfında da getValue fonksiyonu bulunmaktadır. Bu fonksiyona 5 sayısından büyük değer verildiğinde “ok” yazmakta, küçük değer verildiğinde DifRandomError.bigNumber throw etmektedir. Bu durum LSP’yi ihlal etmektedir. Eğer DifferentRandom.getValue içerisinde 7 sayısını girersek error throw ederken DifferentRandom sınıfını Random sınıfı ile yer değiştirirsek “ok” çıktısını vermektedir. Alt sınıf, üst sınıfın davranışı değiştirmektedir.

Sağ tarafta, ortak enum kullanarak error handling konusunda kontrollerin bozulmamasını sağlayacağız. getValue fonksiyonu içerisinde benzer logicleri kullanarak sınıfların aynı davranışları sergilemesini sağladık.

Interface Segregation Principle

Arayüz Ayrımı Prensibi, uygulama arayüzlerini daha küçük arayüzler halinde böler, böylece istemcilerin sadece ihtiyaç duydukları arayüzleri kullanmalarını sağlar. Bu prensibin amacı, “fat interfaces” olarak adlandırılan ve gereksiz yeniden düzenleme ve test zorunluluğu gibi sorunlara yol açan büyük arayüzleri azaltmaktır.

Sol tarafta, DBProtocol içerisinde çeşitli amaçlar için kullanılmak üzere fonksiyonlar belirtilmiş. DBProtocol’ü conform eden DBManager, sadece save fonksiyonlarını kullanmak istese bile mecburen get fonksiyonlarını da conform etmek zorunda.

Sağ tarafta, kullandıkları yönteme göre(projenin ihtiyacına göre değişiklik gösterir) ayrılmış 2 adet protocol bulunuyor. DBManager artık save fonksiyonlarını kullanmak isterse DBSaveProtocol tüm ihtiyaçlarını karşılayacaktır. Böylece gereksiz karmaşaya ve aşırı şişen protocollerden kurtulmuş oluruz.

Dependency Inversion Principle

Dependency Inversion prensibi, iki ana ilkeye dayanır:

  1. Yüksek seviyeli modüller, düşük seviyeli modüllere bağlı olmamalıdır. Her ikisi de soyutlamalara dayanmalıdır.
  2. Soyutlamalar ayrıntılara bağlı olmamalıdır. Ayrıntılar soyutlamalar üzerine olmalıdır.

Bu prensibin temel amacı, kodu modüller arasındaki bağımlılıklardan kurtarmak ve modüllerin gerçekten bağımsız olmasını sağlamaktır. Uygulamaları birden fazla modüle bölmek yaygın bir uygulama olmasına rağmen, bu bağımlılıklar kodu sıklıkla değişen modüllere bağlı kılar. Somut sınıflar, arayüzlerin aksine sıklıkla değişebilirler, örneğin, bir bug fix ya da refactor sırasında. Dependency Inversion, modüllerin bağımsız geliştirilmesini ve dağıtılmasını kolaylaştırır.

Ayrıca, Open-Closed Prensibi ve Liskov Substitution Prensibi doğru bir şekilde uygulandığında, modülünüz aynı zamanda Dependency Inversion Prensibi’ni de uygulamış olacaktır.

Sol tarafta, Class 2, Class 1'i conform etmiştir. İlerleyen bir zamanda bu sınıfları bağımsız modüller haline dönüştürmek istediğimizde Class 2'nin somut sınıfa bağımlı olması işleri zorlaştırmaktadır.

Sağ tarafta, Class 1 ve Class 2 bir protocolü conform etmektedirler. İlerleyen zamanda bağımsız modüllere ayırmak istenildiğinde somut sınıf bağımlılığı yerine protocole bağımlı olduğumuz için daha basit ve tutarlı şekilde ayırabileceğiz.

Örnek

Başlamadan önce | Uygulamayı incelemeye başlamadan önce projeyi inceleyip SOLID prensiplerini ihlal eden alanları kendiniz bulmaya çalışabilirsiniz. Aşağıdaki Gist’lerde sınıflardaki tüm kodlar bulunmuyor. Tüm kodlar görmek için Github’a bakabilirsiniz. Uygulamanın bazı kısımlarında kodlar prensipleri pratik edebilmek için gelişigüzel yazıldı. Örn. Network katmanı

Uygulamanın amacı | Bir ekrandan oluşan mobil bir uygulamamız bulunuyor. Uygulamanın amacı API’den rastgele Chuck Norris şakası almak ve bunu ekranda göstermek. Kullanıcı yeni şaka görmek isteyebilir veya gösterilen şakayı Core Data, Sqlite’a kaydedebilir.

SOLID’den önce

Uygulama mimarisi:

    .
├── Components
│ └── TableViewCell
│ ├── FavoriteJokeTableViewCell
│ │ ├── FavoriteJokeTableViewCell.swift
│ │ └── FavoriteJokeTableViewCell.xib
│ └── SimpleJokeTableViewCell
│ ├── SimpleJokeTableViewCell.swift
│ └── SimpleJokeTableViewCell.xib
├── Enums
│ ├── CoreDataError.swift
│ ├── HomeCellType.swift
│ ├── NetworkError.swift
│ └── SqliteError.swift
├── Managers
│ ├── CoreDataManager.swift
│ └── SqliteManager.swift
├── Models
│ └── JokeModel.swift
├── Protocols
│ ├── HomeCellDelegate.swift
│ ├── HomeCellProtocol.swift
│ └── RepositoryProtocol.swift
├── Screens/Home
│ ├── View
│ │ ├── HomeViewController.swift
│ │ └── HomeViewController.xib
│ └── ViewModel
│ └── HomeViewModel.swift
└── Services
└── NetworkService.swift

İhlalleri incelemeye başlayalım.

getRemoteJoke ve getFavoriteJoke fonksiyonları SRP’yi ihlal etmektedir. Aynı fonksiyon içerisinde hem servisten veri alınıyor, gelen veri işlenip jokes listesine atılıyor ve en sonunda da delegate yardımı ile tableView’ı yeniliyor.

networkService, sqliteManager ve coreDataManager somut sınıflara bağımlıdır. Ayrıca HomeViewModel soyutlamaya dayanmamaktadır. Bu durumda DIP ihlal edilmiştir.

getFavoriteJoke fonksiyonunda do catch bloğunda CoreDataManager ve SqliteManager’da inceleyeceğimiz bir durumdan dolayı LSP ihlal edilmektedir. Bu ihlalin sebebi coreDataManager ve sqliteManager’ın yer değiştirdiğinde farklı error typelarını throw etmesidir.

viewModel, somut bir sınıfa bağımlıdır. Bu da DIP ihlalinin bir belirtisidir.

tableView:numberOfRowsInSection, cellForRowAt, didSelectRowAt, titleForHeaderInSection incelendiğinde if-else, switch-case kullanılarak fonksiyonların değişikliğe açık hale getirildiğini görmekteyiz. Her yeni section veya cellType oluşturulduğunda fonksiyonlarda değişiklik yapılmalıdır. Bu sebeple Open for extension, closed for modification açıklamasının sahibi olan OCP ihlal edilmiştir.

FavoriteJokeTableViewCell ve SimpleJokeTableViewCell’i incelerken bahsedeceğim ancak burada da belirtmekte fayda var. HomeCellDelegate ISP’yi ihlal etmektedir. Yazının devamında göreceğiz.

Buradaki en büyük ihlal, bir servis katmanında herhangi bir soyutlama yapılmamış olmasıdır. DIP ihlali vardır.

HomeCellDelegate içerisinde tanımlanan updateJoke fonksiyonu SimpleJokeTableViewCell içerisinde kullanılabilirken, FavoriteJokeTableViewCell içerisinde kullanılmasına gerek yoktur. Bu da hem LSP, hem de ISP ihlalini işaret ediyor. FavoriteJokeTableViewCell içerisinde updateJoke fonksiyonunu kullanmaya çalışırsak ve planlanan bir logic bulunmuyorsa uygulamada beklenmedik sonuçlar olabilir. joke objesinin somut sınıfa bağımlı olarak oluşturulması da DIP ihlalini göstermektedir.

CoreDataManager ve SqliteManager, RepositoryProtocol’ü conform etmektedir. getJokes(), CoreDataManager içerisinde CoreDataManager.fetchError throw ederken SqliteManager içerisinde SqliteError.fetchError throw etmektedir. Bu da LSP ihlalinin bir göstergesidir. Aynı zamanda getJokeEntity(), CoreDataManager içerisinde kullanılmamaktadır. Bu da ISP ihlaline sebep vermektedir.

Tüm uygulama JokeModel isimli struct’ı kullanıyor. DIP’te belirtildiği gibi Soyutlamalar ayrıntılara bağlı olmamalıdır. Ayrıntılar soyutlamalar üzerine olmalıdır. Bu sebeple DIP ihlal edilmektedir. Çözümü bir protokol tanımlayıp JokeModel’in bu protocol’ü conform etmesi olacaktır.

SOLID’den sonra

Uygulama mimarisi:

    .
├── Components
│ └── TableViewCell
│ ├── FavoriteJokeTableViewCell
│ │ ├── FavoriteJokeCell.swift
│ │ ├── FavoriteJokeTableViewCell.xib
│ │ └── FavoriteJokeTableViewCell.xib
│ └── SimpleJokeTableViewCell
│ │ ├── SimpleJokeCell.swift
│ ├── SimpleJokeTableViewCell.swift
│ └── SimpleJokeTableViewCell.xib
├── Enums
│ ├── ManagerError.swift
│ └── NetworkError.swift
├── Managers
│ ├── CoreDataManager.swift
│ └── SqliteManager.swift
├── Models
│ └── JokeModel.swift
├── Protocols
│ ├── CoreDataRepositoryManager.swift
│ ├── HomeCellDelegate.swift
│ ├── HomeCellItem.swift
│ ├── HomeCellProtocol.swift
│ ├── HomeDataProviderProtocol.swift
│ ├── HomeRepositoryProtocol.swift
│ ├── HomeServiceProtocol.swift
│ ├── HomeViewModelProtocol.swift
│ ├── RepositoryProtocol.swift
│ ├── NetworkServiceProtocol.swift
│ ├── SimpleHomeCellDelegate.swift
│ ├── SimpleHomeCellItem.swift
│ └── JokeModelProtocol.swift
├── Screens/Home
│ ├── Builder
│ │ └── HomeBuilder.swift
│ ├── DataProvider
│ │ └── HomeBuilder.swift
│ ├── Repository
│ │ └── HomeRepository.swift
│ ├── Service
│ │ └── HomeRepository.swift
│ ├── View
│ │ ├── HomeViewController.swift
│ │ └── HomeViewController.xib
│ └── ViewModel
│ └── HomeViewModel.swift
└── Services
└── NetworkService.swift

İhlalleri incelemeye başlayalım.

HomeViewModel artık HomeViewModelProtocol’ü conform etmektedir. Bu sayede soyutlaştırılmış oldu. sqliteManager, coreDataManager, serviceManager objeleri DataProvider’a taşınmıştır, DataProvider objesi de soyutlamaya bağımlı haldedir. JokeModel kullanılan her yer JokeModelProtocol ile değiştirildi. Böylece DIP’e uygun hale getirildi.

getRemoteJoke(), getFavorite() fonksiyonlarının içi düzenlenerek SRP’ye uygun hale getirildi.

getFavorite() içerisinde önceden bulunan LSP ihlali düzeltildi. Buna CoreDataManager ve SqliteManager’dan bahsederken değineceğim.

Tek amacı verilerin nereden, nasıl geleceğini yöneten bir sınıf oluşturdum. Bu sayede HomeViewModel içerisinde SRP ihlali engelleniyor. Aynı şekilde repository ve service objeleri somut sınıf yerine protocol’e bağımlı haldedir. Ayrıca HomeDataProvider sınıfı HomeDataProviderProtocol’ü conform ederek DIP ihlalini engellemektedir.

Tek amacı verilerin Core Data veya Sqlite ile yönetilmesini sağlayan bir sınıf oluşturdum. Bu sayede HomeViewModel içerisinde SRP ihlali engelleniyor. manager objesi somut sınıf yerine protocol’e bağımlı haldedir. Ayrıca HomeRepository sınıfı HomeRepositoryProtocol’ü conform ederek DIP ihlalini engellemektedir.

Tek amacı verilerin network katmanı ile yönetilmesini sağlayan bir sınıf oluşturdum. Bu sayede HomeViewModel içerisinde SRP ihlali engelleniyor. service objesi somut sınıf yerine protocol’e bağımlı haldedir. Ayrıca HomeService sınıfı HomeServiceProtocol’ü conform ederek DIP ihlalini engellemektedir.

viewModel objemiz artık soyut bir bağımlılığa sahip. Böylece DIP ihlali düzeltildi.

tableView fonksiyonlarına baktığımızda if-else, switch-case yapısından kurtulduğunu görüyoruz. Kontrol ederken kullanılan HomeCellType enum’ı değiştirilerek HomeCellItem protocol’ü kullanıldı. Kullanılan bu yöntemle birlikte artık tableView fonksiyonlarımız değişikliğe kapalı hale getirildi. SimpleJokeCell ve FavoriteJokeCell classlarını inceleyerek nasıl bir yaklaşımda bulunduğumu görebilirsiniz. Bu sayede OCP ihlali de düzeltilmiş oldu.

NetworkService sınıfını DIP ihlalinden kurtarmak için bir protocol oluşturduk ve bu protocol’ü conform etmesini sağladık.

HomeCellDelegate içerisinde her zaman kullanılmayan updateJoke fonksiyonu ayrı bir protocol içerisine alınarak LSP ve ISP ihlali düzeltildi. Birden fazla type conform etmesini istediğimiz objeler varsa typealias kullanabiliriz.

Ayrıca delegate, joke objeleri soyutlaştırılarak DIP ihlali de düzeltilmiş oldu.

CoreDataManager ve SqliteManager aynı protocol’ü conform etmektedir. CoreDataManagerProtocol oluşturularak SqliteManager’da ihtiyaç duyulmayacak fonksiyonlar tanımlanmıştır. Ayrıca her iki sınıfta bulunan getJokes()’un throw ettiği Error ortaklaştırılmıştır. Bu çözümler sayesinde LSP ve ISP ihlalleri düzeltilmiştir.

Uygulamanın struct olan JokeModel’e bağımlılık oluşturması yerine JokeModelProtocol soyutlamasıyla DIP ihlalinin önüne geçmiş oluyoruz.

Tüm bu yapıyı oluşturduktan sonra Dependency Injection ile Home modülümü ayağa kaldırıyorum.

SOLID’in uygulamaya kazandırdıkları

  • Ana sayfadaki tableView yapısı değişikliğe kapalı hale getirildi. Bu sayede yeni tasarım, logic eklemek istediğimizde mevcut yapıyı bozmadan düzenleme yapabilme şansına sahibiz. Rigidity, yapıyı korumaya devam ettiğimiz sürece bizi daha az etkileyecek.
  • ViewModel’a ve diğer sınıflara test yazabilmemiş kolaylaştı. Artık daha sağlıklı testler yazabiliyoruz.
  • Beklenmedik logicler ile karşılaştığımızda daha dikkatli Error Handling yapabilme şansı tanımış olduk. Fragility sorunu artık daha az tehlike yaratıyor.
  • Home modülünü uygulamadan kolaylıkla çıkarıp ayrı bir proje/modül haline getirebileceğiz. Immobility sorunu en aza indirgendi.
  • Kodun okunabilirliği arttığı için, daha kolay ve daha amaca yönelik testler yazılabildiği için, kodların değiştirilmesi, eklenmesinin sonuçlarının daha öngörülebilir olduğu için ekibe yeni katılan bir geliştiricinin adaptasyonu kolay olacaktır.
  • Proje içerisindeki planlamalar daha tutarlı öngörülebilir olacaktır.

Sonuç olarak

Bu yazıda, SOLID prensiplerini hem teorik açıdan inceledik hem de çeşitli örneklerle bu prensiplerin pratikte nasıl uygulanabileceğini gösterdik. Her bir prensibin yazılım tasarımı ve geliştirme sürecindeki önemini vurgulayarak, daha esnek, sürdürülebilir ve anlaşılabilir kodlar oluşturmanın önemini anladık.

SOLID prensiplerini anlamak ve uygulamak, yazılım projelerinin kalitesini artırmak ve uzun vadeli bakım maliyetlerini azaltmak için kritik öneme sahiptir. Her bir prensibin, yazılım geliştirme sürecinde karşılaşılan yaygın sorunlara çözümler sunduğunu ve daha iyi bir kod tabanı oluşturmanın anahtarı olduğunu gördük.

Herhangi bir öneriniz, sorunuz veya yorumunuz varsa lütfen bana Twitter üzerinden ulaşmaktan çekinmeyin. Katkılarınız bir geliştirici ve yazar olarak gelişmeme yardımcı oluyor.

Bu yazıda kullanılan tüm yazılı, görsel içeriğe ve kod içeriğine buradan ulaşabilirsiniz.

İlginiz için teşekkürler, iyi kodlamalar!

--

--