Software Design Principles (Part 2)

Firuze Gümüş
7 min readApr 19, 2024

--

Selamlar herkese,

Bu yazıda software design prensiplerine kaldığımız yerden devam ediyor olacağız. Part1'e bir göz atmak isterseniz sizi şöyle alalım :)

Gelelim diğer prensiplere..

Hide Implementation Details

Uygulama detaylarını gizlemek, bir bileşeni kullanan diğer modüllerde/classlarda değişiklik yapmaksızın o bileşende değişiklik yapabilmemize olanak tanır. Bunun için somut(concrete) sınıflar yerine interfaceler kullanılması gerekir. Bunun yanında tabii ki gerekli olduğu noktalarda fonksiyonları erişime açarak gerekmediğinde kapalı tutarak encapsulation ile erişilebilirlik yönetimini de doğru yapmak gerekir.

Daha iyi anlamak adına aşağıdaki örneğe hızlıca bir bakalım..

interface UserService {
fun getUserById(userId: Int): User
}

class User(private val userId: Int, private val userName: String) {
fun displayUserInfo() {
println("User ID: $userId, Username: $userName")
}
}

class DatabaseUserService : UserService {
override fun getUserById(userId: Int): User {
// Örnek bir kullanıcıyı veritabanından alalım
return User(userId, "John Doe")
}
}

class UserManager(private val userService: UserService) {
fun displayUserDetails(userId: Int) {
val user = userService.getUserById(userId)
user.displayUserInfo()
}
}

fun main() {
val userService: UserService = DatabaseUserService()
val userManager = UserManager(userService)
// Kullanıcı bilgilerini görüntüleme
userManager.displayUserDetails(123)
}

Basitçe anlatmak gerekirse UserManager classı bağımlı olduğu UserService interface’inin sunduğu getUserById metodunu çağırdığında UserService’in bu datayı nereden, nasıl aldığından habersizdir. Sadece sonucu alır ve bununla ilgilenir. Bu hem kodun bakımını kolaylaştırır hem de okunabilirliğini arttırır. İlerde örneğin database’in Sql’den Realm’e değiştirilmesi söz konusu olduğunda bile kodun genişleyebilir olmasına imkan tanır. Bu aynı zamanda SOLID’ın D harfinden gelen Dependency Inversion prensibinin de amacıdır ki bu konuya daha sonraki yazılarda değineceğiz.

Separation of Concerns

Seperation of concerns, bir bilgisayar programını her biri ayrı bir concern’u ele alan ayrı bölümlere bölen bir tasarım ilkesidir.

Peki concernden kastımız nedir?

“Concern”, bir yazılım sistemindeki belirli bir sorumluluğu, işlevi veya ilgi alanını ifade eder. Başka bir deyişle, bir “concern”, bir bileşenin veya birim’in ilgilendiği belirli bir konudur.

Örneğin, bir e-ticaret uygulaması üzerinde çalışırken, sipariş yönetimi, ödeme işlemleri, envanter takibi ve kullanıcı yönetimi gibi farklı konulara odaklanabiliriz. Her biri, uygulamanın farklı alanlarına ait olan bu konular, ayrı ayrı ilgi alanları veya “concerns” olarak düşünülebilir.

Separation of Concerns prensibi, bu farklı ilgi alanlarını birbirinden ayırarak, her birinin kendi içinde bağımsız olarak yönetilmesini ve değiştirilmesini sağlar. İyi bir şekilde concernlerin ayrılmasını içeren bir programa modüler program denir.

Modülerlik ve dolayısıyla concernlerin ayrılması, iyi tanımlanmış bir arabirime sahip kod birimi içindeki bilgiyi kapsülleyerek elde edilir. OOP’de Encapsulation dediğimiz olay, bilgiyi gerekmediği takdirde dışarıdan gizlemektir. Hepimizin projelerimizde bolca aşina olduğu katmanlı tasarımlar, concernlerin ayrılmasının bir başka uygulamasıdır. Layer-based veya Feature-based gibi modular architecture uygulamaları buna hizmet eder. (Layer-based mimaride kullanılan presentation katmanı, domain katmanı, data katmanı gibi)

SoC, bakımı kolaylaştırır ve kodun yeniden kullanımını sağlar. Tabii bu zamanla oluşacak bir tecrübedir. Neyi nasıl ayıracağımızı doğru planlamamız gerekir. Concernlerin birbiriyle örtüşmemesi yani couplingin sıkı değil gevşek olması oldukça önemliyken cohesion’ın yüksek tutulması da bir o kadar gerekli ve önemlidir. Peki o zaman sırası gelmişken cohesion ve coupling kavramlarına bir bakalım.

Cohesion

Low coupling, high cohesion

Cohesion (Yapışıklık), bir yazılım modülünün veya bileşenin içindeki farklı sorumlulukların ne kadar güçlü bir şekilde ilişkilendirildiğini belirtir. Yani bir modülün içindeki farklı işlevlerin birbirleriyle ne kadar uyumlu ve ilgili olduğunu ifade eder. Yüksek cohesion, bir modülün içindeki işlevlerin birbirleriyle güçlü bir şekilde ilişkilendirilmiş olduğunu gösterirken, düşük cohesion ise işlevler arasında zayıf bir ilişki veya bağlantı olduğunu gösterir.

Aslında tam anlamıyla SOLID’in S harfinin karşılığı olan Single Responsibility Prensibi’nin bize anlatmak istediği gibi bir sınıfın ya da metodun yalnızca tek bir sorumluluğu olması yani değiştirilmesi için tek bir nedene ihtiyaç duyulması da bununla örtüşen bir prensiptir. Dolayısıyla cohesion’ı maksimize etmeye çalışmalıyız. Böylece yüksek cohesion bileşenlerin daha iyi okunabilir olmasını, kolayca bakımının yapılabilmesini ve yeniden kullanılmasını sağlar. Bir sınıfın içerdiği metodlar birbirlerinden tamamen farklı işlerden sorumluysa onları bir arada tutmak low cohesiona neden olur. Bu da istemediğimiz bir durumdur.

Cohesion kavramını daha iyi anlamak için aşağıdaki kodu inceleyelim.

class MusteriEklemeIslemleri {
fun ekle(musteri: Musteri) {

}
}

class MusteriGuncellemeIslemleri {
fun guncelle(musteri: Musteri) {

}
}

class MusteriSilmeIslemleri {
fun sil(musteriId: Int) {

}
}

class MusteriListelemeIslemleri {
fun listele(): List<Musteri> {
return listOf()
}
}

data class Musteri(val id: Int, val ad: String, val soyad: String)

fun main() {
val musteriEklemeIslemleri = MusteriEklemeIslemleri()
musteriEklemeIslemleri.ekle(Musteri(1, "John", "Doe"))

val musteriGuncellemeIslemleri = MusteriGuncellemeIslemleri()
musteriGuncellemeIslemleri.guncelle(Musteri(1, "Joe", "Doe"))

val musteriSilmeIslemleri = MusteriSilmeIslemleri()
musteriSilmeIslemleri.sil(1)

val musteriListelemeIslemleri = MusteriListelemeIslemleri()
val musteriler = musteriListelemeIslemleri.listele()
println("Müşteriler: $musteriler")
}

Bu örnekte, müşteri işlemleri farklı sınıflara ayrılmıştır (MusteriEklemeIslemleri, MusteriGuncellemeIslemleri, MusteriSilmeIslemleri ve MusteriListelemeIslemleri). Her bir sınıf özünde birbirleriyle ilgili işleri yaptığı halde her bir iş için ayrı bir class oluşturulmuş olduğunu görmekteyiz. Bu nedenle bu tasarım düşük cohesion’a sahiptir. Halbuki müşteri ile alakalı olan CRUD işlemlerini tek bir sınıfta ayrı ayrı metodlar altında yazabiliriz.

Nasıl yani?

class MusteriIslemleri {
fun ekle(musteri: Musteri) {

}

fun guncelle(musteri: Musteri) {

}

fun sil(musteriId: Int) {

}

fun listele(): List<Musteri> {
return listOf()
}
}

data class Musteri(val id: Int, val ad: String, val soyad: String)

fun main() {
val musteriIslemleri = MusteriIslemleri()

musteriIslemleri.ekle(Musteri(1, "John", "Doe"))

musteriIslemleri.guncelle(Musteri(1, "Joe", "Doe"))

musteriIslemleri.sil(1)

val musteriler = musteriIslemleri.listele()
println("Müşteriler: $musteriler")
}

SOLID’in S’si ne olacak?!!

Müşteri işlemleri classının başlı başına tek bir işi var o da müşteri bilgilerini düzenlemek. İçerisindeki her metot da kendi içinde tek bir işi yapacağından hem single responsibility’e aykırı davranmış olmayız hem de cohesion’ı düşük tutmamış oluruz. Burada önemli bir konuya dikkat çekmek gerekiyor. Tasarım prensiplerini uygularken neyi ne seviyede yapabileceğimiz hayati önem taşıyor. Ezbere bir şekilde herşeyi tamamen ayrık yazmak değil amacımız. SRP’ye olabildiğince aykırı davranmamalıyız evet ama bu kesinlikle herşeyin ayrı ayrı sınıflara yazılması anlamına gelmiyor. Her zaman anlamlı işlevleri bir arada tutmak esastır.

Coupling

Coupling türkçeye çevirecek olursak “Bağlanma, Bağlaşım”, iki veya daha fazla bileşen arasındaki ilişkinin bağlılık derecesini ifade eder. Bir diğer deyişle iki modülün ne kadar yakından ilişkili olduğunun bir ölçüsüdür. Bir bileşenin diğerine olan bağlılığı ne kadar yüksekse, bu bileşenler arasındaki bağlanma o kadar yüksek kabul edilir. Bu bağımlılık dereceleri genelde Gevşek bağlılık (Loosely Coupled) ve Sıkı bağlılık (Tightly Coupled) olarak belirtilir.

Yüksek bağlanma, yazılım sistemlerinde istenmeyen bir durumdur çünkü bir bileşende yapılan herhangi bir değişiklik, diğer bileşenlerde beklenmeyen etkilere veya hatalara neden olabilir. Bu nedenle, düşük bağlanma, yazılım sistemlerinin daha esnek, modüler olmasını ve bakımının daha kolay yapılmasını sağlar.

Coupling ve cohesion genellikle birbirleriyle ilişkilendirilir ve karşılaştırılır. İdeal olarak düşük bağlanma ve yüksek cohesion hedeflenir. Çünkü bu daha modüler, esnek ve bakımı kolay bir yazılım tasarımını işaret eder.

Düşük Bağlanma (Low Coupling): Bileşenler arasındaki bağımlılık minimum düzeydedir. Bu, bir bileşenin iç yapısındaki değişikliklerin diğer bileşenleri minimum düzeyde etkilemesi anlamına gelir. Genellikle, bileşenler arasındaki iletişim, arayüzler veya soyutlamalar aracılığıyla gerçekleştirilir, böylece bileşenler birbirlerinin iç yapısını bilmezler.

Yüksek bağlanma (High coupling): Yüksek bağlanma, yazılım bileşenleri arasında sıkı bir ilişki veya bağımlılık olduğunu gösterir. Bir bileşenin iç yapısındaki değişikliklerin diğer bileşenleri olumsuz etkileme olasılığı yüksektir. Örneğin, bir bileşen başka bir bileşene doğrudan referanslar içeriyorsa ve bu referanslar sık sık kullanılıyorsa, bu durum yüksek bağlanmayı temsil eder.

Sıkı bağlı (high coupling) modüllerin birçok dezavantajı vardır:

  1. Sıkı bağlı modüllerde, bir modülde yapılan herhangi bir değişiklik, diğer modüllerde beklenmeyen hatalara veya bozulmalara neden olabilir. Bu durum, yazılımın istikrarını ve güvenilirliğini olumsuz etkileyebilir.
  2. Sıkı bağlı modüllerde, bir modülde yapılan bir değişiklik genellikle diğer modüllerde de değişikliklerin bir dalgasını tetikler. Bu durum da yine, yazılım geliştirme sürecini karmaşıklaştırabilir ve daha fazla test ve kontrol gerektirebilir.
  3. Bağımlılık arttıkça, modüller arasında yeniden kullanılabilirlik azalır. Bu da, yazılımın bakımını zorlaştırabilir ve kod tekrarını artırabilir.

Couplingi azaltmak için şu yöntemler kullanılabilir:

  • Modüller arasındaki bağımlılığı azaltmak için, modüllerin iç ayrıntılarını gizlemek ve sadece arayüzler aracılığıyla etkileşim kurmak önemlidir. Bu, modüllerin birbirlerinin iç yapısını bilmeden birbirleriyle iletişim kurmasını sağlar.
  • Bir modülün diğer modülleri doğrudan işlemesinden kaçınılmalıdır. Bunun yerine, modüller arasında soyutlama veya ara katmanlar kullanarak etkileşim sağlanmalıdır.
  • Modüller, mümkün olduğunca bağımsız olmalı ve alternatif uygulamalarla değiştirilebilir olmalıdır. Bu, sistemdeki bağımlılığı azaltarak esneklik ve yeniden kullanılabilirlik sağlar.

Yine bir örnekle ilerleyelim:

class OdemeServisi {
fun odemeYap(kullaniciId: Int, miktar: Double) {

}
}

class SiparisServisi(private val odemeServisi: OdemeServisi) {
fun siparisVer(kullaniciId: Int, miktar: Double) {
// Sipariş verme işlemini gerçekleştir
// Ödemeyi ayrı bir servise bırak
odemeServisi.odemeYap(kullaniciId, miktar)
}
}

Bu örnekte, SiparisServisi sınıfı, sipariş alma işlemini gerçekleştirirken ödeme işlemini doğrudan kendisi yapmaz. Bunun yerine, ödeme işlemini OdemeServisi sınıfına bırakır. Bu şekilde, SiparisServisi sınıfı, sipariş verme işlevini gerçekleştirirken doğrudan ödeme işlemiyle ve bu işlemin detaylarıyla ilgilenmez.

Yalnız dikkatinizi çektiyse burda bazı sıkıntılar mevcut. SiparisServisi sınıfı doğrudan concrete(somut) bir sınıf olan OdemeServisi ile iletişim kuruyor. Bu bağımlılığın sıkı olduğu bir durumdur. Şimdi örneğimizi biraz daha düzenleyelim..

interface OdemeServisi {
fun odemeYap(kullaniciId: Int, miktar: Double)
}

class PayPalOdemeServisi : OdemeServisi {
override fun odemeYap(kullaniciId: Int, miktar: Double) {
// PayPal üzerinden ödeme yap
}
}

class SiparisServisi(private val odemeServisi: OdemeServisi) {
fun siparisVer(kullaniciId: Int, miktar: Double) {
odemeServisi.odemeYap(kullaniciId, miktar)
}
}

Artık SiparisServisi sınıfı, ödeme işlemleri için bir arayüz olan OdemeServisi ile etkileşim kuruyor. SiparisServisi sınıfı, hangi ödeme servisinin kullanılacağını belirlemekten bağımsızdır. Ödeme servisi, PaypalOdemeServisi gibi farklı bir sınıf olabilir. Bu, sistemde gevşek bağlılık sağlar ve ödeme servisinin değiştirilmesini kolaylaştırır. Ayrıca SOLID’in Open-Closed prensibine uygun bir kod olduğundan bahsetmiyorum bile :)

Evet bu yazının da sonuna geldik. Aralarda SOLID’e bazı atıflarda bulunsak da Part3'te daha detaylı inceleme fırsatı bulacağız. O zaman bir sonraki yazıda görüşmek üzere..

Kodla kalın!

--

--