Python üzerinde Design Patterns (Tasarım Desenleri) örnekleri
Bu blog post'unda kendi projelerimde ya da çalıştığım şirkette development sürecinde kullandığımız design pattern'ları ve bu konudaki python'un avantajlarına değineceğim.
Design Patterns
Bu kavramın henüz türkçesi bulunmadı :) Genelde tasarım örüntüleri ya da tasarım desenleri denmekte.
Design pattern'lar development sürecinde sıkça rastlanan problemleri çözmek için kullanılan desenlerdir. Bunların bir -desen- olmasının sebebi problemi çözmekten ziyade probleme object-oriented'ın temel felsefelerinden olan reusable (yeniden kullanılabilir) çözümler getirmektir.
Gang of Four (dörtlü çete) olarak bilinen 4 kafadar 1994 yılında Design Patterns: Elements of Reusable Object-Oriented Software isimli kitaplarında 3 farklı kategoride 23 farklı tasarım deseni derlemiştir. Bu kategoriler;
Creational patterns: Objelerin oluşturulması ile ilgili desenlerdir
Structural patterns: Sınıflar arasındaki bağlantıları gevşek tutmak ve projenin genişletebilmesini sağlayan desenlerdir.
Behavioral patterns: Sınıflar ya da objeler arasında composition ya da inheritance kullanılarak iletişim kurmak için kullanılan desenlerdir.
Desen örnekleri vermeden design pattern ve object-oriented prensiplerine göre bir kaç şeyden bahsetmek istiyorum.
Design pattern'lar ezberlenmemelidir. Bütün desenleri inceledikten sonra zaten ortaya çıkan problem sizi design pattern kullanmaya itecektir.
Design pattern'ların kullanımı abartılmamalıdır. Problem sizi zaten gerekli yerlerde design pattern kullanmaya itecektir. Zira çözümünüz bir anti-pattern'e dönüşebilir.
Kullandığınız platformu ya da programlama dilininin getirtiği avantajları tam anlamıyla bilmeden bir deseni uygulamamak gerekir.
Her sınıfın sadece tek bir sorumluluğu olmalıdır. Bunu desen örneklerinde bolca göreceksiniz. Başka sorumluluklar ayrı sınıflara dağıtılmalıdır. Bu yol inheritance değil, composition olmalıdır. Aksi takdirde object-oriented kasıcam diye ileride kodlara dönüp baktığınızda bir god-object görebilirsiniz :)
Burada listelediğim bir kaç desen size yetmeyecektir. Blog post'unun altında bu konu ile ilgili bolca link yazdım. Ayrıca django, pika, sqlalchemy, tornado gibi tonlarca open-source yazılımların kaynak kodlarını okumanızı tavsiye ederim. Desen yuvalarıdır onlar :) buradaki kuru örnekler yerine onları okumak daha faydalı olacaktır.
3. maddeyi biraz daha açıklamak istiyorum. Örnek olarak python'da zaten built-in gelen iterator pattern'ini kendimiz yazmamız aptalca olur. Diğer bir örnek ise örnek python'da fonksiyonların first-class objeler olmasıdır. Yani fonksiyonları istediğimiz saklayabilir, çalışma zamanında olarak programın akışına göre değiştirebiliriz. Bu bize çoğu pattern'i uygularken inanılmaz kolaylıklar sağlamaktadır. Wikipedia'daki strategy pattern'in açıklamasında bunu görebilirsiniz.
Python üzerinde en sık kullanılan 8 pattern'in örneğini vereceğim. Bu 8 pattern'in 8 pattern olmasının sebebi tamamen zannımca, kendi gözlemlerimdir.
Bu desende bir class'ın sadece bir tane instance'ı olması gerekmektedir. En çok kullanılan ve en basit desenlerden biridir. Amacı aynı işi yapan bir sürü instance'ın bellekte ayrı ayrı yer işgal etmesi yerine tek bir instance üzerinden iş yapmasını sağlamaktır. Genellikle database' bağlanmak gibi bir kere yapılması gereken yerlerde kullanılır.
def singleton(klass): if not hasattr(klass, 'instance'): klass.instance = klass() return klass.instance class Connection(object): def __init__(self): print "init..." # sadece bir kez calismali. a = singleton(Connection) b = singleton(Connection) print a is b # True
Factory pattern'i programın akışına göre belirlediğiniz sınıf ya da objeleri getiren fabrika olarak düşünebilirsiniz. Örneğin kullanılan database türüne göre bir database client'i getirmek için kullanılabilir. Ya da eğer makina linux ise şu modülü, windows işe şu modülü yükle gibi...
Django üzerinde formset'lerle işlem yaparken kullandığımız formset_factory, modelformset_factory birer factory pattern'ına örnektir;
def formset_factory(form, formset=BaseFormSet, extra=1, can_order=False, can_delete=False, max_num=None): attrs = {
'form' : form,
'extra' : extra, 'can_order' : can_order,
'can_delete': can_delete, 'max_num' : max_num
} return type(form.__name__ + 'FormSet', (formset,), attrs)
Bu desende çağırırken verdiğiniz forma göre ve belirlediğiniz miktara göre form içeren bir FormSet sınıfı döndürülmektedir. Daha da basit bir örnek verecek olursak; tumblr gibi bir servis üzerindeki post tiplerine göre bize model döndüren bir factory method yazalım.
def post_model_factory(model): return { 'dialog' : Dialog, 'quote' : Quote, 'link' : Link, 'video' : Video, 'audio' : Audio, }.get(model) or Post # eger bulunmazsa, default olarak Post modeli donduruluyor.
Decorator pattern'i bir sınıfa onu türetmeden ya da temel yapısında değişiklik yapmadan çalışma zamanında ek özellikler eklememizi sağlayan bir desendir.
Aslında Singleton Pattern örneğinde bir decorator pattern'i uyguladık. Singleton olmasını istediğimiz sınıf üzerinde birazcık oynama yaptık. Decorator pattern'i yaptığımız işlemin ta kendisidir
def singleton(klass): if not hasattr(klass, 'instance'): klass.instance = klass() return klass.instance
class Connection(object): def __init__(self): print "init..." # sadece bir kez calismali.
connection = singleton(Connection)
Ayrıca bildiğiniz üzere python'da built-in olarak decorator desteği var. Örneğin singleton pattern örneğini şu şekilde yapabilirdik.
def singleton(klass): if not hasattr(klass, 'instance'): klass.instance = klass() return klass.instance @singleton # Connection artik bir class degil, instance.
class Connection(object): def __init__(self): print "init..." # sadece bir kez calismali.
Proxy Pattern orijinal class'a dokunmadan, başka bir class oluşturup yeni özellikler eklememizi sağlayan bir desendir. Bunun için en iyi örnek olarak django'daki proxy model'leri gösterebilirim.
Django dökümantasyonundan bir örnek vermek istiyorum;
from django.contrib.auth.models import User class MyUser(User): class Meta: proxy = True def do_something(self): pass
Bu pattern python üzerinde built-in olarak gelmektedir. Iterator Pattern bir listedeki elemanların içerisinde dolaşmak, sonraki ve önceki elemanı bulmamızı sağlar. Bu liste sonsuz bir liste olabilir. Örneğin fibonacci serisinin tüm değerlerini bir listeye atamazsınız, ama iterator ile yaptığınızda sadece bir sonraki fibonacci sayısını istediğinizde size o sayıyı hesaplar ve getirir.
En basit şekilde bir iterator deseni;
def iterator(): yield "hey" yield "selam" yield "naber"
for item in iterator(): print item
Bir örnek yapacak olursak; bir model'deki kayıtlar üzerinde hesaplama yapıp sonuçlarından bir liste oluşturmak gerekmekte.
def orders(): result = [] for item in Orders.objects.all(): result.append(item.calculate()) # uzun suren bir islem return result
Bu şekilde listenin oluşabilmesi için bütün kayıtların hesaplanması ve listeye eklenmesi gerekiyor. Ama bize sadece ilk 10 kaydın hesaplamaları gerekli. Fail :)
Bunu aşağıdaki gibi yapmamız gerekiyor;
def orders(): for item in Orders.objects.all(): yield item.calculate() # uzun suren bir islem
Ayrıca iterator'lerin aşağıdaki gibi bir kullanımı da mümkündür. Satchmo'nun cart modelinden örnek veriyorum;
class Cart(models.Model): site = models.ForeignKey(Site, verbose_name=_('Site')) desc = models.CharField(_("Description"), blank=True, null=True, max_length=10) date_time_created = models.DateTimeField(_("Creation Date")) customer = models.ForeignKey(Contact, blank=True, null=True, verbose_name=_('Customer')) objects = CartManager() # her hangi bir sinifa asagidaki gibi iteration ekleyebiliyoruz. def __iter__(self): return iter(self.cartitem_set.all())
# ...
Bu sayede sepete ait ürünleri aşağıdaki kolayca alabiliyoruz.
cart = Cart.objects.from_request(request)
for item in cart:
print item # artik item bir CartItem instance'ı...
Publish/subscribe olarakta bilinmektedir. Temel anlamda sınıflarımıza event, yani olay atamamızı sağlayan desenlerdir. Bu desende bir objenin durumunun değişmesi durumunda, dinlemekte olan belirlediğiniz gözlemcilere haber gitmektedir.
Django'daki signal dispatcher kullanımı observer pattern'ine bir örnektir. Django'daki signals kullanımı ilgili bir örnek veriyorum;
def create_user_profile(sender, instance, created, **kwargs): if created: profile, created = UserProfile.objects.get_or_create(user=instance) post_save.connect(create_user_profile, sender=User)
Strategy pattern'i bir işin yapılmasının yöntemini çalışma zamanında belirlemenizi sağlar. Benim en sevdiğim desendir :) Örneğin bir servisiniz için bir importer yazacaksınız diyelim. Yazacağınız sınıfta verileri import ettiğiniz ve nereden import edeleceği kısımlarının ayrı tutulması gerekir. Import ettiğiniz servis bir rss ya da tamamen standart dışı bir xml olabilir. Bu gibi durumlarda komple import servisiniz yerine, import edeceğiniz yere özel strategy sınıfı düzenleyip o şekilde import etmeniz gerekir.
Pika'nın connection sınıfında bu deseni görebiliriz.
# ReconnectionStrategy super sinifi bos bir interface sinifidir.
class NullReconnectionStrategy(ReconnectionStrategy): pass
class SimpleReconnectionStrategy(ReconnectionStrategy): can_reconnect = True def __init__(self, initial_retry_delay=1.0, multiplier=2.0, max_delay=30.0, jitter=0.5): # implement edilen yerler onemli degil, kirptim.
class Connection(object): def __init__(self, parameters=None, on_open_callback=None, reconnection_strategy=None):
# ... self.reconnection = reconnection_strategy or NullReconnectionStrategy()
# ...
Connection sinifinda re-connection işleminin strategy olarak alındığını görüyoruz. Burada yapılan işlemin adı composition'dur. Connection sınıfında re-connection işlemlerinin yapılması yerine re-connection işlemi yapılacak yerlerde buradaki tanım üzerinden yapılacaktır.
Ayrıca zorunda olmadıkça buradaki gibi uzun uzun strategy tanımları yapmamıza gerek yok. Zaten python'daki fonksiyonlar first-class objelerdir. Birazcık pitonik olmak lazım :) Aşağıdaki örnekle daha iyi anlaşılacağını umuyorum.
def urllib_strategy(url): import urllib2 return urllib2.urlopen(url).read() def requests_strategy(url): import requests return requests.get(url).content def get_html_source(url, opener_strategy=urllib_strategy): # default urllib_strategy return opener_strategy(url) print get_html_source("http://fatiherikli.com") print get_html_source("http://fatiherikli.com", opener_strategy=requests_strategy) print get_html_source("http://fatiherikli.com", opener_strategy=lambda url : "fake html source.")
Bu deseni çoğu yerde görebilirsiniz. Bir örnek daha; Django'daki User application'ının view'larındaki login view'ında bu pattern uygulanmış durumda.
def login(request, template_name='registration/login.html', redirect_field_name=REDIRECT_FIELD_NAME, authentication_form=AuthenticationForm): redirect_to = request.REQUEST.get(redirect_field_name, '') form = authentication_form(data=request.POST)
# ...
Gördüğünüz üzere AuthenticationForm 'u view'da parametre olarak almaktadır. Varsayılan olarak kendi formunu kullanıyor, ancak siz view'ı çağırırken urls.py'nizde kendi login formunuzu verebilmektesiniz.
Null object pattern'i olmayan bir obje üzerinde işlem yaparken programın hata vermemesini sağlamaktır. Ayrıca bu desen sayesinde gereksiz null kontrollerinden kurtulmuş oluyoruz.
Bunun örneğini yine django üzerinden vereceğim. Bildiğiniz üzere django bize request.user dediğimizde bize aşağıdaki User sınıfını döndürmekte;
class User(models.Model)
# ...
def is_anonymous(self):
return False def is_authenticated(self): return True
# ...
Gördüğünüz üzere User sınıfında is_authenticated ve is_anonymous method'ları direk true-false döndürecek şekilde implement edilmiş. Bunun sebebi eğer kullanıcı login olmuşsa bu sınıf kullanılacak, login olmamış ise aşağıdaki AnonymousUser sınfı kullanılmasıdır. AnonymousUser sınıfında is_anonymous ve is_authenticated methodları tam ders durumda olacaktır.
class AnonymousUser(object): id = None username = '' is_staff = False is_active = False is_superuser = False
# ... def __unicode__(self): return 'AnonymousUser' def is_anonymous(self): return True def is_authenticated(self): return False
# ...
Gördüğünüz gibi bu sınıfta bir model instance'ı taklit edilmiş durumda. Bu sayede request.user.is_authenticated dediğimizde None type bla bla hatasını almadan kullanıcının login olup olmadığını anlayabiliyoruz.
Ayrıca bu desenin örneğini strategy pattern'inin örneklerinde verdiğim pika kütüphanesinin NullReconnectionStrategy sınıfında da görebilirsiniz.
Bu desenin başka bir örneğini satchmo'nun cart modelinde görebilirsiniz.
class NullCart(object): desc = None date_time_created = None customer = None total = Decimal("0") numItems = 0 def add_item(self, *args, **kwargs): pass def remove_item(self, *args, **kwargs): pass def empty(self): pass def __str__(self): return "NullCart (empty)" def __iter__(self): return iter([]) def __len__(self): return 0 def save(self): pass
http://en.wikipedia.org/wiki/Software_design_pattern
http://www.python.org/workshops/1997-10/proceedings/savikko.html
http://sourcemaking.com/design_patterns
http://www.javacamp.org/designPattern/
http://c2.com/cgi/wiki?DesignPatterns
http://www.blackwasp.co.uk/GofPatterns.aspx
Ayrıca türkçe olarak python üzerinde design pattern'lerle ilgili Yaşar Arabacı'nın bir yazısı bulunmakta;
http://yasararabaci.tumblr.com/post/14873752371
Sunumlar
http://www.slideshare.net/guest46da5428/application-of-...
http://www.slideshare.net/saurabh.net/design-patterns...