Ruby'de module iki iş görür: isimleri gruplayan bir ad alanı (namespace) ve birden fazla sınıfa ortak metot kazandıran bir mixin. Modülden nesne üretilmez. Bir modülü sınıfa include ile eklerseniz metotları o sınıfın nesnelerine instance metodu olarak gelir; extend ile eklerseniz metotlar tek bir nesneye (ya da sınıf nesnesine) eklenir; prepend ile eklerseniz modül sınıfın önüne geçer ve sınıfın kendi metodunu sarabilir.

Bu yazıda üç ekleme biçiminin ancestors listesini nasıl değiştirdiğini, included kancasını, module_function ile hem modül üzerinden hem mixin olarak kullanılan yardımcıları ve ad alanlarındaki sabit arama kuralını Ruby 4.0.9'da çalıştırılmış örneklerle gösteriyoruz. Sınıfların temeli için Ruby class ve nesneler yazısını okuyabilirsiniz.

Modül bir ad alanı olarak

Modül bir ad alanı olarak

Farklı kütüphaneler aynı ada sahip sınıflar tanımlayabilir. Her birini bir modülün içine koymak bu çakışmayı önler: Muhasebe::Fatura ile Kargo::Fatura birbirinden bağımsız iki sınıftır. İç içe tanımlanan bir sınıf, dıştaki modülün sabitlerini doğrudan görebilir.

ruby
module Muhasebe
  KDV = 20

  class Fatura
    def oran = KDV
  end

  module Rapor
    p Module.nesting   # çıktı: [Muhasebe::Rapor, Muhasebe]
  end
end

module Kargo
  class Fatura
  end
end

p Muhasebe::Fatura.new.oran              # çıktı: 20
p Muhasebe::Fatura == Kargo::Fatura      # çıktı: false
p Muhasebe::KDV                          # çıktı: 20
p Muhasebe.constants.sort                # çıktı: [:Fatura, :KDV, :Rapor]

module A::B yazımındaki tuzak

Dış modül zaten tanımlıysa module Muhasebe::Ozet biçiminde tek satırla iç modül açabilirsiniz; girinti azalır. Ancak belgeler sabit aramanın bu durumda farklı çalıştığını belirtiyor: Module.nesting listesinde yalnızca Muhasebe::Ozet yer alır, Muhasebe yer almaz. Bu yüzden dış modüldeki sabit bulunamaz:

ruby
module Muhasebe::Ozet
  p Module.nesting   # çıktı: [Muhasebe::Ozet]
  p KDV
  # NameError: uninitialized constant Muhasebe::Ozet::KDV
end

Çözüm ya iç içe module Muhasebe; module Ozet yazımını kullanmak ya da sabite tam yoluyla, Muhasebe::KDV diye erişmektir. En üst seviyedeki bir sabite ::KDV biçiminde ulaşılır.

include: modülü mixin olarak eklemek

include, modüldeki instance metotlarını sınıfın nesnelerine kazandırır. Modülün metodu, içinde bulunduğu nesnenin diğer metotlarını çağırabilir; aşağıda selam metodu her sınıfın kendi ad metodunu kullanıyor. Ruby'nin kendi Comparable ve Enumerable modülleri de bu şekilde çalışır.

ruby
module Selamlayan
  def selam
    "Merhaba, ben #{ad}"
  end
end

class Kisi
  include Selamlayan
  attr_reader :ad

  def initialize(ad)
    @ad = ad
  end
end

class Robot
  include Selamlayan
  def ad = "R2"
end

p Kisi.new("Deniz").selam               # çıktı: "Merhaba, ben Deniz"
p Robot.new.selam                       # çıktı: "Merhaba, ben R2"
p Kisi.ancestors                        # çıktı: [Kisi, Selamlayan, Object, Kernel, BasicObject]
p Kisi.include?(Selamlayan)             # çıktı: true
p Kisi.new("Deniz").is_a?(Selamlayan)   # çıktı: true

Selamlayan.new
# NoMethodError: undefined method 'new' for module Selamlayan
class Kisi
  include Robot
end
# TypeError: wrong argument type Class (expected Module)

ancestors çıktısında modül, sınıfın hemen arkasına yerleşmiş. Ruby bir metodu ararken önce Kisi'ye, bulamazsa Selamlayan'a, sonra Object'e bakar. Bu yüzden sınıfta aynı adlı bir metot varsa sınıfınki kazanır. Bir sınıfın başka bir sınıfa mixin olarak eklenemediğini de son hata gösteriyor.

Birden fazla include ve sıra

Ayrı satırlarda include A ve include B yazarsanız son eklenen modül sınıfa en yakın yere girer ve önce o aranır. Belgelere göre include A, B biçiminde tek çağrıda verilen modüller ise ters sırayla işlenir; sonuçta A öne geçer. Aynı modülü ikinci kez eklemek listeyi değiştirmez.

ruby
module A; def kim = "A"; end
module B; def kim = "B"; end

class X
  include A
  include B
end

class Y
  include A, B
end

p X.ancestors.first(3)   # çıktı: [X, B, A]
p X.new.kim              # çıktı: "B"
p Y.ancestors.first(3)   # çıktı: [Y, A, B]
p Y.new.kim              # çıktı: "A"

extend: metotları tek bir nesneye eklemek

extend, modülün instance metotlarını çağrıldığı nesnenin singleton sınıfına ekler. Bir sınıfın içinde extend Modul yazdığınızda nesne sınıfın kendisi olduğu için metotlar sınıf metodu olur. Sıradan bir nesneye uyguladığınızda ise yalnızca o nesne yeni yeteneği kazanır.

ruby
module Fabrika
  def ornek_olustur(*args)
    new(*args)
  end
end

class Nokta
  extend Fabrika
  def initialize(x, y)
    @x, @y = x, y
  end
end

p Nokta.ornek_olustur(1, 2).instance_variables  # çıktı: [:@x, :@y]
p Nokta.singleton_class.include?(Fabrika)       # çıktı: true
p Nokta.ancestors.include?(Fabrika)             # çıktı: false

module Konusan
  def konus = "Bu nesne konuşabiliyor"
end

a = Object.new
b = Object.new
a.extend(Konusan)
p a.konus                  # çıktı: "Bu nesne konuşabiliyor"
p b.respond_to?(:konus)    # çıktı: false

prepend: sınıfın önüne geçip metodu sarmak

prepend: sınıfın önüne geçip metodu sarmak: include M, extend M, prepend M, module_function

prepend ile eklenen modül ancestors listesinde sınıfın önüne yerleşir. Böylece aynı adlı metotta önce modülünki çalışır; modül super çağırarak sınıfın asıl metoduna geçer. Kayıt tutma, ölçüm ya da önbellek gibi bir metodun öncesine ve sonrasına iş eklemek için uygun bir yoldur. Aynı modülü include ile eklediğinizde sınıfın metodu kazanır ve modüldeki sarmalayıcı hiç çalışmaz:

ruby
module Kayitci
  def kaydet
    puts "önce: kayıt başlıyor"
    sonuc = super
    puts "sonra: kayıt bitti"
    sonuc
  end
end

class Belge
  prepend Kayitci

  def kaydet
    puts "belge diske yazıldı"
    :tamam
  end
end

class Not
  include Kayitci

  def kaydet
    puts "not kaydedildi"
    :tamam
  end
end

p Belge.new.kaydet
# çıktı: önce: kayıt başlıyor
# çıktı: belge diske yazıldı
# çıktı: sonra: kayıt bitti
# çıktı: :tamam
p Belge.ancestors.first(3)   # çıktı: [Kayitci, Belge, Object]
p Not.new.kaydet
# çıktı: not kaydedildi
# çıktı: :tamam
p Not.ancestors.first(3)     # çıktı: [Not, Kayitci, Object]
YöntemMetotlar nereye giderancestors içindeki yeriTipik kullanım
include MSınıfın nesnelerine (instance metodu)Sınıfın arkasındaOrtak davranış paylaşmak
extend MTek nesneye ya da sınıf nesnesineSınıfın listesinde görünmezSınıf metodu eklemek, tek nesneyi genişletmek
prepend MSınıfın nesnelerine, sınıftan önceSınıfın önündeVar olan metodu sarmak
module_functionModülün kendisine ve private kopya olarakDeğiştirmezYardımcı fonksiyonlar

included kancası ile sınıf metotları eklemek

Bir modül eklendiğinde Ruby modül üzerindeki included metodunu, eklendiği sınıfı argüman olarak vererek çağırır. extend için extended, prepend için prepended karşılıkları vardır. Yaygın bir kalıp, included içinde eklenen sınıfa ikinci bir modülü extend ederek tek bir include ile hem instance hem sınıf metotları kazandırmaktır:

ruby
module Izlenebilir
  def self.included(sinif)
    puts "#{self}, #{sinif} içine eklendi"
    sinif.extend(SinifMetotlari)
  end

  module SinifMetotlari
    def izlenen_alanlar = @alanlar ||= []
    def izle(*adlar) = izlenen_alanlar.concat(adlar)
  end

  def degisenler
    self.class.izlenen_alanlar
  end
end

class Makale
  include Izlenebilir      # çıktı: Izlenebilir, Makale içine eklendi
  izle :baslik, :govde
end

p Makale.izlenen_alanlar   # çıktı: [:baslik, :govde]
p Makale.new.degisenler    # çıktı: [:baslik, :govde]

module_function: hem Modul.metot hem mixin

Bazı modüller nesne durumu tutmayan yardımcı fonksiyonlar toplar. module_function, ardından gelen metotları modülün kendisi üzerinden çağrılabilir yapar ve modülü include eden sınıflara da bu metotların private bir kopyasını verir. Belgeler bunların kopya olduğunu vurgular: modüldeki instance metodunu sonradan değiştirirseniz modül fonksiyonu eski haliyle kalır.

ruby
module Para
  module_function

  def kurus_to_tl(kurus)
    format("%.2f TL", kurus / 100.0)
  end
end

p Para.kurus_to_tl(12_950)   # çıktı: "129.50 TL"

class Kasa
  include Para

  def goster(k)
    kurus_to_tl(k)
  end
end

p Kasa.new.goster(500)       # çıktı: "5.00 TL"
Kasa.new.kurus_to_tl(500)
# NoMethodError: private method 'kurus_to_tl' called for an instance of Kasa

Sık yapılan hatalar

Sık yapılan hatalar: include ile sınıf metodu beklemek, prepend yerine include ile sarmaya çalışmak, module A::B yazımında dış sabitleri beklemek, Modülden nesne üretmeye çalışmak
  • include ile sınıf metodu beklemek: include instance metodu ekler. Sinif.metot biçiminde çağırmak istiyorsanız extend kullanın.
  • prepend yerine include ile sarmaya çalışmak: Sınıfta aynı adlı metot varsa include edilen modülün metodu hiç çalışmaz, çünkü önce sınıfa bakılır.
  • module A::B yazımında dış sabitleri beklemek: Bu yazımda dış modülün sabitleri arama zincirinde yoktur, NameError alırsınız.
  • Modülden nesne üretmeye çalışmak: Modüllerin new metodu yoktur; nesne gerekiyorsa sınıf tanımlayın.
  • Sahibi olmadığınız modülü yeniden açmak: Belgeler bunun çakışmalara yol açabileceğini söylüyor. Kapsamı sınırlı değişiklik için refinements tercih edilebilir.

Sık Sorulan Sorular

Modül ile sınıf arasındaki fark nedir?

Her sınıf aynı zamanda bir modüldür, ama sınıftan nesne üretilebilir ve sınıf bir üst sınıftan miras alır. Modülden nesne üretilmez; buna karşılık modül başka sınıflara ve modüllere karıştırılabilir. Sınıflar başka bir sınıfa karıştırılamaz.

ancestors listesini neden kontrol etmeliyim?

Bir metodun hangi tanımının çalışacağı bu listedeki sıraya bağlıdır. Beklemediğiniz bir metot çalışıyorsa Sinif.ancestors çıktısına bakmak hangi modülün öne geçtiğini hemen gösterir.

Comparable ve Enumerable nasıl çalışıyor?

İkisi de birer mixin'dir: siz <=> ya da each gibi tek bir metot yazarsınız, modül onlarca metodu bunun üzerine kurar. Uygulamalı anlatım için kendi sınıfınızda Comparable ve Enumerable yazısına bakın.

Modül metodunu nesne üzerinden çağırınca neden private hatası alıyorum?

module_function ile tanımlanan metotların mixin kopyası private olur. Sınıfın içinden alıcı olmadan çağırabilirsiniz, nesne üzerinden çağıramazsınız. Görünürlük kuralları public, private ve protected yazısında anlatılıyor.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Ruby öğrenme rehberi · Ruby class ve nesneler · public, private, protected · Ruby refinements · Comparable ve Enumerable · Enumerable map, select, reduce