Neden önemli?
Tasarım kalıpları (design patterns), tekrar eden tasarım sorunlarına verilmiş isimli çözümlerdir. Kod incelemesinde (code review) "burada strateji kalıbı daha uygun" cümlesi, dakikalarca anlatımın yerini tutar. Ama kalıpların çoğu Java/C++ dünyasında, fonksiyonların nesne olmadığı bir ortamda doğdu. Python'da fonksiyonlar birinci sınıf (first-class) değerdir, sınıflar çalışma zamanında oluşturulabilir, modüller zaten tekil nesnedir. Bu yüzden bu derste her kalıbı iki kez göreceksin: klasik biçimi ve Python'da sade biçimi. Hedef, kalıbı ezberlemek değil; ne zaman gerektiğini ve ne zaman gerekmediğini ayırt etmektir.
Strateji: değiştirilebilir davranış
Anadolu Market'te kargo ücreti şehre, ağırlığa ve kampanyaya göre farklı kurallarla hesaplanıyor. Klasik çözüm bir KargoStratejisi soyut sınıfı ve alt sınıflarıdır. Python'da aynı iş, aynı imzaya sahip fonksiyonlar ve bir sözlükle çözülür.
from typing import Callable
Strateji = Callable[[float, str], float] # (agirlik_kg, sehir) -> ücret
def sabit(agirlik, sehir):
return 49.90
def agirlik_bazli(agirlik, sehir):
return 20 + 8 * agirlik
def bolgesel(agirlik, sehir):
uzak = {'Hakkari', 'Kars', 'Van'}
return agirlik_bazli(agirlik, sehir) * (1.5 if sehir in uzak else 1.0)
STRATEJILER: dict[str, Strateji] = {
'sabit': sabit, 'agirlik': agirlik_bazli, 'bolgesel': bolgesel,
}
def kargo_ucreti(agirlik, sehir, strateji: str = 'agirlik'):
hesapla = STRATEJILER[strateji]
return round(hesapla(agirlik, sehir), 2)
for s in STRATEJILER:
print(f'{s:<9}', kargo_ucreti(3.5, 'Van', s))
sorted(key=...), map, dekoratör ile her gün yaptığın şeydir. Stratejinin durumu (state) ve birden fazla metodu varsa o zaman sınıf (ya da Protocol) düşün.Fabrika ve kayıt (registry)
Fabrika (factory), "hangi sınıfın üretileceğini" çağıran koddan gizler. Ilgaz Bank'ta rapor türüne göre farklı dışa aktarıcı üretmek gibi. Python'da en temiz biçim: bir sözlükte sınıfları tut, __init_subclass__ ya da dekoratörle otomatik kaydet. Sınıflar zaten çağrılabilir olduğu için ayrı bir "Creator" hiyerarşisine gerek kalmaz.
import json, csv, io
KAYIT: dict[str, type] = {}
def disa_aktarici(format_adi):
def kaydet(cls):
KAYIT[format_adi] = cls
return cls
return kaydet
@disa_aktarici('json')
class JsonAktar:
def yaz(self, satirlar):
return json.dumps(satirlar, ensure_ascii=False)
@disa_aktarici('csv')
class CsvAktar:
def yaz(self, satirlar):
buf = io.StringIO()
w = csv.DictWriter(buf, fieldnames=list(satirlar[0]))
w.writeheader(); w.writerows(satirlar)
return buf.getvalue().strip()
def aktarici_uret(format_adi: str):
try:
return KAYIT[format_adi]()
except KeyError:
raise ValueError(f'desteklenmeyen format: {format_adi!r}; seçenekler: {sorted(KAYIT)}')
veri = [{'hesap': 'TR01', 'bakiye': 1500.0}, {'hesap': 'TR02', 'bakiye': -20.5}]
for f in ('json', 'csv'):
print(aktarici_uret(f).yaz(veri))
try:
aktarici_uret('xml')
except ValueError as e:
print(e)
Gözlemci: olaylara abone olmak
Gözlemci (observer), bir olay olduğunda ilgilenen herkese haber verir; yayıncı aboneleri tanımaz. Sipariş onaylanınca stok düşsün, e-posta gitsin, muhasebe kaydı açılsın. Python'da abone bir fonksiyon, abone listesi de bir listedir.
from collections import defaultdict
class OlayYayini:
def __init__(self):
self._aboneler = defaultdict(list)
def abone_ol(self, olay, fn=None):
if fn is None: # @bus.abone_ol('olay') biçimi: dekoratör döndür
return lambda f: self.abone_ol(olay, f)
self._aboneler[olay].append(fn)
return fn
def yayinla(self, olay, **veri):
for fn in self._aboneler[olay]:
fn(**veri)
bus = OlayYayini()
stok = {'kahve': 10}
@bus.abone_ol('siparis_onay')
def stok_dus(urun, adet, **_):
stok[urun] -= adet
print(f'stok güncellendi: {urun}={stok[urun]}')
@bus.abone_ol('siparis_onay')
def eposta(musteri, **_):
print(f'e-posta: {musteri} siparişiniz onaylandı')
bus.yayinla('siparis_onay', urun='kahve', adet=3, musteri='ayse@x.com')
bus.yayinla('iade', urun='kahve') # abone yok, sessizce geçer
try/except ile sarıp loglamak, ya da hataları toplayıp sonunda raporlamak gerekir. Ayrıca abonelerin çağrı sırasına güvenme; sıraya bağımlılık varsa gözlemci yanlış kalıptır.Tekil (singleton) ve neden modül daha iyi
Tekil, bir sınıftan yalnızca tek örnek olmasını garanti eder: yapılandırma, bağlantı havuzu, logger. Klasik gerçekleştirme __new__ üzerinden yapılır. Ancak Python'da her modül yalnızca bir kez yüklenir ve zaten tekildir: config.py içinde AYARLAR = {...} yazmak, aynı garantiyi sıfır kodla verir ve test ederken yerine sahte nesne koymak (monkeypatch) çok daha kolaydır.
class Ayarlar:
_ornek = None
def __new__(cls):
if cls._ornek is None:
cls._ornek = super().__new__(cls)
cls._ornek.degerler = {'para_birimi': 'TRY'}
return cls._ornek
a, b = Ayarlar(), Ayarlar()
print(a is b, a.degerler)
# Pythonic eşdeğer: modül düzeyinde tek nesne
import types
config = types.SimpleNamespace(para_birimi='TRY', kdv=0.20)
print(config.para_birimi, config.kdv)
| Kalıp | Çözdüğü sorun | Klasik biçim | Python'da sade biçim |
|---|---|---|---|
| Strateji | Değiştirilebilir algoritma | Soyut sınıf + alt sınıflar | Fonksiyon sözlüğü, key= |
| Fabrika | Üretimi gizlemek | Creator hiyerarşisi | Kayıt sözlüğü + sınıf çağrısı |
| Gözlemci | Gevşek bağlı bildirim | Subject/Observer arayüzü | Fonksiyon listesi |
| Tekil | Tek örnek | __new__ ile kontrol | Modül düzeyi nesne |
| Adaptör | Uyumsuz arayüz | Sarmalayıcı sınıf | Sarmalayıcı sınıf ya da fonksiyon |
| Komut | İşlemi nesne yapmak, geri alma | execute/undo arayüzü | dataclass + Protocol |
Adaptör: uyumsuz arayüzleri bağlamak
Eski bir ödeme kütüphanesi charge(amount_cents) bekliyor, yeni kodun ise tahsil_et(tutar_tl) çağırıyor. Hiçbirini değiştirmeden ikisini bir adaptörle bağlarsın. Adaptör ince olmalı: dönüştür, yönlendir, bitir.
class EskiOdemeKutuphanesi: # değiştiremediğin dış kod
def charge(self, amount_cents: int) -> dict:
return {'ok': amount_cents > 0, 'cents': amount_cents}
class OdemeAdaptoru:
def __init__(self, eski):
self._eski = eski
def tahsil_et(self, tutar_tl: float) -> bool:
return self._eski.charge(round(tutar_tl * 100))['ok']
def siparisi_tamamla(saglayici, tutar):
return 'tamam' if saglayici.tahsil_et(tutar) else 'reddedildi'
print(siparisi_tamamla(OdemeAdaptoru(EskiOdemeKutuphanesi()), 149.90))
print(siparisi_tamamla(OdemeAdaptoru(EskiOdemeKutuphanesi()), 0))
Komut: işlemi nesne yapmak
Komut (command) kalıbında her işlem; çalıştırılabilen ve geri alınabilen bir nesnedir. Geri al/yinele, işlem kuyruğu, denetim kaydı (audit log) için kullanılır. Ilgaz Bank'ta hesap hareketlerini komut olarak modellemek, hatalı bir toplu işlemi tersine çevirmeyi mümkün kılar.
from dataclasses import dataclass, field
@dataclass
class YatirKomutu:
hesaplar: dict
hesap: str
tutar: float
def uygula(self):
self.hesaplar[self.hesap] += self.tutar
def geri_al(self):
self.hesaplar[self.hesap] -= self.tutar
@dataclass
class TransferKomutu:
hesaplar: dict
kaynak: str
hedef: str
tutar: float
def uygula(self):
if self.hesaplar[self.kaynak] < self.tutar:
raise ValueError('yetersiz bakiye')
self.hesaplar[self.kaynak] -= self.tutar
self.hesaplar[self.hedef] += self.tutar
def geri_al(self):
self.hesaplar[self.hedef] -= self.tutar
self.hesaplar[self.kaynak] += self.tutar
@dataclass
class Islemci:
gecmis: list = field(default_factory=list)
def calistir(self, komut):
komut.uygula()
self.gecmis.append(komut)
def geri_al(self):
if self.gecmis:
self.gecmis.pop().geri_al()
h = {'TR01': 100.0, 'TR02': 0.0}
p = Islemci()
p.calistir(YatirKomutu(h, 'TR01', 50))
p.calistir(TransferKomutu(h, 'TR01', 'TR02', 120))
print(h)
p.geri_al(); print('transfer geri alındı:', h)
p.geri_al(); print('yatırma geri alındı:', h)
Derste olmayan altı kalıp, Python'ca
Strateji, fabrika, gözlemci, adaptör ve komutu gördün. Kurumsal Python kodunda en az onlar kadar sık karşına çıkan altı kalıp daha var. Hepsinin ortak noktası, "değişecek olanı değişmeyenden ayırmak"tır. Python'da çoğu, sınıf hiyerarşisi yerine birkaç fonksiyonla ya da küçük bir sözlükle kurulur.
| Kalıp | Çözdüğü sorun | Python'daki kısa yolu | Nerede görürsün? |
|---|---|---|---|
| Durum (State) | Nesnenin davranışı durumuna göre değişir, if/elif patlar | Geçiş tablosu sözlüğü ya da Enum | Sipariş, kredi başvurusu akışı |
| Sorumluluk zinciri (Chain of responsibility) | İstek birkaç adımdan sırayla geçmeli | Fonksiyon listesi, ara katman (middleware) | Web çatıları, doğrulama hattı |
| Şablon metot (Template method) | Algoritmanın iskeleti sabit, adımları değişir | Taban sınıfta somut metot + soyut adımlar | Rapor üretimi, ETL işleri |
| Kurucu (Builder) | Çok parçalı nesneyi adım adım kurmak | self döndüren akıcı (fluent) metotlar | SQL sorgu kurucular, HTTP istemcileri |
| Depo (Repository) + bağımlılık enjeksiyonu | İş kuralı veritabanına yapışık, test edilemez | Depoyu kurucuya parametre olarak ver | Servis katmanı, birim testler |
| Süsleyici (Decorator) nesne | Nesneye çalışma anında yetenek eklemek | Aynı arayüzü uygulayan sarmalayıcı sınıf | Loglama, önbellek (cache), yeniden deneme |
Durum makinesi: geçişleri veri olarak yaz
Anadolu Market siparişi yeni → ödendi → kargoda → teslim akışında ilerler; iptal yalnızca kargoya verilmeden önce mümkündür. Bu kuralları if/elif ile her metoda dağıtırsan yeni bir durum eklemek tüm metotları değiştirmeyi gerektirir. Geçiş tablosu kuralları tek yere toplar, tablo aynı zamanda belge görevi görür.
GECISLER = {
"yeni": {"ode": "odendi", "iptal": "iptal"},
"odendi": {"kargola": "kargoda", "iptal": "iptal"},
"kargoda": {"teslim": "teslim"},
}
class Siparis:
def __init__(self, no):
self.no, self.durum, self.gecmis = no, "yeni", ["yeni"]
def olay(self, ad):
hedef = GECISLER.get(self.durum, {}).get(ad)
if hedef is None:
raise ValueError(f"{self.durum!r} durumunda {ad!r} yapılamaz")
self.durum = hedef
self.gecmis.append(hedef)
s = Siparis(1042)
for ad in ["ode", "kargola", "iptal"]:
try:
s.olay(ad)
except ValueError as h:
print("Reddedildi:", h)
print(s.durum, s.gecmis)
Sorumluluk zinciri: ara katmanlar
Bir API isteği önce kimlik doğrulamadan, sonra hız sınırından, sonra loglamadan geçip asıl işleyiciye ulaşır. Her katman ya isteği reddeder ya da bir sonrakine devreder. Flask, Django ve FastAPI'deki middleware'ler bu kalıptır. Python'da her katman (istek, sonraki) alan bir fonksiyon olabilir.
def kimlik(istek, sonraki):
if not istek.get("token"):
return {"durum": 401}
return sonraki(istek)
def log(istek, sonraki):
yanit = sonraki(istek)
print(f"{istek['yol']} -> {yanit['durum']}")
return yanit
def isleyici(istek):
return {"durum": 200, "veri": "bakiye: 1500"}
def zincir(katmanlar, son):
for katman in reversed(katmanlar):
son = (lambda k, s: lambda istek: k(istek, s))(katman, son)
return son
uygulama = zincir([log, kimlik], isleyici)
uygulama({"yol": "/hesap", "token": "abc"})
uygulama({"yol": "/hesap"})
Şablon metot ve kurucu
Şablon metotta taban sınıf algoritmanın sırasını sabitler (uret(): veriyi getir, süz, biçimle), alt sınıflar yalnızca değişen adımları doldurur. Strateji kalıbıyla farkı: strateji davranışı dışarıdan verilen bir nesneyle değiştirir (bileşim), şablon metot ise kalıtımla (inheritance) değiştirir. Kurucu (builder) ise çok seçenekli bir nesneyi zincirleme çağrılarla kurar; her metot self döndürür, en sonda sql(), build() gibi bir metot sonucu üretir. SQLAlchemy'nin select(...).where(...).order_by(...) yazımı buna örnektir.
from abc import ABC, abstractmethod
class Rapor(ABC):
def uret(self): # şablon: sıra sabit
satirlar = [s for s in self.veri() if self.dahil_mi(s)]
return self.bicimle(satirlar)
@abstractmethod
def veri(self): ...
def dahil_mi(self, satir): # isteğe bağlı kanca (hook)
return True
@abstractmethod
def bicimle(self, satirlar): ...
class BuyukSiparisCSV(Rapor):
def veri(self):
return [("İzmir", 1200), ("Bursa", 300), ("Ankara", 5400)]
def dahil_mi(self, s):
return s[1] >= 1000
def bicimle(self, satirlar):
return "\n".join(f"{a};{t}" for a, t in satirlar)
print(BuyukSiparisCSV().uret())
Depo kalıbı ve bağımlılık enjeksiyonu
İş kuralı içinde sqlite3.connect("prod.db") yazarsan o kuralı veritabanı olmadan test edemezsin. Çözüm: veri erişimini bir depo (repository) arayüzünün arkasına koy ve servisi kurarken depoyu parametre olarak ver (dependency injection). Üretimde SQL deposu, testte bellek içi sözlük deposu verilir; servis kodu değişmez. Python'da bunun için çatıya gerek yoktur: kurucu parametresi yeter. Arayüzü Protocol ile tanımlamak (ileri OOP dersi) tip denetleyicisine de sözleşmeyi bildirir.
from typing import Protocol
class HesapDeposu(Protocol):
def bakiye(self, no: str) -> int: ...
def guncelle(self, no: str, yeni: int) -> None: ...
class BellekDepo:
def __init__(self, veri): self.veri = dict(veri)
def bakiye(self, no): return self.veri[no]
def guncelle(self, no, yeni): self.veri[no] = yeni
class HavaleServisi:
def __init__(self, depo: HesapDeposu):
self.depo = depo # enjekte edilen bağımlılık
def havale(self, kimden, kime, tutar):
if self.depo.bakiye(kimden) < tutar:
raise ValueError("Yetersiz bakiye")
self.depo.guncelle(kimden, self.depo.bakiye(kimden) - tutar)
self.depo.guncelle(kime, self.depo.bakiye(kime) + tutar)
depo = BellekDepo({"TR1": 1000, "TR2": 0})
HavaleServisi(depo).havale("TR1", "TR2", 250)
print(depo.veri)
Kalıp seçiminde kurumsal refleksler
Kod incelemelerinde en sık görülen kalıp hatası "fazla mimari"dir: üç satırlık bir koşul için strateji hiyerarşisi, iki format için fabrika sınıfı. Kuralı şöyle koy: kalıbı, aynı sorunu ikinci kez çözerken tanıt, birincide değil. Birinci sınıf fonksiyonlar, functools.partial, dataclass ve Protocol, klasik kalıpların büyük kısmını iki üç satıra indirir.
Buna karşılık bazı kalıplar Python'da bile sınıf ister: komut (durum + geri alma), gözlemci (abone yönetimi, hata izolasyonu), adaptör (dış sistem sınırı). Bunların ortak noktası, kodun bir sınırda durmasıdır: dış kütüphane, ağ, veritabanı, kullanıcı etkileşimi. Sınırlarda açık arayüz (Protocol) tanımla; içeride sade fonksiyonlarla kal.
Testte kalıpların karşılığı: strateji sözlüğünü testte sahte fonksiyonla değiştir; gözlemcide aboneleri listeye toplayıp yayın sonrası kontrol et; komutlarda uygula + geri_al sonrası durumun başlangıca döndüğünü doğrula (round-trip testi). Tekil yerine modül düzeyi nesne kullanmanın en büyük getirisi de burada: monkeypatch.setattr(config, 'kdv', 0) ile tek satırda izole test.