Ek Çalışma — Prosedürel Koddan Nesne Tasarımına
Prosedürel koddan nesne tasarımına
Bu dört problemde başlangıç kodu çalışır durumdadır. Görev yeni özellik eklemek değil, dış davranışı koruyarak sorumlulukları daha anlaşılır nesnelere taşımaktır.
Her problemde temel kural:
Önce testleri çalıştır → küçük refactoring yap → testleri yeniden çalıştır.
Kodun sonunda verilen kontroller refactoring boyunca değişmeden geçmelidir. Sonuçları değiştirmek için testleri “uydurmak” refactoring değildir.
Problem 1 — Banka hesabı: sözlük + serbest fonksiyonlar
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))Davranışı sabitleyen kontroller
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.no,sahipve bakiye durumunun hangi kısmının public arayüz olması gerektiğini belirleyin.- Para yatırma/çekme doğrulamasını nesnenin geçerli durumunu koruyacak şekilde taşıyın.
- Başarısız para çekme işleminden sonra bakiye değişmemelidir.
- Yukarıdaki davranış kontrollerinin eşdeğerlerini yeni public arayüz üzerinden çalıştırın.
Bu problemde tek doğal alan nesnesi yeterlidir. Sırf “OOP” olsun diye DepositManager, BalanceValidator gibi mikro sınıflar üretmek gerekmez. Para çekme kuralı hesabın kendi durumuna yakındır.
Problem 2 — Sipariş: dağılmış toplam ve indirim kuralları
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))Sabit 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
- En az
SiparisveSiparisSatirisorumluluklarını ayırın. - Satırın kendi
toplam()davranışı olmasını değerlendirin. - Sipariş toplamını satırların public davranışını kullanarak hesaplayın.
- Siparişin satır koleksiyonunu dışarıdan doğrudan değiştirmenin invariantı delip delmediğini düşünün; gerekirse salt-okunur görünüm/kopya kullanın.
- İndirim kuralını şimdilik
Siparisiçinde tutabilirsiniz; fakat neden ayrı politika nesnesine çıkarılmadığını açıklayın.
Bu problem iki sınıf için doğal bir kompozisyon örneğidir: sipariş, sipariş satırlarına sahiptir. SiparisSatiri, Siparisin alt türü değildir.
Problem 3 — Kütüphane: 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"])Sabit 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
KitapKopyasi,UyeveKutuphaneadaylarının sorumluluklarını yazın.KitapKopyasikendi ödünç durumunu;Uyekendi limit/koleksiyon kuralını;Kutuphanebulma ve koordinasyonu üstlenecek bir tasarımı deneyin.- Başarısız işlem iki nesneyi yarı güncellenmiş bırakmamalıdır. Kontrolleri durum değişiminden önce yapın.
- İç
kitaplarkoleksiyonunu dışarı açmanın aliasing/kapsülleme riskini değerlendirin. - Genel
ValueErroryerine alanı anlamlandıran özel istisnalar eklemeyi deneyin.
İade ve ödünç işlemi birden çok nesnenin tutarlılığını etkiler. Bu nedenle “tek satırda her şeyi nesnenin içine taşıma” yerine yerel davranış + koordinasyon ayrımını düşünün. Testte yalnız sonucu değil, hata sonrası iki nesnenin de eski durumda kaldığını kontrol edin.
Problem 4 — Rezervasyon kuyruğu: 14. bölüm provası
Bu son problemde başlangıç sistemi çalışır; yeni rezervasyon davranışını eklerken önce sorumluluğu tasarlayın, sonra kodlayı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 olsun.
- Boş sırada sıradaki üye
Noneolsun. - Sıradan üye almak, sorgudan farklı olarak durumu değiştirsin ve metodun adı 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:
ReservationQueuenesnesiniBookCopyiçinde kompozisyonla kullanmak.
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?
Yalnız testlerin yeşil olması yeterli değildir. Çalışan ama bütün sorumlulukları Library içinde toplayan bir çözüm ile, rezervasyon tekrar/FIFO kuralını doğal bir sınırda tutan çözüm aynı tasarım kalitesine sahip değildir. Kodun neden o sınıfta olduğunu savunun.