Giriş
SOLID, popüler olarak Uncle Bob olarak bilinen Robert C. Martin tarafından ortaya atılan, nesne yönelimli tasarımın beş ilkesinin anımsatıcı bir kısaltmasıdır. Bu ilkeler; yazılım tasarımcıları, mimarları, mühendisleri ve geliştiricilerinin daha esnek, bakımı kolay ve ölçeklenebilir yazılımlar oluşturmasına yardımcı olmayı amaçlar. Bu ilkeleri takip ederek test edilmesi, yeniden yapılandırılması (refactor), yeniden kullanılması ve genişletilmesi daha kolay sınıflar tasarlayabilirsiniz.
SOLID kısaltmasının açılımı şudur:
S – Tek Sorumluluk İlkesi (Single Responsibility Principle)
O – Açık-Kapalı İlkesi (Open-Closed Principle)
L – Liskov'un Yerine Geçme İlkesi (Liskov Substitution Principle)
I – Arayüz Ayrımı İlkesi (Interface Segregation Principle)
D – Bağımlılığın Tersine Çevrilmesi İlkesi (Dependency Inversion Principle)
Bu makalede, daha iyi kod yazmanıza nasıl yardımcı olabileceğini anlamak için her bir ilkeyi ayrı ayrı açıklayacağız. Ayrıca, bunları kodlama yolculuğunuzda nasıl uygulayabileceğinizi ve temiz kod mimarisinde nelerden kaçınmanız gerektiğini göstermek için her bir ilke için kod parçacıkları ekleyeceğiz. Kavramları göstermek için, JetBrains ve açık kaynak katkıda bulunanlar tarafından geliştirilen Kotlin programlama dilini kullanacağız.
Tek Sorumluluk İlkesi (Single Responsibility Principle)
Tek Sorumluluk İlkesi (SRP), bir programdaki her sınıfın veya modülün tek ve iyi tanımlanmış bir sorumluluğu olması gerektiğini belirten bir yazılım tasarım ilkesidir. Bu, bir sınıfın değişmek için yalnızca tek bir nedeni olması ve programın işlevselliğinin yalnızca tek bir bölümünden sorumlu olması gerektiği anlamına gelir.
Bu ilkenin Kotlin:
|
1 2 3 4 5 6 7 8 9 |
data class User( val firstName: String, val lastName: String, val email: String ) |
Kotlin'deki data anahtar kelimesi, bu sınıfın bir veri sınıfı (data class) olduğunu belirtir; bu da veri tutmak amacıyla tasarlandığı ve karmaşık bir davranışa sahip olmadığı anlamına gelir. Bu veri sınıfını kullanarak, aşağıda gösterildiği gibi kullanıcıları yönetmek için bir UserService sınıfı oluşturabiliriz:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class UserService { fun createUser(user: User) { // veritabanında kullanıcı oluşturma kodu } fun deleteUser(user: User) { // veritabanından kullanıcı silme kodu } fun updateUser(user: User) { // veritabanında kullanıcı güncelleme kodu } fun getUser(id: Int): User { // veritabanından kullanıcı getirme kodu } } |
Bu örnekte, UserService sınıfının tek bir sorumluluğu vardır: veritabanındaki kullanıcıları yönetmek. Her kullanıcı, daha önce paylaşılan data class User kodundaki veri sınıfı ile temsil edilir. UserService sınıfındaki tüm metotlar bu sorumlulukla ilgilidir ve bu nedenle uyumludur (cohesive). Bu durum, sınıftaki tüm metotların tek ve iyi tanımlanmış bir görevle ilgili olduğu açık olduğundan, sınıfın anlaşılmasını ve bakımını kolaylaştırır.
Tek Sorumluluk İlkesi'ni (SRP) ihlal eden bir metot, UserService sınıfında, sınıfın birincil sorumluluğu olan veritabanındaki kullanıcıları yönetmekle ilgisi olmayan bir metot olacaktır. Örneğin, UserService sınıfının aşağıdaki varyasyonunu ele alalım:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class UserService { fun createUser(user: User) { // veritabanında bir kullanıcı oluşturmak için kod } fun deleteUser(user: User) { // veritabanından bir kullanıcıyı silmek için kod } fun updateUser(user: User) { // veritabanındaki bir kullanıcıyı güncellemek için kod } fun getUser(id: Int): User { // veritabanından bir kullanıcıyı getirmek için kod } fun sendEmail(user: User, subject: String, message: String) { // kullanıcıya e-posta göndermek için kod } } |
Bu örnekte, sendEmail metodu, UserService sınıfının bir veritabanındaki kullanıcıları yönetmek olan birincil sorumluluğuyla ilgili değildir. Bu metot, kullanıcıları yönetmekten ayrı bir konu olan e-posta göndermekten sorumludur. Sonuç olarak, bu metot UserService sınıfının değişmesi için ikinci bir neden ortaya çıkardığından SRP'yi ihlal eder.
Tek Sorumluluk İlkesine uymak için, e-posta gönderme işlevini EmailService sınıfı gibi ayrı bir sınıfa ayırmak daha iyi olacaktır. Bu, UserService sınıfının kullanıcıları yönetme şeklindeki birincil sorumluluğuna odaklanmasını ve EmailService sınıfının e-posta gönderme sorumluluğuna odaklanmasını sağlayacaktır.
Tek Sorumluluk İlkesinin bir sınıfın sahip olduğu metot sayısı ile değil, metotların uyumu ve bir sınıf içindeki sorumlulukların net bir şekilde ayrılmasıyla ilgili olduğunu unutmamalısınız.
Açık-Kapalı İlkesi
Açık Kapalı İlkesi (OCP), yazılım varlıklarının (sınıflar, modüller veya fonksiyonlar gibi) genişletilmeye açık, ancak değiştirilmeye kapalı olması gerektiğini belirten bir yazılım tasarım ilkesidir. Bu, mevcut kodunu değiştirmeden bir sınıfa veya modüle yeni işlevsellik eklemenin mümkün olması gerektiği anlamına gelir.
Önceki örnekteki UserService sınıfını ele alalım. UserService sınıfına, kullanıcıları e-posta adresine göre aramamızı sağlayan yeni bir özellik eklemek istediğimizi varsayalım. Bunu yapmanın bir yolu, aşağıdaki kodda vurgulandığı gibi UserService sınıfına yeni bir metot eklemektir:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class UserService { fun createUser(user: User) { // veritabanında bir kullanıcı oluşturmak için kod } fun deleteUser(user: User) { // veritabanından bir kullanıcıyı silmek için kod } fun updateUser(user: User) { // veritabanındaki bir kullanıcıyı güncellemek için kod } fun getUser(id: Int): User { // veritabanından bir kullanıcıyı getirmek için kod } fun searchUsersByEmail(email: String): List<User> { // kullanıcıları e-posta adresine göre aramak için kod } } |
Bu yaklaşım çalışır, ancak yeni özelliği eklemek için mevcut UserService sınıfını değiştirmemiz gerektiğinden Açık Kapalı İlkesini ihlal eder. Daha iyi bir yaklaşım, UserService sınıfının kodunu değiştirmeden işlevselliğini genişletmek için kalıtım veya kompozisyon kullanmak olacaktır.
Bunu başarmak için, UserSearchService adında, UserService sınıfını genişleten ve e-posta arama işlevini ekleyen yeni bir sınıf oluşturabiliriz:
|
1 2 3 4 5 6 7 8 9 |
class UserSearchService(private val userService: UserService) : UserService() { fun searchUsersByEmail(email: String): List<User> { // kullanıcıları e-posta adresine göre aramak için kod } } |
Bu örnekte, UserSearchService sınıfı, tarafından sunulanın ötesinde ek işlevsellik sağladığı için genişletilmeye açıktır.UserService sınıfı. Aynı zamanda, UserService sınıfı değişikliğe kapalı kalır, çünkü e-posta arama özelliğini eklemek için kodunu değiştirmemiz gerekmedi.
Liskov İkame Prensibi
Liskov İkame Prensibi (LSP), bir üst sınıfın nesnelerinin, programın doğruluğunu etkilemeden bir alt sınıfın nesneleriyle değiştirilebilmesi gerektiğini belirten bir yazılım tasarım prensibidir. Bu, bir alt sınıfın kendi üst sınıfı için geçerli bir ikame olması ve aynı bağlamda kullanıldığında üst sınıfla aynı şekilde davranması gerektiği anlamına gelir.
Gösterime User ve UserService sınıflarını kullanarak devam edeceğiz. User sınıfının genişletilebilmesini sağlamak için Kotlin'de open anahtar kelimesini kullanırız:
|
1 2 3 4 5 6 7 8 9 |
open class User( val firstName: String, val lastName: String, val email: String ) |
İşte yukarıdaki UserService veri sınıfını kullanan orijinal User sınıfı:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class UserService { fun createUser(user: User) { // veritabanında bir kullanıcı oluşturmak için kod } fun deleteUser(user: User) { // veritabanından bir kullanıcıyı silmek için kod } fun updateUser(user: User) { // veritabanındaki bir kullanıcıyı güncellemek için kod } fun getUser(id: Int): User { // veritabanından bir kullanıcıyı getirmek için kod } } |
Farz edelim ki User sınıfının, AdminUser adında yönetici yetkilerine sahip kullanıcıları temsil eden bir alt sınıfını oluşturmak istiyoruz. Bunu şu şekilde yapabiliriz:
|
1 2 3 4 5 6 7 8 9 |
class AdminUser( firstName: String, lastName: String, email: String, ) : User(firstName, lastName, email) |
Bu örnekte, AdminUser sınıfı, User sınıfı ile aynı şekilde davrandığı ve bir User nesnesinin beklendiği her yerde kullanılabileceği için User sınıfı için geçerli bir ikamedir. Örneğin, AdminUser sınıfını UserService sınıfı ile şu şekilde kullanabiliriz:
|
1 2 3 4 5 6 7 8 9 |
fun main() { val userService = UserService() val adminUser = AdminUser("John", "Doe", "john.doe@example.com") userService.createUser(adminUser) } |
Bu kod doğrudur, çünkü AdminUser sınıfı, User sınıfı için geçerli bir ikamedir ve bir User nesnesiyle aynı şekilde kullanılabilir.
Liskov İkame Prensibi’nin sadece kalıtımdan ibaret olmadığını unutmamak önemlidir. Bu prensip, alt sınıfın nasıl uygulandığına bakılmaksızın, alt sınıf nesnelerinin üst sınıf nesneleriyle aynı şekilde davranmasını sağlamakla ilgilidir. Örneğin, eğer AdminUser sınıfı, bir şekilde User sınıfından farklı davranacak olsaydı, User sınıfı için geçerli bir ikame olmayacağından Liskov İkame Prensibi'ni ihlal ederdi.
Interface Segregation Principle
Arayüz Ayrımı Prensibi (ISP), istemcilerin kullanmadıkları interfaces bağımlı olmaya zorlanmaması gerektiğini belirten bir yazılım tasarım prensibidir. Bu, tek bir işi iyi yapan küçük, odaklanmış interfaces oluşturmanın, birçok şeyi yapmaya çalışan büyük, genel amaçlı interfaces oluşturmaktan genellikle daha iyi bir fikir olduğu anlamına gelir.
İşte önceki örneklerdeki User ve UserService sınıflarını yeniden yazarak bu prensibin nasıl uygulanabileceğine dair bir örnek:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
interface UserService { fun createUser(user: User) fun deleteUser(user: User) fun updateUser(user: User) fun getUser(id: Int): User } class DatabaseUserService : UserService { override fun createUser(user: User) { // veritabanında bir kullanıcı oluşturmak için kod } override fun deleteUser(user: User) { // veritabanından bir kullanıcıyı silmek için kod } override fun updateUser(user: User) { // veritabanında bir kullanıcıyı güncellemek için kod } override fun getUser(id: Int): User { // veritabanından bir kullanıcıyı getirmek için kod } } |
Bu örnekte, UserService arayüzü, bir veritabanındaki kullanıcıları yönetmekle ilişkili dört yöntem tanımlar. DatabaseUserService sınıfı bu arayüzü uygular ve bu yöntemler için somut uygulamalar sağlar.
Farz edelim ki UserService arayüzüne, kullanıcıları e-posta adresine göre aramamıza olanak tanıyan yeni bir özellik eklemek istiyoruz. Bunu yapmanın bir yolu, UserService arayüzüne yeni bir yöntem eklemek olacaktır:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
interface UserService { fun createUser(user: User) fun deleteUser(user: User) fun updateUser(user: User) fun getUser(id: Int): User fun searchUsersByEmail(email: String): List<User> } |
Kodunuz, UserService arayüzünü uygulayan tüm sınıflarda bu yöntemi de uygulamadığınız sürece çalışmayacaktır:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class DatabaseUserService : UserService { override fun createUser(user: User) { // veritabanında bir kullanıcı oluşturmak için kod } override fun deleteUser(user: User) { // veritabanından bir kullanıcıyı silmek için kod } override fun updateUser(user: User) { // veritabanında bir kullanıcıyı güncellemek için kod } override fun getUser(id: Int): User { // veritabanından bir kullanıcıyı getirmek için kod } override fun searchUsersByEmail(email: String): List<User> { // kullanıcıları e-posta adresine göre aramak için kod } } |
Bu yaklaşım çalışsa da, DatabaseUserService sınıfını, ihtiyaç duymayabileceği veya kullanmayabileceği bir yöntemi ( searchUsersByEmail ) uygulamaya zorladığı için Arayüz Ayrımı Prensibini (Interface Segregation Principle) ihlal eder.
Daha iyi bir yaklaşım, ayrı bir arayüz oluşturmak olacaktır:
|
1 2 3 4 5 |
interface UserSearchService { fun searchUsersByEmail(email: String): List<User> } |
Artık ayrı, küçük ve odaklanmış arayüzlerimiz var, yani UserService ve UserSearchServicetek bir sorumluluğa sahip olan. Bu arayüzlerin tüm işlevlerine ihtiyaç duyan bir sınıf, bunları aşağıdaki kod parçacığında gösterildiği gibi uygulayabilir:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class DatabaseUserService : UserService, UserSearchService { override fun createUser(user: User) { // veritabanında bir kullanıcı oluşturmak için kod } override fun deleteUser(user: User) { // veritabanından bir kullanıcıyı silmek için kod } override fun updateUser(user: User) { // veritabanındaki bir kullanıcıyı güncellemek için kod } override fun getUser(id: Int): User { // veritabanından bir kullanıcıyı getirmek için kod } override fun searchUsersByEmail(email: String): List<User> { // e-posta adresine göre kullanıcıları aramak için kod } } |
Bu, istemcilerin (örneğin DatabaseUserService sınıfının) kullanmadıkları arayüzlere bağımlı olmaya zorlanmamasını sağladığı için Arayüz Ayrımı Prensibi'ne (Interface Segregation Principle) uygundur.
Bu kavramı daha iyi anlamak için, MemoryUserService adında, UserService arayüzünü uygulayan ancak e-posta arama işlevine ihtiyaç duymayan başka bir sınıfımız olduğunu varsayalım; kodu şu şekilde yazabiliriz:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class MemoryUserService : UserService { private val users = mutableListOf<User>() override fun createUser(user: User) { users.add(user) } override fun deleteUser(user: User) { users.remove(user) } override fun updateUser(user: User) { val index = users.indexOf(user) if (index >= 0) { users[index] = user } } override fun getUser(id: Int): User { return users[id] } } |
Bu örnekte, MemoryUserService sınıfının yalnızca UserService arayüzünde tanımlanan yöntemleri uygulaması gerekir ve e-posta arama işlevi konusunda endişelenmesine gerek yoktur. Bu durum, MemoryUserService sınıfının, ilgisiz işlevleri uygulamaya zorlanmak yerine, bellekteki kullanıcıları yönetme şeklindeki birincil sorumluluğuna odaklanmasını sağlar.
Dependency Inversion Principle
Bağımlılığın Tersine Çevrilmesi İlkesi (Dependency Inversion Principle - DIP), üst düzey modüllerin alt düzey modüllere bağımlı olmaması gerektiğini, aksine her ikisinin de soyutlamalara bağımlı olması gerektiğini belirten bir yazılım tasarım ilkesidir. Bu, yazılımınızı genel olarak üst düzey bileşenlerin alt düzey bileşenlerin belirli uygulamalarına bağlı olmayacak, aksine farklı şekillerde uygulanabilen soyutlamalara (arayüzler veya soyut sınıflar gibi) bağımlı olacak şekilde tasarlamanın iyi bir fikir olduğu anlamına gelir.
Bu ilkenin, önceki kod parçacıklarında kullanılan User ve UserService sınıflarına nasıl uygulanabileceğine dair bir örneğe bakalım:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
interface UserRepository { fun create(user: User) fun delete(user: User) fun update(user: User) fun get(id: Int): User } class UserService(private val repository: UserRepository) { fun createUser(user: User) { repository.create(user) } fun deleteUser(user: User) { repository.delete(user) } fun updateUser(user: User) { repository.update(user) } fun getUser(id: Int): User { return repository.get(id) } } |
Bu örnekte, UserService sınıfı, kullanıcı deposunun belirli bir uygulamasına bağımlı olmak yerine UserRepository arayüzü (interface) adı verilen bir soyutlamaya bağımlıdır. Bu, UserRepository arayüzünü, UserService sınıfını etkilemeden veritabanı veya bellek içi depolama gibi farklı şekillerde uygulamamıza olanak tanır.
Örneğin, veritabanı kullanan UserRepository arayüzünün bir uygulaması şöyledir:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class DatabaseUserRepository: UserRepository { override fun create(user: User) { // veritabanında bir kullanıcı oluşturmak için kod } override fun delete(user: User) { // veritabanından bir kullanıcıyı silmek için kod } override fun update(user: User) { // veritabanındaki bir kullanıcıyı güncellemek için kod } override fun get(id: Int): User { // veritabanından bir kullanıcıyı getirmek için kod } } |
İşte bellek içi depolama kullanan başka bir UserRepository arayüzü uygulaması:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class MemoryUserRepository : UserRepository { private val users = mutableListOf<User>() override fun create(user: User) { users.add(user) } override fun delete(user: User) { users.remove(user) } override fun update(user: User) { val index = users.indexOf(user) if (index >= 0) { users[index] = user } } override fun get(id: Int): User { return users[id] } } |
Bu, sistemin geri kalanını etkilemeden depo uygulamasını değiştirmemize olanak tanıdığı için sistemi daha esnek ve bakımı kolay hale getirir. Ayrıca testlerimizde UserRepository bağımlılığını taklit edebileceğimiz (mock) için UserService sınıfını test etmeyi de kolaylaştırır.
Sonuç
Bu makalede, SOLID'in beş prensibinden bahsettik. her bir ilkeyi karşılayan kod ve paylaşılan kod parçacıkları. Bu ilkelere bağlı kalmak, daha esnek, bakımı kolay ve ölçeklenebilir yazılım sistemleri tasarlamanıza yardımcı olabilir. Ancak, bu ilkelerin kesin kurallar olmaktan ziyade kılavuz ilkeler olduğunu ve bunları kendi özel projeleri bağlamında ne zaman ve nasıl uygulayacağına karar vermenin geliştiriciye bağlı olduğunu unutmamak önemlidir. Daha derinlemesine ve güncel makaleler ve eğitimler üzerine bulut bilişim ve DevOps, yazılım tasarımı ve geliştirme, takip edilmesi gereken teknoloji trendleri ve daha fazlası.
Keyifli Kodlamalar!
Yorumlar
Henüz yorum yapılmamış. İlk siz olun.