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

Görev

  1. BankaHesabi sınıfı oluşturun.
  2. no, sahip ve bakiye durumunun hangi kısmının public arayüz olması gerektiğini belirleyin.
  3. Para yatırma/çekme doğrulamasını nesnenin geçerli durumunu koruyacak şekilde taşıyın.
  4. Başarısız para çekme işleminden sonra bakiye değişmemelidir.
  5. 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

  1. En az Siparis ve SiparisSatiri sorumluluklarını ayırın.
  2. Satırın kendi toplam() davranışı olmasını değerlendirin.
  3. Sipariş toplamını satırların public davranışını kullanarak hesaplayın.
  4. 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.
  5. İndirim kuralını şimdilik Siparis iç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"] == 20

Görev

  1. KitapKopyasi, Uye ve Kutuphane adaylarının sorumluluklarını yazın.
  2. KitapKopyasi kendi ödünç durumunu; Uye kendi limit/koleksiyon kuralını; Kutuphane bulma ve koordinasyonu üstlenecek bir tasarımı deneyin.
  3. Başarısız işlem iki nesneyi yarı güncellenmiş bırakmamalıdır. Kontrolleri durum değişiminden önce yapın.
  4. İç kitaplar koleksiyonunu dışarı açmanın aliasing/kapsülleme riskini değerlendirin.
  5. Genel ValueError yerine 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_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 olsun.
  • Boş sırada sıradaki üye None olsun.
  • 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 None

Tasarım görevi

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

  • A: rezervasyon listesini doğrudan BookCopy içinde tutmak,
  • B: ReservationQueue nesnesini BookCopy içinde kompozisyonla kullanmak.

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 puanlanan şey

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.

Back to top