Kompozisyon, delegation ve nesne koleksiyonları
Gerçek programlar tek bir sınıftan oluşmaz. Bir siparişin müşterisi ve ürünleri, bir kütüphanenin kitapları ve üyeleri vardır. Bu hafta bir nesnenin başka nesneleri niteliklerinde tutmasını (has-a, yani “sahiptir” ilişkisi), sınıfları bu yolla birleştirmeyi (kompozisyon) ve bir nesnenin bir hesabı, o hesabın kuralını bilen başka bir nesneye bırakmasını (delegation) göreceğiz.
Bu sayfadaki kod hücreleri kendiliğinden çalışmaz. Her hücrede önce çıktıyı tahmin edin, sonra Run Code düğmesine basın. Kodu değiştirip yeniden çalıştırabilir, Start Over ile ilk hâline döndürebilirsiniz.
Bu bölümün kapsamı
- Nesnenin başka nesneyi tuttuğu
has-ailişkisi - Koleksiyonun nesneleri değil referanslarını tutması
- Değiştirilebilir (mutable) iç koleksiyonu dışarı vermenin kapsüllemeyi bozması
- İç listeyi dışarıya demet ya da kopya olarak vermek
- Davranışı kuralın sahibine bırakan delegation
- Sorumluluğu yanlış sınıfa yığmanın sonucu
- Bire çok ilişkide liste ile sözlük seçimi
- UML’deki aggregation ve composition ayrımının biçimsel tanımı
- ORM (nesneleri veritabanı tablolarına eşleyen araçlar) ve kalıcı veri katmanı
collections.abckoleksiyon protokolleri
Bu başlıklar konunun devamıdır. İleride karşınıza çıkar, ama bu derste ezberlemeniz beklenmiyor.
Sipariş programındaki beş işi C’de nasıl yapardınız?
Bir internet mağazasının sipariş programını yazıyorsunuz. Her siparişin bir müşterisi ve birkaç ürünü var. Program şu beş işi yapacak:
- Siparişin müşterisini ve ürünlerini tutup toplam tutarı hesaplamak
- Ürünleri siparişe yalnız kontrolden geçirerek eklemek: aynı ürün iki kez eklenmemeli
- Toplu alımda ürünün indirim kuralını uygulamak: 10 adet ve üstüne yüzde 10 indirim
- Sipariş verilince ürünün stoğunu düşmek
- Müşteriyi numarasıyla bulmak
C’de müşteriyi ve ürünü birer struct ile tutardınız. Siparişin struct’ında bir müşteri ve bir ürün dizisi olurdu. Bu işlerin her birini C’de nasıl yapardınız? Okumaya devam etmeden önce her iş için bir satır yazın. 8. haftadaki Product sınıfıyla da düşünebilirsiniz. “Ürün dizisini döngüyle dolaşıp fiyatları toplarım” da bir cevap.
Bölümde bu beş işin Python’da nasıl yazıldığını göreceğiz. Bölümün sonunda listenize döneceğiz.
Bir nesne başka nesneyi tutabilir
C’de bir struct’ın içine başka bir struct koyduğunuzda iç yapının alanları kopyalanırdı. Hücredeki Order, bir Customer nesnesini customer niteliğinde tutuyor. Sipariş müşterinin bir kopyasını mı tutuyor, yoksa aynı nesneyi mi? Son satır ne yazar?
self.customer = customer ataması kopya almaz: 3. haftada gördüğünüz gibi = var olan nesneye bir ad daha bağlar. C’de aynı davranışı struct’ın içinde müşteriye bir işaretçi tutarak elde ederdiniz.
İngilizcede bu ilişki Order has a Customer (sipariş bir müşteriye sahiptir) diye okunur. has-a adı buradan gelir. Bir sınıfın işini, niteliklerinde tuttuğu başka nesnelerle yapmasına kompozisyon (composition) denir.
Bu derste kompozisyon sözcüğünü geniş anlamda kullanıyoruz: bir sınıfın işini, niteliklerinde tuttuğu başka nesnelerle yapması. UML (sınıfları ve aralarındaki ilişkileri kutu ve okla çizmek için kullanılan standart gösterim) ve bazı kaynaklar burada iki ilişkiyi ayırır. Parça bütünle birlikte yok oluyorsa, örneğin sipariş silinince sipariş satırları da gidiyorsa buna composition (bileşim) denir. Parça bütünden bağımsız yaşıyorsa, örneğin takım dağılınca oyuncular yerinde kalıyorsa buna aggregation (agregasyon) denir. Bu ayrım sınavda sorulmaz. Nesneler arasındaki ilişkiyi doğru kurmanız ve her kuralı doğru sınıfa koymanız yeter.
Nesne koleksiyonu
C’de siparişin ürünlerini bir struct dizisinde tutar, toplamı döngüyle hesaplardınız. Python’da sipariş ürünleri bir listede tutar. Hücrede klavye siparişe eklendikten sonra fiyatı 1000’den 900’e iniyor. order.total() kaç döndürür?
_items bir sayı listesi değildir, Product nesnelerinin referanslarını tutar. keyboard adı da listedeki ilk eleman da aynı nesneyi gösterir. total() her çağrıldığında ürünlerin o anki fiyatına bakar.
İç listeyi dışarı vermek kapsüllemeyi bozabilir
Siparişe ürün eklemenin yolu add_product() olmalı, çünkü kontrol orada. Hücredeki add_product aynı ürünün iki kez eklenmesini engelliyor. Kısa kalsın diye ürünleri adlarıyla tutuyoruz. Liste _items adıyla duruyor ve dışarıya bir property ile veriliyor. Dışarıdaki kod order.items.append("Klavye") yazıyor. Bu satır hata verir mi? Son satır ne yazar?
Property listeyi olduğu gibi döndürdü. Dış kod sınıfın listesine referans aldı ve add_product() metoduna hiç uğramadan ekleme yaptı. items düz bir nitelik olsaydı (self.items = []) sonuç yine aynı olurdu.
Dış kodun listeyi yalnız okuyabilmesini istiyorsanız bir demet döndürebilirsiniz. Bu kez property demet döndürüyor. Son satırda ne olur?
Demetin append metodu yoktur. Dış kod listeyi okuyabilir, ama eleman eklemenin tek yolu yine add_product().
Dış kod üzerinde değişiklik yapabileceği ayrı bir liste istiyorsa sığ kopya döndürebilirsiniz. Kopyaya eklenen eleman iç listeye gitmez:
@property
def items(self):
return self._items.copy()Hangisinin uygun olduğu, sınıfın çağırana ne söz verdiğine bağlıdır. Demet “bu listeyi değiştirmeyin” der, kopya “bu liste artık sizin” der.
3. haftada aliasing gördük: iki ad aynı değiştirilebilir nesneyi gösterince birinden yapılan değişiklik ötekinden de görünür. 8. haftada nesnenin durumunu yalnız sınıfın kendi metotlarıyla değiştirmeyi öğrendik, çünkü kurallar o metotlarda denetlenir. İç listeyi dışarı verdiğinizde dış kod sınıfın listesine referans alır. Artık add_product() gibi denetleyen metotlara hiç uğramadan listeyi değiştirebilir.
Delegation: işi sahibine bırakmak
Sipariş satırı (OrderLine) bir ürünü ve adedini tutar. Toplu alım indirimi ise ürünün fiyat kuralıdır: Product.price_for() metodu 10 adet ve üstüne yüzde 10 indirim uygular. Satırın tutarını hesaplayan subtotal() bu metodu çağırıyor. İki satır ne yazar?
5 kalemde indirim yoktur, discount 0 olur ve sonuç 10 * 5 * 1 = 50 çıkar. 12 kalemde discount 0.10 olur. 1 - 0.10 bir ondalık sayı (float) olduğu için sonuç da float olur: 108.0.
OrderLine fiyat kuralını kopyalamaz, hesabı Product.price_for() metoduna devreder. Bir nesnenin bir işi, o işin kuralını bilen başka bir nesneye bırakmasına delegation denir. İndirim kuralı değişirse yalnız Product sınıfını güncellersiniz. C’de satır tutarını hesaplayan fonksiyonun, ürünün fiyat fonksiyonunu çağırması gibi düşünebilirsiniz.
Delegation, davranışı kuralın sahibi olan sınıfa koyar. İndirim kuralı ürüne aitse hesabı Product yapar. Ürüne özgü bir kural yoksa, örneğin ara toplam yalnız birim fiyatla adedin çarpımıysa, bu çarpmayı OrderLine içinde yapmak da makuldür.
Sorumluluk yanlış yerdeyse ne olur?
Sipariş verilince ürünün stoğu düşmeli. Bu işi Order kendisi yapabilir:
class Order:
def add_line(self, product, quantity):
if quantity > product._stock:
raise ValueError("Yetersiz stok")
product._stock -= quantity
self._lines.append(OrderLine(product, quantity))
# kargo ücreti, müşteri puanı, kampanya kuralları...Kod çalışır, ama stok kuralı artık iki yerde yazılı: 8. haftada aynı kuralı Product.sell() içine yazmıştık. Order ürünün alt çizgili niteliğine dışarıdan dokunuyor. Kargo, puan ve kampanya kuralları da buraya eklendikçe sınıf kısa sürede “her şeyi yapan” bir merkeze dönüşür. Stoğu düşmek ürünün işidir:
class Order:
def add_line(self, product, quantity):
product.sell(quantity)
self._lines.append(OrderLine(product, quantity))Order artık yalnız işlemin sırasını yönetir: önce ürüne satışı yaptırır, sonra satırı ekler. Stok yetmezse sell() hata verir ve satır da eklenmez.
Bire çok ilişki ve koleksiyon seçimi
- Tek nesne:
order.customer - Çok sayıda nesne:
order.lines
Bir siparişin tek müşterisi vardır, onu tek bir nitelik tutar. Satırları ise çoktur, onları bir koleksiyon tutar. Bir nesnenin aynı türden çok sayıda nesne tuttuğu bu ilişkiye bire çok ilişki denir. Koleksiyonun liste mi sözlük mü olacağına, ona nasıl erişeceğiniz karar verir.
Mağazanın müşterilerini tutan bir Shop sınıfı düşünün. C’de müşteriyi numarasıyla bulmak için diziyi baştan sona tarardınız. Sözlükte ise anahtar müşteri numarası, değer Customer nesnesi olur ve müşteriye taramadan ulaşırsınız. Hücrede 103 numaralı müşteri yok. Üç satır ne yazar?
Aynı numaranın iki kez eklenmesini Shop engeller, çünkü bütün numaraları yalnız koleksiyonu tutan sınıf görür. Tek bir Customer nesnesi öteki müşterileri bilmez.
Soruya dönelim: beş iş Python’da
Bölümün başında sipariş programının beş işini C’de nasıl yapacağınızı sormuştuk. Python’daki karşılıkları şöyle:
| İş | C’de | Bu haftaki Python yolu | Dikkat |
|---|---|---|---|
| Müşteriyi ve ürünleri tutup toplamı hesaplamak | struct içinde müşteri ve ürün dizisi, toplam için döngü |
self.customer niteliği, self._items listesi ve total() metodu |
Liste ürünlerin kopyasını değil referansını tutar. Ürünün fiyatı değişirse toplam da değişir |
| Ürünü yalnız kontrolden geçirerek eklemek | add_item() fonksiyonu yazılır, ama diziye her kod yazabilir |
add_product() metodu ve _items listesi |
Property self._items döndürürse dış kod kontrolü atlar. Demet ya da kopya döndürün |
| İndirim kuralını uygulamak | Satır tutarını hesaplayan fonksiyon ürünün fiyat fonksiyonunu çağırır | subtotal() içinde self.product.price_for(self.quantity) |
Kuralı satıra kopyalamayın, ürüne bırakın |
| Stoğu düşmek | Sipariş fonksiyonu ürünün stok alanını azaltır | add_line() içinde product.sell(quantity) |
Order ürünün _stock niteliğine dokunmaz |
| Müşteriyi numarasıyla bulmak | Diziyi baştan sona tararsınız | self._customers sözlüğü ve find_customer() |
Tekrar kontrolünü koleksiyonu tutan Shop yapar |
Listenizi tabloyla karşılaştırın. 1. iş için “müşteri struct’ını siparişin içine koyarım” diye yazdıysanız C’de müşterinin bir kopyasını tutmuş olurdunuz: müşterinin adı değişince sipariş eski adı gösterirdi. Python’da sipariş aynı müşteri nesnesini gösterir. 2. işte C ile Python aynı yerde zorlanır: diziyi ya da listeyi dışarı açarsanız kontrolü atlayan kodu hiçbir şey durdurmaz. Python’da çözüm, listeyi dışarı hiç vermemek ya da demet veya kopya olarak vermektir. 4. iş için “sipariş fonksiyonu stoğu azaltır” dediyseniz kod çalışır, ama stok kuralı iki yerde yazılmış olur.
Alıştırma: İlişkiyi belirle
Car ve Engine, Order ve Customer, Playlist ve Song, Student ve Course çiftlerine tek tek bakın. İlişki has-a mı? Öyleyse hangi sınıfın ötekini tutması uygun? Tartışın. Sonra şunu düşünün: ilişkiyi iki tarafta birden saklarsanız, yani öğrenci derslerini, ders de öğrencilerini tutarsa, bir öğrenci dersi bıraktığında hangi iki yeri güncellemeniz gerekir? İkisinden birini unutursanız ne olur?
Alıştırma: Nesne koleksiyonunu tamamla
Team sınıfı oyuncuları bir listede tutuyor. add_player aynı numaralı oyuncunun iki kez eklenmesini engelliyor. Kontroldeki any(...), parantezin içindeki koşul listedeki oyunculardan en az biri için doğruysa True döndürür. Kontrol geçtikten sonra oyuncuyu iç listeye ekleyen satırı boşluğa yazın.
self._players.append(player)Sıra sizde: Kütüphane modeli
Book, Member ve Library sınıflarıyla küçük bir model kurun:
- Her kitap ISBN’iyle ayırt edilsin.
- Aynı ISBN iki kez eklenemesin.
- Üye numarası benzersiz olsun.
Librarykitapları ve üyeleri koleksiyonlarda tutsun.Librarybu değiştirilebilir koleksiyonları doğrudan dışarı vermesin.- Ödünç alma davranışı hangi sınıfa ya da sınıflara ait? Gerekçesiyle karar verin.
Çalışır kod görevi: İki kitap ve iki üye ekleyin. Aynı ISBN’i ya da aynı üye numarasını ikinci kez eklemeye çalışınca hata alındığını assert ve try/except ile doğrulayın. Ayrıca dışarıya verdiğiniz koleksiyona append() yapılamadığını ya da yapılan eklemenin iç koleksiyona yansımadığını gösterin.
Tek sayfa özet
- Bir nesne başka bir nesneyi niteliğinde tutabilir (
has-a). Python nesneyi kopyalamaz, aynı nesneye referans tutar. - Nesne koleksiyonu da nesnelerin referanslarını tutar. Listedeki ürünün fiyatı değişirse siparişin toplamı da değişir.
- Bir nesnenin tuttuğu liste ya da sözlük de o nesnenin durumudur. Kapsülleme kuralları onlar için de geçerlidir.
- İç listeyi olduğu gibi dışarı verirseniz dış kod, aliasing yüzünden sınıfın kontrollerini atlayabilir. Yalnız okunacaksa demet, çağıran düzenleyecekse kopya döndürün.
- Delegation, bir işi o işin kuralını bilen nesneye bırakmaktır. Kural tek sınıfta kalır.
- Bir sınıf öteki nesnelerin niteliklerini kendisi değiştirirse kurallar o sınıfta birikir. Her nesne kendi durumunu kendi metoduyla değiştirsin.
- Bire çok ilişkide liste mi sözlük mü kullanacağınızı, koleksiyona nasıl erişeceğiniz belirler. Numarasıyla bulunacak nesneler için sözlük kullanın.
- Başka bir sınıfın davranışından yararlanmanın tek yolu kalıtım (gelecek hafta) değildir. Kompozisyonla o sınıfın nesnesini tutup metodunu çağırabilirsiniz.
Bu bölümün kazanımları
Bu bölümü bitiren öğrenci:
- Bir nesnenin başka bir nesneyi tuttuğu
has-ailişkisini kurar. - Nesne koleksiyonunun nesnelerin kendisini değil referanslarını tuttuğunu belirler.
- Değiştirilebilir iç koleksiyonu dışarı vermenin kapsüllemeyi bozduğunu gösterir.
- İç listeyi dışarıya demet ya da kopya olarak verme seçeneklerini karşılaştırır.
- Davranışı kuralın sahibine bırakarak delegation uygular.
- Sorumluluğu yanlış sınıfa yığmanın o sınıfı “her şeyi yapan merkeze” dönüştürdüğünü açıklar.
- Bire çok ilişkide liste ile sözlük seçimini kullanım biçimine göre yapar.