Software Design Principles (Part 4)
Herkese merhaba, bu yazı 4 bölümlük Software Design Principles serisinin son makalesidir. Daha öncekiler için sizi şöyle alalım:
Bu yazıda SOLID Design Prensipleri’nin L, I ve D harflerine denk gelen Liskov Substitution, Interface Segregation ve Dependency Inversion Prensiplerini inceliyor olacağız.
3. Liskov Substitution Principle
Barbara Liskov’un adını taşıyan bu prensip, türetilmiş bir sınıfın nesnelerinin taban sınıfın nesneleri yerine geçebilmesi gerektiğini, bunun da programın doğruluğunu etkilememesi gerektiğini belirtir. Bu, türetilmiş sınıfların, taban sınıfın beklenen davranışını koruyarak tasarlanmasının önemini vurgular.
Bunun ne demek olduğunu bir örnek üzerinde anlamaya çalışalım:
Bir ödeme işlemcisi sınıfının, güvenli bir ödeme sürecini sağlamak için SMS doğrulaması gerektirdiğini düşünelim. Ancak bir alt sınıf, bu doğrulama adımını atlayarak taban sınıfın beklenen davranışını bozabilir. Örneğin kredi kartı ile ödeme işlemi SMS doğrulaması gerektirirken Banka kartı ile ödeme işlemi SMS doğrulaması gerektirmesin.
İşte LSP’yi ihlal eden kod örneğimiz:
abstract class PaymentProcessor {
abstract fun processPayment(amount: Double, smsCode: String)
abstract fun generateInvoice(amount: Double): String
abstract fun sendNotification(message: String)
}
class CreditCardPaymentProcessor : PaymentProcessor() {
override fun processPayment(amount: Double, smsCode: String) {
if (smsCode.isEmpty()) {
throw IllegalArgumentException("SMS code is required for credit card payments")
}
if (!verifySms(smsCode)) {
throw SecurityException("Invalid SMS code")
}
println("Processing credit card payment of $$amount")
val invoice = generateInvoice(amount)
sendNotification("Payment processed. Invoice: $invoice")
}
private fun verifySms(smsCode: String): Boolean {
return smsCode == "123456"
}
override fun generateInvoice(amount: Double): String {
return "Invoice for $$amount"
}
override fun sendNotification(message: String) {
println("Notification sent: $message")
}
}
class BankTransferPaymentProcessor : PaymentProcessor() {
override fun processPayment(amount: Double, smsCode: String) {
println("Processing bank transfer of $$amount without SMS verification")
val invoice = generateInvoice(amount)
sendNotification("Bank transfer processed. Invoice: $invoice")
}
override fun generateInvoice(amount: Double): String {
return "Invoice for $$amount"
}
override fun sendNotification(message: String) {
println("Notification sent: $message")
}
}
fun testPaymentProcessor(processor: PaymentProcessor) {
try {
processor.processPayment(1000.0, "123456")
} catch (e: Exception) {
println("Test failed: ${e.message}")
}
}
fun main() {
val creditCardProcessor = CreditCardPaymentProcessor()
val bankTransferProcessor = BankTransferPaymentProcessor()
// CreditCardPaymentProcessor beklendiği gibi çalışır
testPaymentProcessor(creditCardProcessor)
// BankTransferPaymentProcessor, SMS doğrulama adımına gere duymadığından LSP ihlali olur
testPaymentProcessor(bankTransferProcessor)
}
Bu kodu aşağıdaki gibi düzenlersek LSP’ye uygun hale getirmiş oluruz:
abstract class PaymentProcessor {
abstract fun processPayment(amount: Double)
abstract fun generateInvoice(amount: Double): String
abstract fun sendNotification(message: String)
}
interface SmsVerifiable {
fun verifySms(smsCode: String): Boolean {
return smsCode == "123456"
}
}
class CreditCardPaymentProcessor(private val smsCode: String) : PaymentProcessor(), SmsVerifiable {
override fun processPayment(amount: Double) {
// SMS verification
if (!verifySms(smsCode)) {
throw SecurityException("Invalid SMS code")
}
println("Processing credit card payment of $$amount")
val invoice = generateInvoice(amount)
sendNotification("Payment processed. Invoice: $invoice")
}
override fun generateInvoice(amount: Double): String {
return "Invoice for $$amount"
}
override fun sendNotification(message: String) {
println("Notification sent: $message")
}
}
class BankTransferPaymentProcessor : PaymentProcessor() {
override fun processPayment(amount: Double) {
println("Processing bank transfer of $$amount without SMS verification")
val invoice = generateInvoice(amount)
sendNotification("Bank transfer processed. Invoice: $invoice")
}
override fun generateInvoice(amount: Double): String {
return "Invoice for $$amount"
}
override fun sendNotification(message: String) {
println("Notification sent: $message")
}
}
fun testPaymentProcessor(processor: PaymentProcessor) {
try {
processor.processPayment(1000.0) // SMS doğrulama gerekmez
} catch (e: Exception) {
println("Test failed: ${e.message}")
}
}
fun main() {
val creditCardProcessor = CreditCardPaymentProcessor("123456")
val bankTransferProcessor = BankTransferPaymentProcessor()
testPaymentProcessor(creditCardProcessor) // Kredi kartı ödeme işlemcisi SMS doğrulamasıyla çalışır
testPaymentProcessor(bankTransferProcessor) // Banka havalesi işlemcisi SMS doğrulama gerektirmez
}
SMS doğrulama işlemi sadece Kredi Kartı ile ödeme işleminde gerektiği için base classımızın processPayment metodu içerisinden sms kodu parametresini kaldırıyoruz. PaymentProcessor’ın sub classlarından olan BankTransferPaymentProcessor’da (Banka kartı ile ödeme) bu adıma ihtiyaç duymuyoruz. Dolayısıyla SMS doğrulama olayını CreditCardPaymentProcessor içerisinde ayrıca ele almak gerekiyor. Böylece base classımız sub classlarımızla değiştirilebilir oluyor ve base classın beklenen davranışını korumuş oluyoruz.
4. Interface Segregation Principle
Bu prensip, interfacelerin büyük ve monolitik olmaktan ziyade ince ayarlı ve spesifik işlevsellikleri temsil etmesi gerektiğini belirtir. Böylece, clientlar yalnızca ilgili interfacelere bağımlı olur, gereksiz bağımlılıklar azalır ve sürdürülebilirlik artar.
Bir araç bakım sistemi için bir interface oluşturduğunuzu hayal edin. Bu interface, birçok farklı bakım ve onarım görevini içeriyor olsun. Ancak, farklı türdeki araçların her biri bu interface’in yalnızca bir kısmını kullanır. Bu, interface ayrımı ilkesine uymayan bir durumdur, çünkü alt sınıflar kullanmadıkları işlevleri uygulamak zorunda kalır.
Aşağıdaki örnek kodumuzu inceleyelim:
interface VehicleMaintenance {
fun changeOil()
fun rotateTires()
fun replaceBrakes()
fun checkTransmission()
fun inspectExhaust()
fun tuneEngine()
}
class SportsCarMaintenance : VehicleMaintenance {
override fun changeOil() {
println("Changing oil for sports car")
}
override fun rotateTires() {
println("Rotating tires for sports car")
}
override fun replaceBrakes() {
println("Replacing brakes for sports car")
}
override fun checkTransmission() {
println("Checking transmission for sports car")
}
override fun inspectExhaust() {
println("Inspecting exhaust for sports car")
}
override fun tuneEngine() {
println("Tuning engine for sports car")
}
}
class BicycleMaintenance : VehicleMaintenance {
override fun changeOil() {
throw UnsupportedOperationException("Bicycles don't use oil")
}
override fun rotateTires() {
println("Rotating tires for bicycle")
}
override fun replaceBrakes() {
println("Replacing brakes for bicycle")
}
override fun checkTransmission() {
throw UnsupportedOperationException("Bicycles don't have transmissions")
}
override fun inspectExhaust() {
throw UnsupportedOperationException("Bicycles don't have exhaust systems")
}
override fun tuneEngine() {
throw UnsupportedOperationException("Bicycles don't have engines")
}
}
fun main() {
val sportsCar = SportsCarMaintenance()
sportsCar.changeOil() // Çalışır
val bicycle = BicycleMaintenance()
bicycle.changeOil() // UnsupportedOperationException
}
Bu örnekte, VehicleMaintenance arayüzü çok fazla işlevi içerir. Spor otomobiller bu işlevlerin çoğunu kullanabilir, ancak bisiklet bakımında birçok işlev gereksizdir. BicycleMaintenance sınıfı, UnsupportedOperationException atmak zorunda kalır, bu da ISP’nin ihlal edildiğini gösterir.
Kodu şimdi olması gerektiği gibi ISP uygun bir şekilde yazalım:
interface OilChangeable {
fun changeOil()
}
interface TireRotatable {
fun rotateTires()
}
interface BrakeReplaceable {
fun replaceBrakes()
}
class SportsCarMaintenance : OilChangeable, TireRotatable, BrakeReplaceable {
override fun changeOil() {
println("Changing oil for sports car")
}
override fun rotateTires() {
println("Rotating tires for sports car")
}
override fun replaceBrakes() {
println("Replacing brakes for sports car")
}
}
class BicycleMaintenance : TireRotatable, BrakeReplaceable {
override fun rotateTires() {
println("Rotating tires for bicycle")
}
override fun replaceBrakes() {
println("Replacing brakes for bicycle")
}
}
fun main() {
val sportsCar = SportsCarMaintenance()
sportsCar.changeOil()
val bicycle = BicycleMaintenance()
try {
// bicycle.changeOil() // Artık UnsupportedOperationException atmaz çünkü bu işlev gereksiz olduğundan dolayı kullanılmadı
} catch (e: Exception) {
println("Error: ${e.message}")
}
bicycle.rotateTires()
}
5. Dependency Inversion Principle
Bu prensip, üst düzey kodu alt düzey uygulama detaylarından ayırmak için soyutlamanın kullanılmasını teşvik eder. Bağımlılık yapısını tersine çevirerek, yazılım sistemleri daha modüler ve esnek hale gelir, bu da değişiklikleri ve test edilebilirliği kolaylaştırır.
Hemen bu prensibi örneklendirebileceğimiz bir senaryo düşünelim:
Bir e-ticaret uygulamasında, bir sipariş sınıfının doğrudan bir somut ödeme işlemcisine (örneğin, kredi kartı ödeme işlemcisi) bağımlı olduğunu düşünelim. Bu durum, ödeme işlemcisini değiştirmek istediğinizde tüm kodu güncelleme ihtiyacı doğurur ve test etme esnekliğini azaltır.
Hemen örnek kodumuzu inceleyelim:
// Düşük seviye modül: Kredi Kartı Ödeme İşlemcisi
class CreditCardPaymentProcessor {
fun processCreditCardPayment(amount: Double) {
println("Processing credit card payment of $$amount")
}
}
// Düşük seviye modül: PayPal Ödeme İşlemcisi
class PayPalPaymentProcessor {
fun processPayPalPayment(amount: Double) {
println("Processing payment of $$amount using PayPal")
}
}
// Yüksek seviye modül: Sipariş İşlemcisi
class OrderProcessor {
private val creditCardPaymentProcessor = CreditCardPaymentProcessor()
private val payPalPaymentProcessor = PayPalPaymentProcessor()
fun processOrder(order: Order, paymentType: String) {
// Yüksek seviye iş mantığı
println("Processing order #${order.id}")
// Ödemeyi işleme
when (paymentType) {
"CreditCard" -> creditCardPaymentProcessor.processCreditCardPayment(order.total)
"PayPal" -> payPalPaymentProcessor.processPayPalPayment(order.total)
else -> println("Unsupported payment method")
}
// Diğer iş mantığı
println("Order #${order.id} processed")
}
}
data class Order(val id: Int, val total: Double)
fun main() {
val order = Order(1, 150.0)
val orderProcessor = OrderProcessor()
orderProcessor.processOrder(order, "CreditCard")
orderProcessor.processOrder(order, "PayPal")
}
Bu örnekte, Order sınıfı, doğrudan CreditCardPaymentProcessor somut sınıfına bağımlıdır. Bu bağımlılık nedeniyle, farklı bir ödeme işlemcisine geçmek veya test esnasında processorı değiştirmek zor olur. Ayrıca, bu bağımlılık, Order sınıfını daha esnek hale getirecek veya gelecekteki değişiklikleri kolaylaştıracak soyutlamaları kullanmayı engeller.
O zaman kodumuzu DI’ya uygun hale getirelim. Yani concrete classlara depend olmak yerine abstract yapılar kullanalım:
// Yüksek seviye modül: Sipariş İşlemcisi
class OrderProcessor(private val paymentProcessor: PaymentProcessor) {
fun processOrder(order: Order) {
// Yüksek seviye iş mantığı
println("Processing order #${order.id}")
// Ödemeyi işleme
paymentProcessor.processPayment(order.total)
// Diğer iş mantığı
println("Order #${order.id} processed")
}
}
interface PaymentProcessor {
fun processPayment(amount: Double)
}
// Düşük seviye modül: Kredi Kartı Ödeme İşlemcisi
class CreditCardPaymentProcessor : PaymentProcessor {
override fun processPayment(amount: Double) {
println("Processing credit card payment of $$amount")
}
}
// Düşük seviye modül: PayPal Ödeme İşlemcisi
class PayPalPaymentProcessor : PaymentProcessor {
override fun processPayment(amount: Double) {
println("Processing payment of $$amount using PayPal")
}
}
data class Order(val id: Int, val total: Double)
fun main() {
val order = Order(1, 150.0)
// Yüksek seviye modül, düşük seviye detaylara bağımlı değil, abstractiona bağımlı
val orderProcessor1 = OrderProcessor(CreditCardPaymentProcessor())
orderProcessor1.processOrder(order)
val orderProcessor2 = OrderProcessor(PayPalPaymentProcessor())
orderProcessor2.processOrder(order)
}
Bu örnekte, PaymentProcessor adlı bir arayüz tanımlanmıştır. CreditCardPaymentProcessor ve PayPalPaymentProcessor, bu arayüzü uygular, böylece farklı ödeme yöntemleriyle çalışmak için aynı soyutlama kullanılır. Order sınıfı, artık somut bir sınıfa bağımlı değil, soyutlamaya bağımlıdır. Bu sayede, farklı ödeme işlemcilerini kullanarak sipariş işlemi yapılabilir.
Evet, 4 yazıdan oluşan Software Design Principles serisinin sonuna geldik. Olabildiğince somut senaryolar üreterek soyut kavramların daha anlaşılabilir olmasını amaçladım :) Umarım faydalı olmuştur.
Kodla kalın..