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) == 500

Görev

  1. BankaHesabi sınıfı oluşturun.
  2. Sınıfın dışarıdan kullanılan kısmına public arayüz denir. no, sahip ve bakiyeden hangilerinin bu arayüzde yer alacağına karar verin.
  3. 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.
  4. Başarısız para çekme işleminden sonra bakiye değişmemelidir.
  5. 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

  1. Sorumlulukları en az iki sınıfa ayırın: Siparis ve SiparisSatiri.
  2. Satırın kendi toplamını hesaplayan bir toplam() metodu olsun mu, düşünün.
  3. Sipariş toplamını, satırların public metotlarını çağırarak hesaplayın.
  4. 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.
  5. İndirim kuralını şimdilik Siparis iç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"] == 20

Görev

  1. Aday sınıflar KitapKopyasi, Uye ve Kutuphane. Her birinin sorumluluklarını yazın.
  2. Şöyle bir tasarım deneyin: KitapKopyasi kendi ödünç durumunu, Uye kendi ödünç sınırını ve kitap listesini yönetsin; Kutuphane kitabı ve üyeyi bulup ikisi arasındaki işlemi yürütsün (koordinasyon).
  3. İş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.
  4. İç kitaplar koleksiyonunu dışarıya açarsanız aliasing ve kapsülleme açısından hangi riskler doğar, değerlendirin.
  5. Her hatayı genel ValueError ile 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_available

Yeni 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 None olsun.
  • 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 None

Tasarım görevi

İki seçeneği karşılaştırın:

  • A: rezervasyon listesini doğrudan BookCopy içinde tutmak,
  • B: BookCopy içinde bir ReservationQueue nesnesi tutmak (kompozisyon).

Sonra birini seçip 100–150 kelimeyle şu dört soruyu yanıtlayın:

  1. Yeni sorumluluk hangi sınıfta?
  2. Hangi alternatifi neden reddettiniz?
  3. Hangi invariantı korudunuz?
  4. Hangi eski ve yeni testlerle doğruladınız?
ImportantBu problemde ne puanlanır?

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.

Back to top