Merhabalar, daha önce iyi bir yazılım geliştirmek için Clean Code Prensiplerinden bahsetmiştim bu yazıda ise genellikle OOP’de sıklıkla duyulan bir terim olan SOLID’i ele alacağız.

SOLID prensiplerini anlatmadan önce neden kullanmamız gerektiğini açıklarsak: SOLID prensiplerini kullanmanın temel nedeni, yazılımı daha esnek, sürdürülebilir, test edilebilir ve bakımı kolay hale getirmektir. Bu prensipler, kodun uzun vadede daha az hata üretmesini ve yeni özellikler eklerken mevcut yapının bozulmasını önlemeye yardımcı olur.
SOLID, yazılım geliştirmede iyi tasarım ve sürdürülebilirlik için beş temel prensibi kapsayan bir kısaltmadır. Açılımı şu şekildedir:
S – Single Responsibility Principle
O – Open/Closed Principle
L – Liskov Substitution Principle
I – Interface Segregation Principle
D – Dependency Inversibler Principle
SOLID prensiplerinin temel amacını ve açılımını tanımladıktan sonra hızlıca her başlığı tanımlayalım ve python örnekleri ile nasıl kullanmamız gerektiğini daha iyi anlayalım:
1 – Single Responsibility Principle (Tek Sorumluluk Prensibi)
Bir sınıfın değişmesi için tek bir nedeni olmalıdır. Başka bir deyişle, bir sınıfın iyi tanımlanmış tek bir sorumluluğu olmalıdır. Bu sınıf sorumluluğu tamamen kapsüllemelidir.
Bir sınıfın yalnızca tek bir sorumluluğu olmalıdır. Eğer bu sınıfın birden fazla sorumluluğu olursa zamanla daha karmaşık hale gelir ve kodun geliştirilmesi, bakımı ve yeni özelliklerin eklenmesi zorlaşır.
Sınıfın sahip olduğu metodlar sınıfla uyumlu olmalıdır, örneğin online yemek siparişi uygulmamız olsun, sırasıyla sipariş-ödeme-fatura adımlarından geçerek sipariş oluşturulacak. Örneğimizi kod üzerinden açıklayarak daha iyi kavrayalım:
Öncelikle kötü örnekle başlayalım:
class Order:
def __init__(self, items: list, total: float):
self.items = items
self.total = total
def process_payment(self, payment_method: str):
if payment_method == "credit_card":
print(f"Processing credit card payment for ${self.total}")
elif payment_method == "paypal":
print(f"Processing PayPal payment for ${self.total}")
def generate_invoice(self):
print("Generating invoice...")
print(f"Items: {self.items}")
print(f"Total: {self.total}")
Örneğe baktığımızda temiz bir kodlama mevcut ancak dikkat edilmesi gereken ve ilk prensibimizin amacı olan konuya gelirsek; Burada Order sınıfının 3 farklı görevle sorumlu olduğunu görüyoruz: Sipariş bilgileri, ödeme ve fatura.
Eğer ileride kdv oranını eklemek, ürünlerin stoklarını kontrol etmek, indirim yapmak gibi yeni fonksiyonlar tanımlamak istersek sınıfımız karmaşıklaşacak ve metodların birbirlerine olan bağımlılıkları artacaktır. Dolayısıyla Sipariş, Ödeme ve Faturlandırma gibi 3 farklı sorumluluğu birbirinden ayırmamız gerekir:
class Order:
def __init__(self, items: list, total: float):
self.items = items
self.total = total
class PaymentProcessor:
def process_payment(self, order: Order, payment_method: str):
if payment_method == "credit_card":
print(f"Processing credit card payment for ${order.total}")
elif payment_method == "paypal":
print(f"Processing PayPal payment for ${order.total}")
class InvoiceGenerator:
def generate_invoice(self, order: Order):
print("Generating invoice...")
print(f"Items: {order.items}")
print(f"Total: {order.total}")
# Kullanım
order = Order(items=["item1", "item2", "item3"], total=100.0)
payment_processor = PaymentProcessor()
invoice_generator = InvoiceGenerator()
# Sipariş işleme
payment_processor.process_payment(order, payment_method="credit_card")
invoice_generator.generate_invoice(order)
2 – Open/Closed Principle (Açık/Kapalı Prensibi)
Nesneler veya varlıklar genişletmeye açık ancak değiştirmeye kapalı olmalıdır.
Bir sınıfın mevcut kodunu değiştirmeden yeni özellikler eklemeliyiz. Bunun nedeni, mevcut kod üzerinde değişiklikler yaptığımızda, yeni hataların ortaya çıkma ihtimalidir. Bu yüzden test edilmiş ve güvenilir olan kodu mümkünse değiştirmekten kaçınmalıyız.
Ancak sınıfa dokunmadan nasıl yeni işlevler ekleyeceğiz diye sorabilirsiniz. Bu genellikle arayüzler ve soyut sınıflar yardımıyla yapılır.
Amaç
- Açık (Open): Bir sınıf yeni davranışlar ve işlevler eklemek için genişletilebilir olmalıdır.
- Kapalı (Closed): Mevcut sınıfların davranışları değiştirilmemelidir. Yani mevcut kod, değişikliğe kapalı olmalıdır.
Kötü Örnek:
Aşağıdaki örnekte, bir Discount sınıfı farklı indirim türlerini uygulamak için yazılmış. Ancak her yeni indirim türü eklendiğinde, bu sınıfı değiştirmemiz gerekiyor. Bu, Open/Closed prensibine aykırıdır çünkü mevcut kodu değiştirmek zorunda kalıyoruz.
class Discount:
def __init__(self, price: float):
self.price = price
def apply_discount(self, discount_type: str):
if discount_type == "seasonal":
return self.price * 0.9 # %10 indirim
elif discount_type == "holiday":
return self.price * 0.8 # %20 indirim
else:
return self.price
Burada, her yeni indirim türü eklendiğinde apply_discount metodunu değiştirmemiz gerekiyor. Bu, sınıfı değiştirmeye sürekli açık hale getiriyor, ki bu Open/Closed prensibine aykırı.
Yeni indirim türleri eklerken mevcut kodu değiştirmek zorunda kalmadan kodumuzu genişletebilmeliyiz. Farklı indirim türleri için ayrı sınıflar oluşturarak ve bir temel sınıf üzerinden bu sınıfları genişleterek yapabiliriz.
from abc import ABC, abstractmethod
class Discount(ABC):
def __init__(self, price: float):
self.price = price
@abstractmethod
def apply_discount(self):
pass
class SeasonalDiscount(Discount):
def apply_discount(self):
return self.price * 0.9 # %10 indirim
class HolidayDiscount(Discount):
def apply_discount(self):
return self.price * 0.8 # %20 indirim
class NoDiscount(Discount):
def apply_discount(self):
return self.price
# Kullanım
order_price = 100.0
seasonal_discount = SeasonalDiscount(order_price)
print(f"Seasonal Discounted Price: {seasonal_discount.apply_discount()}")
holiday_discount = HolidayDiscount(order_price)
print(f"Holiday Discounted Price: {holiday_discount.apply_discount()}")
no_discount = NoDiscount(order_price)
print(f"No Discount Price: {no_discount.apply_discount()}")
3 – Liskov Substitution Principle (Liskov Yerine Geçme Prensibi)
A sınıfı B sınıfının bir alt türü ise, programımızın davranışını bozmadan B’yi A ile değiştirebilmeliyiz.
Bu prensip, alt sınıfların (subclasses), temel sınıfın (base class) yerini sorunsuz bir şekilde alabilmesi gerektiğini belirtir. Yani, bir alt sınıf temel sınıfın tüm işlevselliğini düzgün bir şekilde yerine getirmelidir
- Eğer bir
Bsınıfı,Asınıfını miras alıyorsa,Asınıfının kullanıldığı her yerdeBsınıfı da kullanılabilir olmalıdır. - Alt sınıflar, üst sınıfların kontratını bozmamalıdır. Yani, üst sınıfta beklenen davranışlar, alt sınıfta da bozulmadan çalışmalıdır.
Kötü Örnek:
Bu örnekte Bird sınıfı uçabilen kuşları temsil ederken, Penguin sınıfı Bird sınıfını miras almıştır. Ancak penguenler uçamaz, bu da fly metodunun işlevselliğini bozar.
class Bird:
def fly(self):
print("Flying in the sky!")
class Penguin(Bird):
def fly(self):
raise Exception("Penguins can't fly!")
# Kullanım
def make_bird_fly(bird: Bird):
bird.fly()
sparrow = Bird()
penguin = Penguin()
make_bird_fly(sparrow) # Bu normal çalışır
make_bird_fly(penguin) # Bu hata verir çünkü penguenler uçamaz!
Bu sorunu çözmek için uçabilen ve uçamayan kuşları ayrı ayrı sınıflandırmamız gerekiyor. Bird sınıfı genel bir kuş sınıfı olurken, FlyableBird sınıfı sadece uçabilen kuşlar için kullanılacak.
from abc import ABC, abstractmethod
class Bird(ABC):
@abstractmethod
def make_sound(self):
pass
class FlyableBird(Bird):
@abstractmethod
def fly(self):
pass
class Sparrow(FlyableBird):
def fly(self):
print("Sparrow is flying!")
def make_sound(self):
print("Chirp chirp!")
class Penguin(Bird):
def make_sound(self):
print("Penguin is squawking!")
# Kullanım
def make_bird_fly(bird: FlyableBird):
bird.fly()
sparrow = Sparrow()
penguin = Penguin()
make_bird_fly(sparrow) # Bu normal çalışır
# make_bird_fly(penguin) # Bu hata verir çünkü penguenler uçmaz!
Daha Esnek ve Anlamlı Yapı: Uçabilen ve uçamayan kuşlar arasında net bir ayrım yapılmıştır. Bu, hem daha esnek hem de daha anlamlı bir yapı sağlar.
4 – Interface Segregation Principle (Arayüz Ayırma Prensibi)
Bir sınıf, kullanmadığı metotları içeren arayüzleri uygulamaya zorlanmamalıdır
Amaç
- Küçük ve Amaca Yönelik Arayüzler: Bir sınıfın gereksiz ve kullanmadığı işlevleri içermesi kötü bir tasarımdır. Bu nedenle büyük arayüzler küçük ve belirli işlevleri olan arayüzlere bölünmelidir.
- Gereksiz Bağımlılıklar: Sınıflar sadece ihtiyaç duydukları işlevlere bağımlı olmalıdır. Gereksiz metotları içeren geniş arayüzler, sınıflara gereksiz bağımlılıklar yükler.
Kötü Örnek:
Bu örnekte, Worker adında sınıfımız var ve bu sınıf birçok metodu içeriyor. Ancak her işçi her işlevi yerine getiremez. Örneğin, bir robotun yemek yeme özelliği yoktur ama Worker sınıfını uygularken bu özelliği tanımlamak zorunda kalır.
class Worker:
def work(self):
pass
def eat(self):
pass
class HumanWorker(Worker):
def work(self):
print("Human working")
def eat(self):
print("Human eating")
class RobotWorker(Worker):
def work(self):
print("Robot working")
def eat(self):
raise Exception("Robots don't eat!") # Gereksiz metod!
# Kullanım
human = HumanWorker()
robot = RobotWorker()
human.work() # İnsan çalışıyor
human.eat() # İnsan yemek yiyor
robot.work() # Robot çalışıyor
robot.eat() # Exception: Robots don't eat!
RobotWorker sınıfı Worker sınıfını uyguladığı için, robotun yemek yeme özelliğini de tanımlamak zorunda kalıyoruz. Bu sınıfın gereksiz işlevler içerdiğini gösterir.
Bu sorunu çözmek için arayüzleri, her sınıfın sadece ihtiyaç duyduğu işlevleri içerecek şekilde bölmeliyiz. İşçilere yönelik Workable ve yemek yiyenlere yönelik Eatable gibi ayrı arayüzler oluşturabiliriz.
from abc import ABC, abstractmethod
# İşlevsel küçük arayüzler
class Workable(ABC):
@abstractmethod
def work(self):
pass
class Eatable(ABC):
@abstractmethod
def eat(self):
pass
# İnsan çalışan, hem çalışabilir hem yemek yiyebilir
class HumanWorker(Workable, Eatable):
def work(self):
print("Human working")
def eat(self):
print("Human eating")
# Robot sadece çalışabilir, yemek yiyemez
class RobotWorker(Workable):
def work(self):
print("Robot working")
# Kullanım
human = HumanWorker()
robot = RobotWorker()
human.work() # İnsan çalışıyor
human.eat() # İnsan yemek yiyor
robot.work() # Robot çalışıyor
# robot.eat() # Bu hata vermez çünkü RobotWorker sınıfı eat() metoduna ihtiyaç duymaz.
5 – Dependency Inversion Principle (Bağımlılıkları Tersine Çevirme Prensibi)
Yüksek seviyeli modüller düşük seviyeli modüllere bağımlı olmamalıdır
Alt seviye modüllerde yapılan değişiklikler, üst seviye modülleri etkilememelidir.
Kötü Örnek:
Aşağıdaki örnekte, PaymentService sınıfı doğrudan CreditCardPayment sınıfına bağımlıdır. Bu bağımlılık, başka bir ödeme yöntemi eklemek istediğimizde PaymentService sınıfının değişmesine neden olur, yani sistem esnek değildir.
class CreditCardPayment:
def process_payment(self, amount):
print(f"Processing credit card payment of {amount}")
class PaymentService:
def __init__(self):
self.payment_method = CreditCardPayment()
def make_payment(self, amount):
self.payment_method.process_payment(amount)
# Kullanım
payment_service = PaymentService()
payment_service.make_payment(100)
Problem
PaymentServicedoğrudanCreditCardPaymentsınıfına bağımlıdır. Eğer yeni bir ödeme yöntemi eklemek istersek,PaymentService‘i değiştirmemiz gerekir. Bu bağımlılık, esnekliği ve genişletilebilirliği sınırlar.
Bu sorunu çözmek için, soyut bir arayüz (interface) tanımlarız ve ödeme yöntemleri bu arayüzü uygular. PaymentService sınıfı artık spesifik bir ödeme yöntemi sınıfına değil, bu arayüze bağımlı olur. Yeni bir ödeme yöntemi eklerken PaymentService sınıfını değiştirmemiz gerekmez, sadece yeni bir sınıf oluştururuz.
from abc import ABC, abstractmethod
# Soyutlama (Arayüz)
class PaymentMethod(ABC):
@abstractmethod
def process_payment(self, amount):
pass
# Kredi Kartı Ödeme Yöntemi
class CreditCardPayment(PaymentMethod):
def process_payment(self, amount):
print(f"Processing credit card payment of {amount}")
# PayPal Ödeme Yöntemi
class PayPalPayment(PaymentMethod):
def process_payment(self, amount):
print(f"Processing PayPal payment of {amount}")
# Üst Seviye Servis (Soyutlamaya Bağımlı)
class PaymentService:
def __init__(self, payment_method: PaymentMethod):
self.payment_method = payment_method
def make_payment(self, amount):
self.payment_method.process_payment(amount)
# Kullanım
credit_card_payment = CreditCardPayment()
paypal_payment = PayPalPayment()
payment_service = PaymentService(credit_card_payment)
payment_service.make_payment(100) # Kredi kartıyla ödeme yapar
payment_service = PaymentService(paypal_payment)
payment_service.make_payment(200) # PayPal ile ödeme yapar
Kaynaklar:
- https://www.digitalocean.com/community/conceptual-articles/s-o-l-i-d-the-first-five-principles-of-object-oriented-design#open-closed-principle
- https://medium.com/@saygiligozde/applying-solid-principles-to-spring-boot-applications-191d7e50e1b3
- https://www.freecodecamp.org/news/solid-principles-explained-in-plain-english/




Yorum bırakın