solid etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster
solid etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster

13 Ocak 2023 Cuma

SOLID prensiplerinden Open-Closed prensibinin python kodları eşliğinden yakından incelenmesi

Merhaba, 

Bugünkü yazımızda SOLID prensiplerinden 'Open-Closed Rrensibi' ni inceleyeceğiz.


Temsil edilen O harfi "Open-Closed Principle" (Açık-Kapalı Prensibi) anlamına gelir. Bu prensip, bir sınıfın veya modülün geliştirilmeye açık olmasının yanı sıra, uzantılar yapılarak kapatılması gerektiğini ifade eder. Bu, sınıfın veya modülün işlevselliğini genişletmek için değiştirilmemesi, ancak ekstra işlevsellik eklemek için uzantılar yapılması gerektiğini ifade eder.

Open-Closed Principle kullanmak, yazılımın daha esnek, daha test edilebilir ve daha az hata içerme olasılığına sahip olmasını sağlar. Özellikle, bu prensip ile yazılım kodunun değiştirilmesi gerektiğinde, mevcut kodun değiştirilmemesi ve yerine yeni kodun eklenmesiyle sorunlar çözülebilir. Bu, yazılımın daha uzun bir ömür yaşamasına ve daha az bakım gerektirmesine olanak tanır. Ayrıca, Open-Closed Principle kullanmak, yazılımın daha kolay test edilebilir olmasını sağlar çünkü sınıflar ve modüller arasındaki bağımlılıklar azaltılmış olur.

Open-Closed Principle kullanmazsanız, yazılım kodunuzun daha az esnek ve daha zor test edilebilir olmasına neden olabilir. Kodunuzun işlevselliğini genişletmek istediğinizde, mevcut kodun değiştirilmesi gerekebilir ve bu değişiklikler, başka yerlerde beklenmedik hatalara neden olabilir. Ayrıca, mevcut kodun değiştirilmesi, yazılımın daha hızlı çürümesine ve daha fazla bakım gerektirmesine neden olabilir.

Bunu bir python kodu ile açıklayalım :

 

class Rectangle: def __init__(self, width, height): self._width = width self._height = height def get_area(self): return self._width * self._height class Square(Rectangle): def __init__(self, side): super().__init__(side, side) class Circle: def __init__(self, radius): self._radius = radius def get_area(self): return 3.14 * (self._radius ** 2) class AreaCalculator: def __init__(self, shapes): self._shapes = shapes def get_total_area(self): total_area = 0 for shape in self._shapes: total_area += shape.get_area() return total_area


Burada, Rectangle, Square ve Circle sınıfları, get_area metodunu kullanarak alanlarını hesaplamak için tasarlanmıştır. AreaCalculator sınıfı ise, verilen şekillerin alanlarını toplamak için kullanılır. Bu örnekte, Rectangle ve Circle sınıfları "Open-Closed Principle" prensibine uymaktadır. Çünkü, bu sınıfların işlevselliği genişletilmek istendiğinde değiştirilmemektedir, ancak yeni sınıflar oluşturulmaktadır (örneğin Square sınıfı) ve bu sınıflar, genişletilmiş işlevselliği sağlamaktadır.

Bu örnekte, AreaCalculator sınıfı, verilen şekillerin alanlarını toplamak için kullanılmaktadır. Bu, AreaCalculator sınıfının, verilen şekillerin türlerinden bağımsız olarak çalışmasını sağlar. Bu, AreaCalculator sınıfının daha esnek ve daha az hata içerme olasılığına sahip olmasını sağlar.

Şimdi de bu prensibin ihlal edildiği bir örnek oluşturalım.

 

class Rectangle: def __init__(self, width, height): self._width = width self._height = height def get_area(self): return self._width * self._height class Square: def __init__(self, side): self._side = side def get_area(self): return self._side * self._side class Circle: def __init__(self, radius): self._radius = radius def get_area(self): return 3.14 * (self._radius ** 2) class AreaCalculator: def __init__(self, shapes): self._shapes = shapes def get_total_area(self): total_area = 0 for shape in self._shapes: if isinstance(shape, Rectangle) or isinstance(shape, Square): total_area += shape.get_area() return total_area


Burada, Rectangle, Square ve Circle sınıfları, get_area metodunu kullanarak alanlarını hesaplamak için tasarlanmıştır. AreaCalculator sınıfı ise, verilen şekillerin alanlarını toplamak için kullanılır. Bu örnekte, Rectangle ve Square sınıfları "Open-Closed Principle" prensibini ihlal etmektedir. Çünkü, AreaCalculator sınıfı, Rectangle ve Square sınıflarının alanlarını toplamak için kodunu değiştirmiştir. Bu değişiklikler, AreaCalculator sınıfının başka yerlerde beklenmedik hatalara neden olmasına olanak tanır.

Ayrıca, AreaCalculator sınıfı, Circle sınıfını dahil etmemektedir. Bu, AreaCalculator sınıfının daha az esnek ve daha zor test edilebilir olmasına neden olur.

Biraz daha detaya inersek :

 
def get_total_area(self): total_area = 0 for shape in self._shapes: if isinstance(shape, Rectangle) or isinstance(shape, Square): total_area += shape.get_area() return total_area

Bu kod bloğunda, AreaCalculator sınıfı, verilen şekillerin her birisini döngü içinde kontrol eder. Eğer şekil bir Rectangle sınıfı veya Square sınıfının bir örneği ise, şeklin alanını toplamaya ekler. Bu, AreaCalculator sınıfının sadece Rectangle ve Square sınıflarının alanlarını toplamasını sağlar.

Bu örnekte, AreaCalculator sınıfı, Rectangle ve Square sınıflarının alanlarını toplamak için kodunu değiştirmiştir. Bu değişiklikler, AreaCalculator sınıfının, daha sonra eklenen yeni şekil türlerini dahil etmemesi sonucunu doğurur. Örneğin, Circle sınıfının alanını toplamak istediğinizde, AreaCalculator sınıfının kodunu değiştirmek zorunda kalırsınız. Bu, AreaCalculator sınıfının daha az esnek ve daha zor test edilebilir olmasına neden olur.

Bu nedenle, Open-Closed Principle kullanmak, yazılımın daha esnek, daha test edilebilir ve daha az hata içerme olasılığına sahip olmasını sağlar.





Aşağıda bir başka kod parçacığında ihlalin nasıl olduğunu tekrar görelim.
 
class Shape: def __init__(self, type): self.type = type def draw(self): if self.type == "circle": # draw circle elif self.type == "square": # draw square # ...

Bu kod parçası, "open-close" prensibini ihlal eder çünkü "Shape" sınıfı, eklenen yeni bir şekil için her seferinde değiştirilmelidir. Örneğin, eğer yeni bir şekil tipi olarak "dikdörtgen" eklenirse, "Shape" sınıfının "draw" metodu değiştirilmelidir. Bu, "Shape" sınıfının açık olduğu anlamına gelir, ancak her yeni şekil için değiştirilmesi gerektiğinden kapalı değildir. Bu nedenle, "Shape" sınıfının değiştirilmesine gerek kalmadan yeni şekil tipleri eklenmelidir.

Teşekkürler
Cem Selmanoğulları

27 Ocak 2022 Perşembe

Dependency Injection ve Dependency Inversion - PYTHON

erhaba,

Bugünkü yazımda Dependency Injection ve Dependency Inversion konularını inceleyip python ile örnek kodlar yazmaya çalışacağım.

Birçok kaynakta karışık bir şekilde anlatılan bu yapıları bakalım herkesin anlayabileceği bir şekle sokabilecek miyiz...

Dependency Inversion konsepti yazılımda S.O.L.I.D prensiplerinde tanıtılmaktadır ve bu kelimenin son harfi olan D ile gösterilmektedir. Dependency Injection ise Dependency Inversion uygulanma tekniklerinden biri olarak tanımlanabilir. Amaç diğer sınıflara bağımlılığı en aza indirecek bir yapı kurmaktır.



Bir memeli sınıfımız olsun. Bu sınıf içinde başka bir hayvan sınıfını kullanıyorsak, burda bir bağımlıktan söz etmek mümkündür. Yani memeli sınıfı hayvan sınıfına bağımlıdır. Bu bağımlılığı azaltabilmek için öncelikle dependency injection uygulanması gerekir. Alttaki kodlarımızda kısaca şunu yapacağız. Hayvan sınıfını da kullanarak bir memeli oluşturacağız. Memeli sınıfından oluşturulan nesne ye metodu aracılığı ile komut verip yürüteceğiz ve durduracağız.

class hayvan():
def yuru(self):
print("Yuru...Yuruyor.")

def dur(self):
print("Dur!...Durdu.")

class memeli():
def __init__(self):
self.hayvan = hayvan()
self.durum = False

def komut_ver(self):
if self.durum:
self.hayvan.yuru()
self.durum = False
else:
self.hayvan.dur()
self.durum = True

kedi = memeli()
kedi.komut_ver()
kedi.komut_ver()
kedi.komut_ver()
kedi.komut_ver()
kedi.komut_ver()
Çıktı :

Dur!...Durdu.
Yuru...Yuruyor.
Dur!...Durdu.
Yuru...Yuruyor.
Dur!...Durdu.

Dependency Injection bağımlılıkların dışardan alınması şeklinde sağlanmaktadır. Şimdi dependency injection ile üstteki kodumuzu alttaki şekilde düzenleyelim.

 
class hayvan():
def yuru(self):
print("Yuru...Yuruyor.")

def dur(self):
print("Dur!...Durdu.")

class memeli():
def __init__(self, memeliHayvan: hayvan):
self.memeliHayvan = memeliHayvan
self.durum = False

def komut_ver(self):
if self.durum:
self.memeliHayvan.yuru()
self.durum = False
else:
self.memeliHayvan.dur()
self.durum = True

kedi = hayvan()
sonic = memeli(kedi)

sonic.komut_ver()
sonic.komut_ver()
sonic.komut_ver()
sonic.komut_ver()
sonic.komut_ver()
Çıktı :

Dur!...Durdu.
Yuru...Yuruyor.
Dur!...Durdu.
Yuru...Yuruyor.
Dur!...Durdu.

Kodun yeni halini incelersek hayvan sınıfından üretilen nesne memeli sınıfına dışarıdan verilmektedir. Yani memeli sınıfı içinde hayvan sınıfı kullanılarak bir nesne üretimi yapılmamaktadır. Bu bağımlılığı dependency injection ile kaldırmış olduk.

Peki işi biraz ileriye götürürsek, memeli sınıfına gönderilecek hayvanları iki ayaklı ve dört ayaklı olarak ikiye ayırıp bunlar için birer ara sınıf oluşturmak istersek bunu nasıl yöneteceğiz. Burada iki ekstra sınıftan bahsediyoruz ama onlarca sınıf olabilir. Peki iki ayaklı ve dört ayaklı şeklinde ayarlayacağımız iki sınıftan üretilen hayvanları memeli sınıfına gönderebilir miyiz ? Yukarıdaki örnekte hayvan sınıfından üretilen hayvan nesnelerini memeli sınıfına gönderecek şekilde ayarlamalarımızı yaptık. Peki iki ayaklı hayvan nesnesi ve dört ayaklı hayvan sınıf nesnelerini memeli sınıfına nasıl gönderebiliriz ? Bunu direkt olarak yapmamız mümkün değildir, yani bu nesneler memeli sınıfı tarafından algılanamayacaktır. Çünkü memeli sınıfı hayvan sınıfı nesnelerini algılamak üzere geliştirildi.

Bunu interface kullanımı yardımı ile çözeriz. iki ayaklı hayvan ve dört ayaklı hayvan sınıfları bu interface'den implement alırsa sorunumuz çözülecektir. Bu durumda hayvan sınıfımız bir interface olacak. Memeli sınıfı hayvan sınıfından nesneleri kabul edecek şekilde ayarlanacak. İki ayaklı hayvan ve dört ayaklı hayvan sınıfları bu interface'den implement alarak bunlardan üretilen nesneler memeli sınıfına gönderilebilecek.

Şimdi bu senaryoya göre dependency inversion uygulayalım.

Tekrar özetlersek. Hayvan isimli bir interface olacak. İki ayaklı hayvan ve Dört ayaklı hayvan isimli birer sınıf bu interfaceden implement alacak ve bu sınıflardan üretilecek nesneler memeli sınıfına gönderilerek memeli metodu olan komut_ver() çağırılacak.


from abc import ABC, abstractmethod

class hayvan(ABC):
@abstractmethod
def yuru(self):
pass

@abstractmethod
def dur(self):
pass

class iki_ayakli_hayvan(hayvan):
def yuru(self):
print("İki Ayaklı Yuru...Yuruyor.")

def dur(self):
print("İki Ayaklı Dur!...Durdu.")

class dort_ayakli_hayvan(hayvan):
def yuru(self):
print("Dort Ayaklı Yuru...Yuruyor.")

def dur(self):
print("Dort Ayaklı Dur!...Durdu.")

class memeli():
def __init__(self, memeliHayvan: hayvan):
self.memeliHayvan = memeliHayvan
self.durum = False

def komut_ver(self):
if self.durum:
self.memeliHayvan.yuru()
self.durum = False
else:
self.memeliHayvan.dur()
self.durum = True

kedi = dort_ayakli_hayvan()
sonic = memeli(kedi)

maymun = iki_ayakli_hayvan()
jimny = memeli(maymun)

jimny.komut_ver()
jimny.komut_ver()
jimny.komut_ver()
print()
sonic.komut_ver()
sonic.komut_ver()
sonic.komut_ver()
Çıktımız :

İki Ayaklı Dur!...Durdu.
İki Ayaklı Yuru...Yuruyor.
İki Ayaklı Dur!...Durdu.

Dort Ayaklı Dur!...Durdu.
Dort Ayaklı Yuru...Yuruyor.
Dort Ayaklı Dur!...Durdu.

Evet böylece ilk kodumuza önce dependency injection uygulayarak sınıf bağımlılığından kurtardık. Sonrasında ise daha detaylı bir sınıf yapısı kurarak sınıf çeşitliliğimizi arttırma amacı ile kodumuzu bir interface den implement ederek inşaa ettik. Bu sayede dependency inversion uygulamış olduk. Bunun bir avantajı da artık aynı interfaceden implement eden yeni farklı ara sınıfları kodumuza eklemek ve memeli sınıfına göndermek sureti ile yeni tür nesneler yaratabileceğiz. Örneğin kırkayak adında üçüncü bir sınıfı hızlıca kodumuza ekleyebiliriz. Hemde interface ve memeli sınıflarını değiştirmeden.

Bunu farklı bir örnek tasarımı ile açıklamaya çalışalım. Bir adet Tır sınıfımız olsun, bir adet Uçak sınıfımız olsun, bir adet Otobüs sınıfımız olsun. Bir kargo lojistik firması için yazılım geliştiriyor olalım. Bu 3 sınıfı bir adet Operasyon isimli interface sınıfından implement ederiz. Bu Operasyon sınıfının Taşı isimli bir metodu olacaktır. Dolayısı ile Uçak, Otobüs, Tır sınıflarının da Taşı isimli metodu olacaktır. Bu 3 sınıftan üretilen nesneleri ana sınıfımız olan Kargo sınıfına göndereceğiz.
Kısaca  Operasyon()  ->  ( Uçak() , Otobüs() , Tır() )  ->  Kargo() şeklinde bir yapı olacaktır.
Görebileceğiniz üzere böyle bir yapısı olan kargo firması ilerleyen yıllarda bir gemi alıp gemi kargosu işlerine girerse Gemi() sınıfını sisteme dahil etmek oldukça kolay olacaktır ve bu ekleme diğer sınıfları etkilemeyecektir.

Teşekkürler,
Cem Selmanoğulları