Ruby'de refinement, var olan bir sınıfa (örneğin String) metot ekleme ya da metodu değiştirme işini yalnızca belirli bir kapsamda geçerli kılan mekanizmadır. Değişikliği bir modülün içinde refine String do ... end ile tanımlar, kullanmak istediğiniz dosyada ya da sınıfta using Modul ile etkinleştirirsiniz. using satırının görmediği kod değişikliği de görmez; bu yüzden programın geri kalanı ve kullandığınız kütüphaneler etkilenmez.

Bu yazıda refinement'ların neden var olduğunu, kapsam kurallarını, metot arama sırasındaki yerini, super davranışını ve import_methods gibi yardımcıları Ruby 4.0.9'da çalıştırılmış örneklerle anlatıyoruz. Sınıfları yeniden açmanın temeli için Ruby class yazısına, modüller için Ruby module yazısına bakabilirsiniz.

Sorun: monkey patch'in etkisi küreseldir

Sorun: monkey patch'in etkisi küreseldir

Ruby'de sınıflar açıktır; class String yazıp yeni bir metot eklediğinizde bu metot programdaki bütün string'lerde görünür. Belgeler buna monkey patch diyor ve değişikliğin herkesi etkilediği için beklenmedik yan etkilere ve bozulmalara yol açabileceğini belirtiyor. İki farklı kütüphane aynı adla farklı işler yapan metotlar eklerse sonuncusu diğerini sessizce ezer.

ruby
class String
  def baslik = split.map { |k| k.capitalize(:turkic) }.join(" ")
end

p "merhaba dünya".baslik                         # çıktı: "Merhaba Dünya"
p String.instance_methods(false).include?(:baslik)  # çıktı: true

refine ve using ile yerel değişiklik

Aynı metodu bir refinement olarak tanımlayalım. Module#refine isimsiz bir modül oluşturur ve değişiklikler bu modülün içinde durur. using çağrılana kadar metot yoktur:

ruby
module MetinAraclari
  refine String do
    def baslik = split.map { |k| k.capitalize(:turkic) }.join(" ")
  end
end

"merhaba dünya".baslik
# NoMethodError: undefined method 'baslik' for an instance of String

using MetinAraclari

p "merhaba dünya".baslik    # çıktı: "Merhaba Dünya"
p "ırmak üzerinde".baslik   # çıktı: "Irmak Üzerinde"

Kapsam kuralları

Kapsam kuralları: using en üst seviyede, sınıf ya da modül gövdesinde çağrılabilir; metot içinde..., En üst seviyede çağrılırsa dosyanın sonuna kadar, sınıf gövdesinde çağrılırsa...

Refinement'lar sözcüksel (lexical) kapsamlıdır: hangi kodun refinement'ı göreceği, kodun çalıştığı ana göre değil, kaynak dosyada nerede yazıldığına göre belirlenir. Belgelerdeki kurallar özetle şöyle:

  • using en üst seviyede, sınıf ya da modül gövdesinde çağrılabilir; metot içinde çağrılamaz.
  • En üst seviyede çağrılırsa dosyanın sonuna kadar, sınıf gövdesinde çağrılırsa o class ... end bloğunun sonuna kadar geçerlidir.
  • using satırından önce yazılmış kod refinement'ı görmez.
  • Refinement'ın etkin olmadığı bir yerde tanımlanmış bir metodu çağırırsanız, o metodun içinde refinement etkin değildir.
  • Refinement'ın etkin olduğu yerde tanımlanmış bir metot, başka bir dosyadan çağrılsa bile refinement'ı görür.
  • Sınıf daha sonra yeniden açıldığında refinement kendiliğinden etkinleşmez.
ruby
module MetinAraclari
  refine String do
    def baslik = split.map { |k| k.capitalize(:turkic) }.join(" ")
  end
end

def disarida_tanimli(s) = s.baslik   # using'den önce yazıldı

using MetinAraclari

def iceride_tanimli(s) = s.baslik    # using'den sonra yazıldı

p iceride_tanimli("iyi günler")   # çıktı: "İyi Günler"
disarida_tanimli("iyi günler")
# NoMethodError: undefined method 'baslik' for an instance of String
p Module.used_modules             # çıktı: [MetinAraclari]

Sınıf gövdesinde using

using'i bir sınıfın içinde çağırdığınızda refinement yalnızca o sınıf tanımının geri kalanında geçerlidir. Metot içinde çağırmaya kalkarsanız Ruby çalışma anında hata verir:

ruby
module Yuzde
  refine Integer do
    def yuzde(oran) = self * oran / 100
  end
end

class Fiyat
  using Yuzde

  def self.indirim(tutar) = tutar - tutar.yuzde(15)
end

p Fiyat.indirim(200)   # çıktı: 170
200.yuzde(15)
# NoMethodError: undefined method 'yuzde' for an instance of Integer

def kur
  using Yuzde
end
kur
# RuntimeError: main.using is permitted only at toplevel

Metot arama sırası ve alt sınıf önceliği

Belgelere göre Ruby bir C nesnesinde metot ararken önce C'nin etkin refinement'larına (son etkinleştirilen önce), sonra C'ye prepend edilen modüllere, sonra C'nin kendisine, en son da include edilen modüllere bakar. Bulamazsa aynı adımları üst sınıfta tekrarlar. Bunun önemli bir sonucu var: alt sınıftaki metot, üst sınıfa yapılan refinement'tan önce gelir.

ruby
module Bolme
  refine Numeric do
    def /(other) = :refined
    def yarim = self / 2.0
  end
end

using Bolme
p 1 / 2      # çıktı: 0
p 7.yarim    # çıktı: 3.5

Numeric için tanımlanan / çalışmadı, çünkü Integer kendi / metoduna sahip ve Numeric'ten önce aranıyor. yarim ise Integer'da olmadığı için arama üst sınıfa çıktı ve refinement bulundu.

Refinement içinde super

Refinement içindeki bir metotta super çağırdığınızda, belgelere göre aynı yerde başka bir refinement etkin olsa bile asıl sınıftaki metot çağrılır. Bu, orijinal davranışı sarmayı kolaylaştırır. Module#refinements bir modülün tanımladığı refinement'ları, Refinement#target ise refinement'ın hangi sınıfa uygulandığını döndürür:

ruby
module Cevir
  refine Integer do
    def to_s(*args)
      "<" + super + ">"
    end
  end
end

using Cevir
p 42.to_s                    # çıktı: "<42>"
p 255.to_s(16)               # çıktı: "<ff>"
p Cevir.refinements          # çıktı: [#<refinement:Integer@Cevir>]
p Cevir.refinements[0].target  # çıktı: Integer
p Module.used_refinements    # çıktı: [#<refinement:Integer@Cevir>]

Ruby 3.3 NEWS dosyası Refinement#target'ı eski refined_class metodunun yerine getirdi, Ruby 3.4 NEWS dosyası ise refined_class'ın kaldırıldığını belirtiyor. Ruby 4.0'da target kullanın.

Birden fazla sınıfı aynı modülde inceltmek

Bir modül içinde birden fazla refine bloğu yazabilirsiniz; tek bir using hepsini etkinleştirir ve birbirlerini çağırabilirler. Belgelerdeki örneğe benzer bu kod, sayı, dizi ve hash için basit bir JSON benzeri çıktı üretiyor:

ruby
module ToJSON
  refine Integer do
    def to_json = to_s
  end

  refine Array do
    def to_json = "[" + map { |i| i.to_json }.join(",") + "]"
  end

  refine Hash do
    def to_json = "{" + map { |k, v| k.to_s.dump + ":" + v.to_json }.join(",") + "}"
  end
end

using ToJSON
puts [{1 => 2}, {3 => 4}].to_json   # çıktı: [{"1":2},{"3":4}]

Bu yalnızca kapsamı göstermek için bir örnek; gerçek JSON üretimi için standart kütüphaneyi kullanın, ayrıntılar Ruby JSON kullanımı yazısında.

import_methods ile hazır modülü aktarmak

Ruby 3.1 NEWS dosyasına göre refinement'ların içinde include ve prepend kullanımı kullanımdan kaldırıldı ve yerine import_methods geldi. Bu metot modüldeki metotları refinement'a kopyalar. Belgeler, kopyalama yapıldığı için yalnızca Ruby koduyla yazılmış metotların aktarılabileceğini belirtiyor:

ruby
module Girinti
  def girintile(seviye) = " " * seviye + self
end

module MetinEk
  refine String do
    import_methods Girinti
  end
end

using MetinEk
p "satır".girintile(4)   # çıktı: "    satır"

İçgözlem metotları ve sınırlar

Belgeler, Kernel#method ve Kernel#methods gibi içgözlem metotlarının refinement'ları dikkate almadığını ve bunun gelecekte değişebileceğini söylüyor. Ruby 4.0.9 ile yaptığımız denemede sonuç karışıktı: "a".methods listesi inceltilmiş metodu içermedi ve String.method_defined? false döndürdü; buna karşılık respond_to?, send ve method(...).call using etkin olan dosyada metodu buldu. Kodunuzun bu metotların sonucuna dayanması gerekiyorsa kendi sürümünüzde deneyin.

Monkey patch mi, refinement mı?

Monkey patch mi, refinement mı? Etki alanı, Çakışma riski, Metot içinde etkinleştirme, Alt sınıftaki aynı adlı metot
ÖzellikMonkey patch (class String ...)Refinement (refine + using)
Etki alanıBütün program ve kütüphanelerYalnızca using sonrası, dosya ya da sınıf sonuna kadar
Çakışma riskiYüksek, son tanım kazanırDüşük, kapsam dışında görünmez
Metot içinde etkinleştirmeGerekmezYapılamaz (RuntimeError)
Alt sınıftaki aynı adlı metotSınıf doğrudan değiştiği için ezilebilirAlt sınıfınki önce gelir
Ne zamanSahibi olduğunuz sınıflarÇekirdek veya başkasının sınıfına yerel ek

Belgeler sınıf ve modülleri yeniden açmanın yalnızca sahibi olduğunuz kod için yapılmasını öneriyor. Çekirdek sınıflara küçük bir kolaylık eklemek istediğinizde refinement, bu kolaylığı kendi dosyanızla sınırlı tutmanın yoludur.

Sık Sorulan Sorular

using'i bir if içinde yazabilir miyim?

Belgelere göre using bir metot olduğu için ancak çalıştığında etkili olur; çalışmayan bir if false dalındaki using refinement'ı etkinleştirmez. Okunabilirlik açısından using'i dosyanın başında ya da sınıfın en üstünde tutmak en anlaşılır yoldur.

Modülleri de inceltebilir miyim?

Evet, belgelere göre refinement'lar hem sınıfları hem modülleri değiştirebilir.

Bir refinement modülünü başka bir modüle include edersem ne olur?

Belgelere göre bir modül, include ettiği modülün refinement'larını devralır; using ile dış modülü etkinleştirdiğinizde içerdiği modüllerin refinement'ları da etkinleşir.

eval içinde using çalışır mı?

Evet. Kernel#eval'e verilen string içinde using çağrılabilir; refinement o string'in sonuna kadar geçerli olur.

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 modülleri · Ruby class ve nesneler · Ruby String metotları · encoding ve Türkçe karakter · Ruby 4.0 yenilikleri