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

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.
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ı: truerefine 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:
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ı

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:
usingen ü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 ... endbloğunun sonuna kadar geçerlidir. usingsatı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.
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:
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 toplevelMetot 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.
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.5Numeric 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:
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:
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:
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ı?

| Özellik | Monkey patch (class String ...) | Refinement (refine + using) |
|---|---|---|
| Etki alanı | Bütün program ve kütüphaneler | Yalnızca using sonrası, dosya ya da sınıf sonuna kadar |
| Çakışma riski | Yüksek, son tanım kazanır | Düşük, kapsam dışında görünmez |
| Metot içinde etkinleştirme | Gerekmez | Yapılamaz (RuntimeError) |
| Alt sınıftaki aynı adlı metot | Sınıf doğrudan değiştiği için ezilebilir | Alt sınıfınki önce gelir |
| Ne zaman | Sahibi 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