İçeriğe geç
academia.sh

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.new NoMethodError verir. include modülü sınıfın hemen üstüne, Hesap ile Object arasına karıştırır; Hesap.include?(Raporlanir) doğru, Object.include?(Raporlanir) yanlıştır.
  • Karışımın yazanı atadır: hesap.rapor için kanıt Raporlanir çıktı; Hesap.instance_methods(false) ise boştur, yöntem sınıfa kopyalanmaz.
  • Comparable karışı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 ikinci include sırayı değiştirmez.
  • Modül içinde super zincirde 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; extend modü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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat