Ek: Prosedürel koddan nesne tasarımına
Prosedürel koddan nesne tasarımına
Bu dört problemdeki başlangıç kodu çalışıyor. Sizden yeni özellik eklemeniz beklenmiyor. Programın dışarıdan görünen davranışı aynı kalacak; siz yalnız sorumlulukları daha anlaşılır nesnelere taşıyacaksınız. Davranışı değiştirmeden kodun yapısını iyileştirmeye refactoring denir.
Her problemde şu sırayı izleyin:
Önce testleri çalıştırın → küçük bir refactoring adımı atın → testleri yeniden çalıştırın.
Başlangıç kodunun ardından verilen kontroller, refactoring boyunca aynı sonucu vermeye devam etmelidir. Testleri yeni koda uysun diye “uydurmak” refactoring sayılmaz.
Problem 1: Sözlük ve serbest fonksiyonlarla banka hesabı
Başlangıç kodu
hesap = {
"no": "TR-101",
"sahip": "Ada",
"bakiye": 500,
}
def para_yatir(hesap, tutar):
if tutar <= 0:
raise ValueError("Tutar pozitif olmalıdır")
hesap["bakiye"] += tutar
def para_cek(hesap, tutar):
if tutar <= 0:
raise ValueError("Tutar pozitif olmalıdır")
if tutar > hesap["bakiye"]:
raise ValueError("Yetersiz bakiye")
hesap["bakiye"] -= tutar
def bakiye(hesap):
return hesap["bakiye"]
def ozet(hesap):
return f'{hesap["no"]} / {hesap["sahip"]}: {hesap["bakiye"]} TL'
para_yatir(hesap, 100)
para_cek(hesap, 50)
print(ozet(hesap))Korunacak davranış
def yeni_hesap():
return {"no": "TR-101", "sahip": "Ada", "bakiye": 500}
h = yeni_hesap()
para_yatir(h, 100)
assert bakiye(h) == 600
h = yeni_hesap()
para_cek(h, 50)
assert bakiye(h) == 450
h = yeni_hesap()
try:
para_cek(h, 700)
except ValueError:
pass
else:
raise AssertionError("Yetersiz bakiye reddedilmeliydi")
assert bakiye(h) == 500Görev
BankaHesabisınıfı oluşturun.- Sınıfın dışarıdan kullanılan kısmına public arayüz denir.
no,sahipve bakiyeden hangilerinin bu arayüzde yer alacağına karar verin. - Para yatırma ve çekmedeki kontrolleri sınıfın içine taşıyın. Nesne hiçbir adımda geçersiz bir duruma düşmesin.
- Başarısız para çekme işleminden sonra bakiye değişmemelidir.
- Yukarıdaki kontrolleri yeni sınıfın public metotlarıyla yeniden yazın ve çalıştırın.
Bu problemde tek bir sınıf, yani hesabın kendisi yeterlidir. Sırf “OOP” olsun diye DepositManager, BalanceValidator gibi küçük sınıflar yazmanız gerekmez. Para çekme kuralı doğrudan hesabın kendi bakiyesine bağlıdır.
Problem 2: Toplam ve indirim kuralları dağınık bir sipariş
Başlangıç kodu
siparis = {
"no": 501,
"durum": "taslak",
"satirlar": [
{"urun": "Klavye", "birim_fiyat": 1200, "adet": 2},
{"urun": "Mouse", "birim_fiyat": 500, "adet": 1},
],
}
def satir_toplami(satir):
if satir["adet"] <= 0:
raise ValueError("Adet pozitif olmalıdır")
if satir["birim_fiyat"] < 0:
raise ValueError("Fiyat negatif olamaz")
return satir["birim_fiyat"] * satir["adet"]
def ara_toplam(siparis):
toplam = 0
for satir in siparis["satirlar"]:
toplam += satir_toplami(satir)
return toplam
def indirim_tutari(siparis):
toplam = ara_toplam(siparis)
if toplam >= 2500:
return toplam * 0.10
return 0
def son_toplam(siparis):
return ara_toplam(siparis) - indirim_tutari(siparis)
def satir_ekle(siparis, urun, birim_fiyat, adet):
satir = {
"urun": urun,
"birim_fiyat": birim_fiyat,
"adet": adet,
}
satir_toplami(satir) # eklemeden önce doğrula
siparis["satirlar"].append(satir)
def onayla(siparis):
if siparis["durum"] != "taslak":
raise ValueError("Yalnız taslak sipariş onaylanabilir")
if not siparis["satirlar"]:
raise ValueError("Boş sipariş onaylanamaz")
siparis["durum"] = "onaylandi"
print(son_toplam(siparis))Korunacak davranış
assert ara_toplam(siparis) == 2900
assert indirim_tutari(siparis) == 290
assert son_toplam(siparis) == 2610
kopya = {
"no": 502,
"durum": "taslak",
"satirlar": [],
}
try:
onayla(kopya)
except ValueError:
pass
else:
raise AssertionError("Boş sipariş onaylanmamalı")
assert kopya["durum"] == "taslak"Görev
- Sorumlulukları en az iki sınıfa ayırın:
SiparisveSiparisSatiri. - Satırın kendi toplamını hesaplayan bir
toplam()metodu olsun mu, düşünün. - Sipariş toplamını, satırların public metotlarını çağırarak hesaplayın.
- Dış kod siparişin satır listesini doğrudan değiştirebilirse invariant bozulur mu, düşünün. Gerekirse dışarıya listenin kendisini değil, salt okunur bir görünümünü ya da kopyasını verin.
- İndirim kuralını şimdilik
Siparisiçinde tutabilirsiniz; ama neden ayrı bir politika nesnesine taşımadığınızı açıklayın.
Bu problem, iki sınıf arasındaki doğal bir kompozisyon örneğidir: sipariş, satırlarına sahiptir. SiparisSatiri bir tür Siparis değildir, bu yüzden ondan kalıtımla türetilmez.
Problem 3: Kütüphanede koleksiyon, kimlik ve koordinasyon
Başlangıç kodu
kitaplar = {
"B-1": {"baslik": "Python", "oduncte": False, "uye_no": None},
"B-2": {"baslik": "Veri Yapıları", "oduncte": False, "uye_no": None},
}
uyeler = {
10: {"ad": "Ada", "kitaplar": []},
20: {"ad": "Ece", "kitaplar": []},
}
MAX_ODUNC = 2
def kitap_bul(barkod):
return kitaplar.get(barkod)
def uye_bul(uye_no):
return uyeler.get(uye_no)
def odunc_ver(barkod, uye_no):
kitap = kitap_bul(barkod)
uye = uye_bul(uye_no)
if kitap is None:
raise ValueError("Kitap bulunamadı")
if uye is None:
raise ValueError("Üye bulunamadı")
if kitap["oduncte"]:
raise ValueError("Kitap zaten ödünçte")
if len(uye["kitaplar"]) >= MAX_ODUNC:
raise ValueError("Ödünç sınırı dolu")
kitap["oduncte"] = True
kitap["uye_no"] = uye_no
uye["kitaplar"].append(barkod)
def iade_al(barkod, uye_no):
kitap = kitap_bul(barkod)
uye = uye_bul(uye_no)
if kitap is None or uye is None:
raise ValueError("Kayıt bulunamadı")
if not kitap["oduncte"] or kitap["uye_no"] != uye_no:
raise ValueError("Bu üyenin ödünç kaydı yok")
kitap["oduncte"] = False
kitap["uye_no"] = None
uye["kitaplar"].remove(barkod)
def uye_odunc_sayisi(uye_no):
uye = uye_bul(uye_no)
if uye is None:
raise ValueError("Üye bulunamadı")
return len(uye["kitaplar"])Korunacak davranış
odunc_ver("B-1", 10)
assert kitaplar["B-1"]["oduncte"] is True
assert kitaplar["B-1"]["uye_no"] == 10
assert uye_odunc_sayisi(10) == 1
iade_al("B-1", 10)
assert kitaplar["B-1"]["oduncte"] is False
assert kitaplar["B-1"]["uye_no"] is None
assert uye_odunc_sayisi(10) == 0
odunc_ver("B-2", 20)
try:
odunc_ver("B-2", 10)
except ValueError:
pass
else:
raise AssertionError("Ödünçteki kitap ikinci üyeye verilmemeli")
assert kitaplar["B-2"]["uye_no"] == 20Görev
- Aday sınıflar
KitapKopyasi,UyeveKutuphane. Her birinin sorumluluklarını yazın. - Şöyle bir tasarım deneyin:
KitapKopyasikendi ödünç durumunu,Uyekendi ödünç sınırını ve kitap listesini yönetsin;Kutuphanekitabı ve üyeyi bulup ikisi arasındaki işlemi yürütsün (koordinasyon). - İşlem başarısız olursa iki nesneden biri güncellenmiş, öteki eski hâlinde kalmamalıdır. Bunun için bütün kontrolleri durumu değiştirmeden önce yapın.
- İç
kitaplarkoleksiyonunu dışarıya açarsanız aliasing ve kapsülleme açısından hangi riskler doğar, değerlendirin. - Her hatayı genel
ValueErrorile bildirmek yerine, adı hatayı anlatan, bu probleme özgü istisna sınıfları yazmayı deneyin.
Ödünç verme ve iade aynı anda iki nesnenin durumunu değiştirir ve bu iki durum birbiriyle uyumlu kalmalıdır. Bu yüzden her şeyi tek hamlede bir nesnenin içine yığmayın. Her nesne kendi durumunu yönetsin (yerel davranış), iki nesneyi birden ilgilendiren adımları ise tek bir yer yürütsün (koordinasyon). Testte başarılı sonucu kontrol etmekle yetinmeyin; hata çıktığında iki nesnenin de eski durumunda kaldığını da sınayın.
Problem 4: Rezervasyon kuyruğu (14. bölüme hazırlık)
Bu son problemdeki başlangıç sistemi de çalışıyor. Rezervasyonu eklerken önce sorumluluğun hangi sınıfta olacağına karar verin, sonra kodu yazın.
Başlangıç sistemi
class BookCopy:
def __init__(self, barcode, title):
self.barcode = barcode
self.title = title
self.borrowed_by = None
@property
def is_available(self):
return self.borrowed_by is None
def borrow_by(self, member_id):
if not self.is_available:
raise ValueError("Kitap uygun değil")
self.borrowed_by = member_id
def return_book(self):
if self.is_available:
raise ValueError("Kitap zaten rafta")
self.borrowed_by = None
class Library:
def __init__(self):
self._copies = {}
def add_copy(self, copy):
if copy.barcode in self._copies:
raise ValueError("Barkod tekrar ediyor")
self._copies[copy.barcode] = copy
def find_copy(self, barcode):
return self._copies.get(barcode)
def lend(self, barcode, member_id):
copy = self.find_copy(barcode)
if copy is None:
raise ValueError("Kitap bulunamadı")
copy.borrow_by(member_id)
def return_copy(self, barcode):
copy = self.find_copy(barcode)
if copy is None:
raise ValueError("Kitap bulunamadı")
copy.return_book()Eski davranışı koruyan regresyon testleri
library = Library()
copy = BookCopy("B-1", "Python")
library.add_copy(copy)
library.lend("B-1", 10)
assert copy.borrowed_by == 10
library.return_copy("B-1")
assert copy.is_availableYeni gereksinim
- Ödünçteki bir kitap için üyeler rezervasyon sırasına girebilsin.
- Aynı üye aynı kopya için iki kez sıraya giremesin.
- Sıra FIFO (first in, first out) olsun: sıraya ilk giren üye ilk çıkar.
- Sıra boşsa sıradaki üye
Noneolsun. - Sıradaki üyeye yalnız bakan metot durumu değiştirmesin. Üyeyi sıradan çıkaran metot ise durumu değiştirsin ve adı da bunu belli etsin.
Yeni kabul kriterleri
# Tasarımınıza göre adları uyarlayabilirsiniz; davranış değişmemeli.
queue = ReservationQueue()
assert queue.peek_next() is None
queue.add(10)
queue.add(20)
assert queue.peek_next() == 10
try:
queue.add(10)
except DuplicateReservationError:
pass
else:
raise AssertionError("Aynı üye iki kez eklenmemeli")
assert queue.peek_next() == 10
assert queue.take_next() == 10
assert queue.take_next() == 20
assert queue.take_next() is NoneTasarım görevi
İki seçeneği karşılaştırın:
- A: rezervasyon listesini doğrudan
BookCopyiçinde tutmak, - B:
BookCopyiçinde birReservationQueuenesnesi tutmak (kompozisyon).
Sonra birini seçip 100–150 kelimeyle şu dört soruyu yanıtlayın:
- Yeni sorumluluk hangi sınıfta?
- Hangi alternatifi neden reddettiniz?
- Hangi invariantı korudunuz?
- Hangi eski ve yeni testlerle doğruladınız?
Testlerin geçmesi puanın yalnız bir parçasıdır. İki çözüm düşünün: biri çalışıyor ama bütün sorumlulukları Library içinde topluyor; öteki tekrar ve FIFO kuralını rezervasyonla ilgilenen nesnede tutuyor. İkisi de testleri geçer, ama ikincisinin tasarımı daha iyidir. Her kodun neden o sınıfta olduğunu gerekçesiyle savunun.