Ders 03 / 09
Modüller ve Karışımlar
Modülün sınıftan farkı; include ile modülü ata zincirine karıştırmak; Comparable ile <=>; aynı adlı iki modülde sıra; tek nesneye extend.
Yayın Güncelleme
Bu derste
- Bir modülün include ile sınıfın ata zincirine girdiğini ancestors ve include? ile gösterir
- Modülün örneği olmadığını ve new çağrısının NoMethodError verdiğini ölçer
- Aynı adı taşıyan iki modülde zincirdeki sıranın sonucu belirlediğini gösterir
- Tek bir nesneye extend ile modül karıştırmanın diğer nesneleri etkilemediğini ölçer
İçindekiler
Önceki ders Hesap sınıfında rapor yazılmadığı hâlde hesap.rapor çağrısının rapor: ece 100 verdiğini bıraktı. Zincirde Hesap ile Object arasında duran Raporlanir yöntemi taşıyor, ama bir modülün bir sınıfa yöntem vermesi tam olarak ne demek? Modül sınıf değilse zincire nasıl girer?
Bu dersin yazanı yine ata yazanıdır, ama atanın türü değişir: tanım bir üst sınıfta değil, karıştırılmış bir modülde durur. Ders önce modülü sınıftan ayırır, sonra karışımın zincirdeki yerini ve sırasını ölçer.
Karıştırılmış Modül Yazan Olur
Aşağıdaki dosya Raporlanir modülünü Hesap sınıfına include ile karıştırır. yazan.rb önceki derslerdeki kuraldır ve burada da aynen yüklenir.
# yazan.rb — bir yontemi kimin yazdigini siniflandiran yardimci
# Siniflandirma sirayla sorar: once alicinin zincirinde bir tanim var mi,
# sonra tanimi alicinin kendi sinifi mi tasiyor, sonra tanim nasil uretildi.
def yazan(alici, ad)
zincir = alici.singleton_class
unless zincir.method_defined?(ad) || zincir.private_method_defined?(ad)
return ["kimse", "tanim yok"]
end
tanim = zincir.instance_method(ad)
sahip = tanim.owner
yer = sahip == zincir ? "tekil sinif" : sahip.to_s
return ["ata", yer] unless sahip == alici.class || sahip == zincir
iz = RubyVM::InstructionSequence.of(tanim)
return ["kod", "#{yer}, ruby govdesi yok"] if iz.nil?
return ["kod", "#{yer}, blok govdesi"] if iz.label.start_with?("block")
["programci", "#{yer}, def govdesi"]
end
# karisim.rb — include ile karistirilan modulun yazani
require_relative "yazan"
module Raporlanir
def rapor = "rapor: #{@sahip} #{@bakiye}"
end
class Hesap
include Raporlanir
def initialize(sahip, bakiye)
@sahip = sahip
@bakiye = bakiye
end
end
hesap = Hesap.new("ece", 100)
puts hesap.rapor
p yazan(hesap, :rapor)
p Hesap.include?(Raporlanir)
p Object.include?(Raporlanir)
p Hesap.ancestors
rapor: ece 100 ["ata", "Raporlanir"] true false [Hesap, Raporlanir, Object, Kernel, BasicObject]
yazan ata yazanını ve kanıt olarak Raporlanir’i verdi: tanım Hesap’ın gövdesinde değil, modülde yazılı. Hesap.include?(Raporlanir) doğru, Object.include?(Raporlanir) yanlış çıktı; karışım yalnız include çağrısını alan sınıfı etkiler. Hesap.ancestors listesinde Raporlanir, Hesap ile Object arasında durur. Bir sınıfa modül karıştırmak, modülü o sınıfın hemen üstüne yerleştirmektir.
SM7. Bu derste “karıştırmak”, include çağrısıyla bir modülü bir sınıfın ata zincirine, sınıfın hemen üstüne yerleştirmektir. Bunu yapan kod satırı include Raporlanir, sonuç ise zincirdeki yeni yerdir. Karışımın yazanı, kuralın diliyle ata çıkar, çünkü tanım alıcının sınıfında değil zincirin üst kısmındadır.
Karışımın zincire bir halka eklediği, yöntemleri sınıfa kopyalamadığı iki ek soruyla görülür. include bir ifadedir ve bir değer döndürür; instance_methods(false) sınıfın kendi gövdesini sorar. Aşağıdaki dosya ikisini ölçer.
# kopya_yok.rb — include zincire halka ekler, yontemi kopyalamaz module Raporlanir def rapor = "rapor" end class Hesap p(include(Raporlanir)) end p Hesap.instance_methods(false) p Raporlanir.instance_methods p Hesap.instance_method(:rapor).owner p Hesap.included_modules.take(2)
Hesap [] [:rapor] Raporlanir [Raporlanir, Kernel]
include çağrısı alıcıyı, yani Hesap sınıfını döndürdü. Hesap.instance_methods(false) boş çıktı: rapor sınıfın gövdesine taşınmadı, modülde kaldı. Raporlanir.instance_methods yöntemi modülün kendi listesinde gösterdi ve instance_method(:rapor).owner sahibin Raporlanir olduğunu doğruladı. included_modules zincirdeki modülleri Raporlanir, Kernel sırasıyla verdi. Yani önceki dersteki boş alt sınıfla aynı durum geçerlidir: çağrılabilir olmak ile sınıfa yazılı olmak ayrı şeylerdir, ve modül yazılı olanın yerini değiştirmez, zincirin uzunluğunu değiştirir.
Modül Sınıf Değildir
Karışım modülü sınıfa benzetir ama ikisi aynı şey değildir. Bir modülden nesne üretilemez; modül yalnız başkalarının zincirine girmek için vardır. Aşağıdaki dosya bunu iki soruyla ölçer.
# modul_sinif_degil.rb — modulun ornegi olmaz module Raporlanir def rapor = "rapor" end p Raporlanir.instance_of?(Class) p Raporlanir.instance_of?(Module) begin Raporlanir.new rescue NoMethodError => e puts e.class end
false true NoMethodError
Raporlanir bir Module örneğidir, Class örneği değildir ve new yöntemi onda tanımlı değildir. Bu fark karışımın neden gerekli olduğunu da açıklar: kalıtım yalnız bir üst sınıf kabul eder, oysa bir sınıfa istenen sayıda modül karıştırılabilir. Birbirinden bağımsız davranışlar, örneğin raporlama ile karşılaştırma, tek bir üst sınıf zincirine sığdırılmaya çalışılmaz; ayrı modüllerde yazılır ve ihtiyaç duyan sınıfa karıştırılır.
Çekirdekten Bir Karışım: Comparable
Karışım yalnız kendi yazdığımız modüllerle sınırlı değildir. Çekirdekteki Comparable modülü, sınıfa yalnız <=> yazıldığında karşılaştırma yöntemlerinin geri kalanını verir. Aşağıdaki dosya küçük bir Puan sınıfı üzerinde bunu ölçer.
# karsilastir.rb — Comparable ile <=>
require_relative "yazan"
class Puan
include Comparable
attr_reader :deger
def initialize(deger)
@deger = deger
end
def <=>(diger) = deger <=> diger.deger
end
a = Puan.new(3)
b = Puan.new(5)
c = Puan.new(9)
p c.between?(a, c)
p b > a
p [c, a, b].min.deger
p yazan(a, :between?)
p yazan(a, :<=>)
true true 3 ["ata", "Comparable"] ["programci", "Puan, def govdesi"]
Puan içinde yalnız <=> yazılı, ama between? ve > çağrıları ile <=> üzerinden çalışan min çağrısı sonuç verdi. between? için yazan ata ve kanıt Comparable; <=> için programcı, çünkü onu Puan kendi gövdesinde yazdı. Aynı sınıfta iki yöntem, iki ayrı yazan: karışım sınıfın kendi tanımını silmez, yanına ekler.
Comparable yalnız between? değil, <=> üzerinden kurulan bütün karşılaştırmaları verir. Aşağıdaki dosya Puan sınıfında hiç yazılmayan iki davranışı daha ölçer: eşitlik ve aralığa sıkıştırma.
# karsilastir_ek.rb — Comparable uc yontemi daha saglar
require_relative "yazan"
class Puan
include Comparable
attr_reader :deger
def initialize(deger)
@deger = deger
end
def <=>(diger) = deger <=> diger.deger
end
a = Puan.new(3)
b = Puan.new(5)
p a == Puan.new(3)
p a.equal?(Puan.new(3))
p Puan.new(9).clamp(a, b).deger
p yazan(a, :==)
p Puan.ancestors.take(3)
true false 5 ["ata", "Comparable"] [Puan, Comparable, Object]
a == Puan.new(3) doğru verdi, çünkü Comparable#== iki nesneyi <=> sonucu sıfır mu diye sınar. Oysa equal? iki ayrı nesne olduklarını gösterip yanlış verdi: eşitlik değere göre, kimlik nesneye göre sorulur. clamp dokuz değerini beşe indirdi. yazan == için Comparable’ı gösterdi; BasicObject de bir == tanımlar, ama zincirde Comparable önce geldiği için o gölgelenir. Bu, aşağıda ölçülen sıra kuralının çekirdek örneğidir.
Aynı Ad İki Modülde
Bir sınıfa iki modül karıştırılırsa ve ikisi de aynı adı tanımlıyorsa hangisi kazanır? Zincirdeki sıra bunu belirler, ve karıştırma sırası zincirin sırasını belirler: sonradan karıştırılan, öncekinin önüne geçer.
# iki_modul.rb — aynı ad, iki modul module A1 def ad = "A1" end module B1 def ad = "B1" end class C1 include A1 include B1 end p C1.ancestors.take(3) puts C1.new.ad p C1.ancestors.count(B1)
[C1, B1, A1] B1 1
include A1 önce, include B1 sonra yazıldı ve zincir [C1, B1, A1] çıktı: son karıştırılan, alıcıya en yakın durur. ad çağrısı bu yüzden B1 verdi. ancestors.count(B1) bir verdi: modül zincirde tek halka olarak durur. Aynı modülü ikinci kez karıştırmak bu tabloyu değiştirir mi, aşağıdaki ölçüm sorar.
# tekrar_include.rb — ikinci include zinciri degistirmez module A1 def ad = "A1" end module B1 def ad = "B1" end class C1 include A1 include B1 include A1 end p C1.ancestors.take(3) puts C1.new.ad p C1.ancestors.count(A1)
[C1, B1, A1] B1 1
Sondaki ikinci include A1 zincirde bir şey kıpırdatmadı. A1 zaten zincirde olduğu için Ruby onu yeniden öne almadı, bu yüzden B1 kazanmaya devam etti ve ancestors.count(A1) bir verdi. Karışım zincirde bir modülü en çok bir kez barındırır; sırayı değiştirmenin yolu yeniden include etmek değildir.
SM8. Bu derste “zincirdeki sıra”, ancestors dizisinin soldan sağa okunuşudur ve yöntem araması bu sırayla yürür. İlk eşleşen tanım kazanır. Sonradan include edilen modül sınıfın hemen üstünde yer aldığı için, öncekini gölgeler.
Modüllerde super
Modül içindeki bir yöntem de super çağırabilir ve super o anki zincirde bir sonraki halkaya iner. Bu, modülün sınıftan önce mi sonra mı geldiğine göre iki ayrı sonuç verir. Aşağıdaki dosya iki modülü bir üst sınıfla ve alt sınıfla birleştirir.
# modul_super.rb — super zincirde modullerin icinden gecer module A2 def ad = "A2 " + super end module B2 def ad = "B2 " + super end class Temel def ad = "temel" end class C2 < Temel include A2 include B2 end class D2 < Temel include A2 def ad = "D2 " + super end puts C2.new.ad p C2.ancestors.take(4) puts D2.new.ad
B2 A2 temel [C2, B2, A2, Temel] D2 A2 temel
C2 için çağrı B2’ye girdi, super ile A2’ye, oradan Temel’e indi ve metin zincir sırasıyla birleşti. ancestors aynı yolu [C2, B2, A2, Temel] olarak gösterdi: super zincirin üzerinde yürümektir. D2’de sınıf kendi ad’ını yazdı; modül sınıfın altında kaldığı için önce D2 çalıştı, super ile A2’ye ve ondan Temel’e inildi. Önceki dersin super_method ölçümüyle birleştirildiğinde sonuç şudur: yeniden tanımlayan alt sınıf da, karışan modül de aynı zincirdeki halkalardır ve super ikisini ayırt etmez.
SM14. Bu derste “zincirde super”, super çağrısının ancestors sırasında bir sonraki halkaya inmesidir. Halkanın sınıf mı modül mü olduğu çağrıyı değiştirmez; yalnız sıra belirleyicidir.
Modüller de birbirine karıştırılabilir. Bir modülün zinciri, içine karıştırdığı modülleri taşır ve o modülü include eden sınıf hepsini birden devralır. Aşağıdaki dosya iki katmanlı bir karışımı ölçer.
# ic_ice.rb — modul icine karisan modul sinifa birlikte gelir module Ust def ust = "ust" end module Alt include Ust def alt = "alt" end class Kutu include Alt end p Alt.ancestors p Kutu.ancestors.take(3) p Kutu.new.ust p Kutu.include?(Ust)
[Alt, Ust] [Kutu, Alt, Ust] "ust" true
Alt.ancestors modülün kendi zincirini gösterdi ve Ust onda Alt’ın hemen arkasında durdu. Kutu yalnız Alt’ı karıştırdı, ama zincirine Ust de girdi; Kutu.new.ust bu yüzden çalıştı ve Kutu.include?(Ust) doğru verdi. Karışımın bir halkayı değil, modülün o anki zincirinin tamamını getirdiği buradan görülür. Bir sınıfın zincirini okurken modüllerin kendi karışımlarını da hesaba katmak gerekir.
Sınırlayıcı Ölçüm
İki sınır var. Birincisi, sınıf kendi rapor’unu yazarsa karışımın tanımı ne olur? İkincisi, modül sınıfın tamamına değil, yalnız tek bir nesneye karıştırılırsa yazan ve zincir nasıl görünür?
# sinirlayici.rb — sinif golgeler, extend tek nesneye karisir
require_relative "yazan"
module Raporlanir
def rapor = "rapor: #{@sahip} #{@bakiye}"
end
class Hesap2
include Raporlanir
def initialize(sahip, bakiye)
@sahip = sahip
@bakiye = bakiye
end
def rapor = "hesap2 raporu"
end
class Hesap3
def initialize(sahip, bakiye)
@sahip = sahip
@bakiye = bakiye
end
end
p yazan(Hesap2.new("ece", 100), :rapor)
tek = Hesap3.new("can", 50)
tek.extend(Raporlanir)
puts tek.rapor
p yazan(tek, :rapor)
p Hesap3.new("a", 1).respond_to?(:rapor)
["programci", "Hesap2, def govdesi"] rapor: can 50 ["ata", "Raporlanir"] false
Hesap2 kendi rapor’unu yazınca kural programcı verdi: sınıfın kendi tanımı, karıştırdığı modülden önce gelir ve onu gölgeler, Raporlanir#rapor zincirde durmaya devam eder ve yalnız super çağrısıyla ulaşılabilir. tek.extend(Raporlanir) ise modülü yalnız tek nesnesine karıştırdı; o nesne rapor: can 50 yanıtı verdi, ata yazanı Raporlanir çıktı, ama aynı sınıftan yeni üretilen bir nesne respond_to?(:rapor) sorusuna yanlış dedi. Bu nesnenin zinciri sınıfından farklı olmuştur; o zincir nereye yerleşir sorusu ilerleyen derslerde tekil sınıf adıyla ele alınacak.
extend’in nesneye ne yaptığını include’la aynı sorularla ölçmek mümkün: nesne modülün örneği sayılır mı, sınıfın diğer nesneleri sayılır mı ve nesnenin kendine özgü zincirinde modül nerede durur?
# extend_olcum.rb — extend yalniz o nesnenin zincirine halka ekler module Raporlanir def rapor = "rapor" end class Hesap3 end tek = Hesap3.new oteki = Hesap3.new tek.extend(Raporlanir) p tek.is_a?(Raporlanir) p oteki.is_a?(Raporlanir) p tek.singleton_class.include?(Raporlanir) p tek.singleton_class.ancestors.drop(1).take(2) p Hesap3.include?(Raporlanir) p oteki.singleton_class.ancestors.drop(1).take(2)
true false true [Raporlanir, Hesap3] false [Hesap3, Object]
tek modülün örneği gibi davrandı, oteki davranmadı ve Hesap3 sınıfı modülü hiç görmedi. Nesnenin kendi zincirinin başındaki halkadan sonra Raporlanir, ardından Hesap3 geldi; oteki için aynı yerde doğrudan Hesap3 ve Object var. Yani extend modülü sınıfın zincirine değil, o tek nesneye özgü zincire ekler. Bu zinciri tutan yapının adı tekil sınıftır ve sonraki derslerde ayrıca incelenecek.
Karışım ile Kalıtım Arasında Seçim
Modül ile üst sınıf arasındaki seçim, ilişkinin türüne bağlıdır. Vadeli bir Hesap’tır: aynı türün özelleşmiş bir hâli. Hesap ise raporlanabilir bir şeydir: türün bir yeteneği. Yetenekler farklı türlerde tekrar eder, örneğin hesap da fatura da raporlanabilir, ama aynı üst sınıfı paylaşmak zorunda değildir. Yeteneği modülde yazmak, onu iki ayrı kalıtım ağacına tek yerde yazılmış olarak vermenin yoludur.
Bir kısıt da var: Ruby’de bir sınıfın tek üst sınıfı olur ama istenen kadar karışımı olabilir. Zincir bu yüzden her zaman doğrusal kalır. Modüller zincire sıralı halkalar olarak girer ve ölçümde görüldüğü gibi bu sıra tahmin edilebilir: son karıştırılan başa gelir. Birden çok üst sınıf olsaydı zincirin hangi yoldan yürüyeceği ayrı bir kural gerektirirdi; tek zincir bunu gereksiz kılar.
Karışımın bir bedeli de yazanı dağıtmasıdır. Bir sınıfın yönteminin hangi modülden geldiğini görmek için sınıfın gövdesi yetmez, ancestors ve yazan gibi ölçümlere ihtiyaç duyulur. Bu ders boyunca kullanılan yazan.rb’nin işi tam olarak budur: tanımın yerini, sınıfın gövdesine bakmadan, çalışan nesneye sorarak bulmak.
Özet
- Modül bir
Moduleörneğidir,Classörneği değildir;Raporlanir.newNoMethodErrorverir.includemodülü sınıfın hemen üstüne,HesapileObjectarasına karıştırır;Hesap.include?(Raporlanir)doğru,Object.include?(Raporlanir)yanlıştır. - Karışımın yazanı atadır:
hesap.raporiçin kanıtRaporlanirçıktı;Hesap.instance_methods(false)ise boştur, yöntem sınıfa kopyalanmaz. Comparablekarışımında yalnız<=>yazılır;between?ve==ata yazanıdır,<=>programcı yazanıdır.- Aynı ad iki modülde varsa sonradan karıştırılan kazanır (
[C1, B1, A1]); aynı modül zincirde bir kez durur ve ikinciincludesırayı değiştirmez. - Modül içinde
superzincirde bir sonraki halkaya iner; modül içine karışan modül de sınıfa birlikte gelir (Kutu.include?(Ust)). - Sınıf kendi tanımını yazarsa karışımı gölgeler ve yazan programcı olur;
extendmodülü yalnız tek nesneye karıştırır.
Sonraki Adım
Hesap sınıfının komisyon adlı bir yöntemi var ve hesap.komisyon dışarıdan çağrılınca NoMethodError veriyor; yine de net yöntemi aynı nesnenin içinden onu çağırabiliyor. Yazan hâlâ programcı, ama kim çağırabiliyor ve neden bazı çağrılar reddediliyor? Sonraki ders, Görünürlük, bu soruyu ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.