Ders 03 / 13
Denetleyiciler ve Filtreler
Denetleyici her istekte yeniden kurulur ve filtre eylemden önce koşar. Ders filtre listesinin sınıfa bağlı olduğunu, denetleyici örneğinin isteğe bağlı olduğunu ve bildirim mesajının (flash) yalnız sonraki isteğe taşındığını ölçer.
Yayın Güncelleme
Bu derste
- Filtrenin eylemden önce koştuğunu ve filtre listesinin sınıfa bağlı olduğunu ölçer
- Denetleyici örneğinin her istekte yeniden kurulduğunu ve örnek değişkeninin istekte kaldığını gösterir
- Kaydet, yönlendir, bildirimi göster akışında bildirim mesajının (flash) kalışını ölçer
- Aynı düzeneğin kullanılışa göre başka bir kalışa düşebildiğini gösterir
İçindekiler
Önceki ders, yönlendirmenin urunler#goster hedefini verdiğini ve yoldaki 7’nin yalnız o istekte okunduğunu gösterdi. Hedefi karşılayan sınıf **denetleyici (controller)**dir. Bu dersin sorusu iki parçalı: denetleyici nesnesi bir istekten ötekine yaşar mı, ve bir eylemin başında koşan filtre (filter) neyi hatırlar? Üçüncü bir soru ikisine bağlanır: ürünü kaydedip başka bir sayfaya yönlendirdikten sonra gösterilen bildirim mesajı (flash) neden bir kez görünür?
M10/K02’de bir değerin ömrünü dilin hangi kapsamda bitirdiği soruldu; o derleyicinin kararıydı. Burada ömrü dil değil istek döngüsü belirler: @urun Ruby’de nesne yaşadıkça yaşar, ama denetleyici her istekte yeniden kurulduğu için değer bu istekte kalır.
Çekirdeğin kalis.rb dosyası bir kez daha aşağıda verilir. Blok kaynaktır, çıktısı yoktur ve require_relative ile çekilir.
# kalis.rb — kucuk bir istek dongusu ve bir degerin hangi istege kadar kaldigini siniflandiran yardimci
# Flash: bu istekte yazilan deger yalniz bir sonraki istege tasinir.
class Flash
def initialize(gelen)
@gelen = gelen
@yeni = {}
end
def [](anahtar) = @yeni.fetch(anahtar) { @gelen[anahtar] }
def []=(anahtar, deger)
@yeni[anahtar] = deger
end
def birakilan = @yeni
end
# Her istek yeni bir baglam alir: params, flash ve istek deposu her istekte bostur.
class Baglam
attr_reader :uygulama, :params, :oturum, :flash, :istek_deposu
def initialize(uygulama, oturum)
@uygulama = uygulama
@params = {}
@oturum = oturum
@flash = Flash.new(oturum.delete(:flash) || {})
@istek_deposu = {}
end
end
# Uygulamanin deposu butun isteklerde ve butun istemcilerde aynidir;
# oturum istemciye gore secilir.
class Uygulama
attr_reader :depo
def initialize
@oturumlar = Hash.new { |h, istemci| h[istemci] = {} }
@depo = { urunler: {}, sema: { urunler: %i[ad fiyat] }, sablonlar: {},
kuyruk: [], onbellek: {}, sayac: 0 }
end
def istek(istemci)
baglam = Baglam.new(self, @oturumlar[istemci])
sonuc = yield baglam
baglam.oturum[:flash] = baglam.flash.birakilan unless baglam.flash.birakilan.empty?
sonuc
end
end
# Kural: istek 1'de (istemci A) deger yazilir; istek 2 ve 3 (A) ile istek 4 (B) okur.
# B goruyorsa kalici; A'nin ucuncu istegi goruyorsa oturum; yalniz ikinci istek
# goruyorsa sonraki; hicbiri gormuyorsa bu_istek.
def kalis(yaz, oku)
uygulama = Uygulama.new
ilk = uygulama.istek("A") { |b| yaz.call(b); oku.call(b) }
raise ArgumentError, "deger yazildigi istekte okunamadi" if ilk.nil?
gorulen = [["A", 2], ["A", 3], ["B", 4]].map do |istemci, sira|
uygulama.istek(istemci) { |b| oku.call(b) }.nil? ? "--" : "#{istemci}#{sira}"
end
tur = if gorulen[2] != "--" then "kalici"
elsif gorulen[1] != "--" then "oturum"
elsif gorulen[0] != "--" then "sonraki"
else "bu_istek"
end
[tur, ["A1", *gorulen].join(" "), ilk]
end
Filtre ve Sınıfa Bağlı Liste
Çekirdeğin denetleyicisi filtreyi once :urunu_bul satırıyla bildirir. Bu bir çağrıdır ve sınıfın bir listesine ad ekler; calistir eylemi çağırmadan önce o listedeki her adı sırayla çalıştırır. Aşağıdaki blok önce listenin kendisine bakar, sonra aynı sınıftan türeyen boş bir sınıfa, sonra da ürün yokken ve ürün varken eylemi koşturur.
# filtre.rb — filtre listesi ve filtrenin bulduğu urun
require_relative "kalis"
# Nesne-iliski esleyici: bir satir bir kayda, tablo bir modele karsilik gelir.
module Urun
def self.olustur(b, nitelikler)
hatalar = []
hatalar << "ad bos olamaz" if nitelikler[:ad].to_s.empty?
hatalar << "fiyat negatif olamaz" if nitelikler[:fiyat].to_i.negative?
return { hatalar: } unless hatalar.empty?
tablo = b.uygulama.depo[:urunler]
id = tablo.size + 1
tablo[id] = nitelikler.merge(id:)
{ id:, hatalar: }
end
# Ayni istekte ayni sorgu ikinci kez veritabanina gitmez.
def self.bul(b, id)
sorgular = (b.istek_deposu[:sorgular] ||= {})
sorgular.fetch([:urunler, id]) do
b.istek_deposu[:sorgu_sayisi] = b.istek_deposu.fetch(:sorgu_sayisi, 0) + 1
sorgular[[:urunler, id]] = b.uygulama.depo[:urunler][id]
end
end
end
# Denetleyici: her istekte yeni bir ornek; filtre eylemden once calisir.
class UrunlerDenetleyicisi
def self.once(*adlar) = (@filtreler ||= []).concat(adlar)
def self.filtreler = @filtreler || []
once :urunu_bul
def initialize(baglam)
@baglam = baglam
end
def calistir(eylem)
self.class.filtreler.each { |filtre| send(filtre) }
public_send(eylem)
end
def goster = @urun
private
def urunu_bul
@urun = Urun.bul(@baglam, @baglam.params[:id])
end
end
kalem = ->(b) { Urun.olustur(b, ad: "kalem", fiyat: 12) }
p UrunlerDenetleyicisi.filtreler
class Alt < UrunlerDenetleyicisi; end
p Alt.filtreler
uygulama = Uygulama.new
p uygulama.istek("A") { |b| b.params[:id] = 99; UrunlerDenetleyicisi.new(b).calistir(:goster) }
p uygulama.istek("A") { |b| kalem.call(b); b.params[:id] = 1; UrunlerDenetleyicisi.new(b).calistir(:goster) }
[:urunu_bul]
[]
nil
{ad: "kalem", fiyat: 12, id: 1}
İlk satır liste, urunu_bul adını taşıyor. İkinci satır şaşırtıcıdır: Alt aynı sınıftan türediği hâlde listesi boş. Filtre listesi sınıfın kendi örnek değişkeninde durur ve alt sınıfa geçmez; alt sınıf bu filtreleri kendisi bildirmelidir. ÇY13. Bu ölçüm tek bir bildirim biçimi içindir; filtre listesini sınıf düzeyinde bir sabitte tutan bir çerçeve alt sınıfa farklı davranabilir. Üçüncü satır, kimliği 99 olan ürün olmadığında eylemin nil verdiğini gösterir: filtre ürünü bulamadı, eylem boş bir örnek değişkeni döndürdü ve hiçbir hata çıkmadı. Dördüncü satırda ürün önce oluşturuldu ve filtre onu buldu. Eylem kimliğe hiç dokunmadı; ürünü hazırlayan filtredir. Bu işbölümü filtrenin asıl nedenidir: ürünü bulma satırı her eylemde yinelenmez, tek yerde durur ve eylem yalnız hazırlanmış değeri kullanır. Karşılığı bir bağımlılıktır; eylemi filtre koşmadan çağıran kod, goster’in nil verdiğini ancak sonuca bakınca fark eder. Önceki dersin yönlendirmesi kimliği params’a yazdığı için filtre ona güvenebilir, ama yönlendirme koşmamışsa filtre nil kimlikle arar ve ürün bulamaz.
Bu dersteki çekirdeğin modeli bir kolaylık yapar ve bir kez söylenmelidir: Urun.bul depoda saklanan nesnenin kendisini verir. Birçok nesne–tablo eşleyicisi (object-relational mapper, ORM) her okumada tablodan yeni bir nesne kurar. Bu fark ölçülen kalışı değiştirmez, çünkü kalış değerin hangi isteğe kadar okunabildiğine bakar, nesnenin aynı olup olmadığına değil.
Denetleyici Her İstekte Yeni
Önceki kurslarda bir nesnenin yaşadığı sürece değerlerini tuttuğunu gördük: Ruby’de nesne, ona ulaşan kod kaldığı sürece yaşar. Web’de buna bir ek gerekir. Denetleyici nesnesi yaşayabilir, ama çerçeve onu her istekte yeniden kurduğu için örnek değişkeni o istekte kalır. Aşağıdaki blok iki ardışık istekte denetleyiciyi kurar ve ikincisinde birincinin örneğiyle aynı olup olmadığına bakar.
# yeni_ornek.rb — denetleyici ornegi istekten istege tasinmaz
require_relative "kalis"
# Nesne-iliski esleyici: bir satir bir kayda, tablo bir modele karsilik gelir.
module Urun
def self.olustur(b, nitelikler)
hatalar = []
hatalar << "ad bos olamaz" if nitelikler[:ad].to_s.empty?
hatalar << "fiyat negatif olamaz" if nitelikler[:fiyat].to_i.negative?
return { hatalar: } unless hatalar.empty?
tablo = b.uygulama.depo[:urunler]
id = tablo.size + 1
tablo[id] = nitelikler.merge(id:)
{ id:, hatalar: }
end
# Ayni istekte ayni sorgu ikinci kez veritabanina gitmez.
def self.bul(b, id)
sorgular = (b.istek_deposu[:sorgular] ||= {})
sorgular.fetch([:urunler, id]) do
b.istek_deposu[:sorgu_sayisi] = b.istek_deposu.fetch(:sorgu_sayisi, 0) + 1
sorgular[[:urunler, id]] = b.uygulama.depo[:urunler][id]
end
end
end
# Denetleyici: her istekte yeni bir ornek; filtre eylemden once calisir.
class UrunlerDenetleyicisi
def self.once(*adlar) = (@filtreler ||= []).concat(adlar)
def self.filtreler = @filtreler || []
once :urunu_bul
def initialize(baglam)
@baglam = baglam
end
def calistir(eylem)
self.class.filtreler.each { |filtre| send(filtre) }
public_send(eylem)
end
def goster = @urun
private
def urunu_bul
@urun = Urun.bul(@baglam, @baglam.params[:id])
end
end
kalem = ->(b) { Urun.olustur(b, ad: "kalem", fiyat: 12) }
uygulama = Uygulama.new
uygulama.istek("A") { |b| kalem.call(b) }
birinci = nil
sonuc = uygulama.istek("A") do |b|
b.params[:id] = 1
birinci = UrunlerDenetleyicisi.new(b)
birinci.calistir(:goster)
end
p sonuc
sonuc = uygulama.istek("A") do |b|
ikinci = UrunlerDenetleyicisi.new(b)
[ikinci.equal?(birinci), ikinci.goster]
end
p sonuc
{ad: "kalem", fiyat: 12, id: 1}
[false, nil]
İlk istekte filtre ürünü bulup @urun’a yerleştirdi ve eylem onu verdi. İkinci istek yeni bir örnek kurdu: equal? yanıtı false, ve goster nil verdi, çünkü filtre bu örnekte koşmadı ve @urun hiç atanmadı. Birinci istekteki örnek hâlâ bellekteydi, ama ikinci istek ona ulaşmadı. Denetleyicinin örnek değişkenleri bu yüzden bu istek kalışındadır. Bir değeri sonraki isteğe taşımak gerekiyorsa denetleyicinin dışında bir yere yazılmalıdır.
ÇY14. Ölçüm, çekirdeğin her istekte yeni bir Baglam ve yeni bir örnek kurduğu varsayımına dayanır. Örneği yeniden kullanan bir sunucu, aynı sınıfı farklı bir kalışla çalıştırabilir.
Filtre Sırası ve Yarıda Kalan Zincir
Birden çok filtre bildirildiğinde sıra bildirim sırasıdır. Bir filtre hata verirse ne olacağı da çekirdeğin calistir’ının yapısından çıkar. Aşağıdaki sınıf bu dersin kendi örneğidir ve çekirdeğin UrunlerDenetleyicisi’ne eklenmez; aynı iki satırlık düzeneği, olay kaydı tutan bir sınıfta yeniden kurar.
# siralama.rb — filtre sirasi ve hata veren filtre
class Deneme
def self.once(*adlar) = (@filtreler ||= []).concat(adlar)
def self.filtreler = @filtreler || []
once :birinci, :ikinci
attr_reader :kayit
def initialize = @kayit = []
def calistir(eylem)
self.class.filtreler.each { |filtre| send(filtre) }
public_send(eylem)
end
def eylem = @kayit << :eylem
private
def birinci = @kayit << :birinci
def ikinci = raise(ArgumentError, "ikinci filtre durdu")
end
d = Deneme.new
begin
d.calistir(:eylem)
rescue ArgumentError => e
p e.class
end
p d.kayit
ArgumentError [:birinci]
Birinci filtre koştu, ikinci hata verdi ve eylem hiç çağrılmadı: kayıtta :eylem yok. Zincir, hatanın çıktığı noktada kesilir. Bu çekirdekte bir tasarım seçimidir, çünkü each hata yükselince durur ve public_send satırına hiç gelinmez. Gerçek bir çerçevede bir filtrenin yanıtı kendisinin vermesi ve zinciri durdurması da mümkündür; çekirdek bunu modellemez. ÇY15. Ölçüm, çekirdeğin each ile sıralı çalıştırma yaptığı ve hatayı yakalamadığı varsayımına dayanır.
Kaydet, Yönlendir, Bildirimi Göster
Bu dersin örneğinde bir formun gönderilmesi üç istekle izlenir. İlkinde ürün kaydedilir ve bildirim mesajı (flash) yazılır. Eylem sonra başka bir sayfaya yönlendirir. İkinci istekte yeni sayfa bildirimi okur ve gösterir. Üçüncü istekte sayfa yenilenir ve bildirim yoktur.
# bildirim.rb — kaydet, yonlendir, mesaji goster
require_relative "kalis"
# Nesne-iliski esleyici: bir satir bir kayda, tablo bir modele karsilik gelir.
module Urun
def self.olustur(b, nitelikler)
hatalar = []
hatalar << "ad bos olamaz" if nitelikler[:ad].to_s.empty?
hatalar << "fiyat negatif olamaz" if nitelikler[:fiyat].to_i.negative?
return { hatalar: } unless hatalar.empty?
tablo = b.uygulama.depo[:urunler]
id = tablo.size + 1
tablo[id] = nitelikler.merge(id:)
{ id:, hatalar: }
end
# Ayni istekte ayni sorgu ikinci kez veritabanina gitmez.
def self.bul(b, id)
sorgular = (b.istek_deposu[:sorgular] ||= {})
sorgular.fetch([:urunler, id]) do
b.istek_deposu[:sorgu_sayisi] = b.istek_deposu.fetch(:sorgu_sayisi, 0) + 1
sorgular[[:urunler, id]] = b.uygulama.depo[:urunler][id]
end
end
end
uygulama = Uygulama.new
p uygulama.istek("A") { |b| s = Urun.olustur(b, ad: "kalem", fiyat: 12); b.flash[:bildirim] = "urun kaydedildi"; s }
p uygulama.istek("A") { |b| b.flash[:bildirim] }
p uygulama.istek("A") { |b| b.flash[:bildirim] }
{id: 1, hatalar: []}
"urun kaydedildi"
nil
İlk istek kaydı oluşturdu ve kimliği 1 verdi; hatalar boş. İkinci istek bildirimi okudu: "urun kaydedildi". Üçüncü istek aynı anahtarı okudu ve nil aldı. Bildirim yazıldığı istekten bir sonrakine taşındı ve orada tüketildi. Çekirdeğin Baglam sınıfı her istekte oturumdaki :flash anahtarını silerek okur; bu yüzden bir bildirim bir kez okunabilir. Kaydın kendisi ise üç istekte de depoda durdu, çünkü ürün uygulama deposuna yazılmıştı.
Okunmayan ve Üzerine Yazılan Bildirim
İki durum daha sorulabilir: bildirim sonraki istekte okunmazsa üçüncü istekte hâlâ durur mu, ve aynı anahtar aynı istekte iki kez yazılırsa hangi değer taşınır?
# okunmayan.rb — okunmayan ve uzerine yazilan bildirim
require_relative "kalis"
uygulama = Uygulama.new
p uygulama.istek("A") { |b| b.flash[:bildirim] = "ilk"; b.flash[:bildirim] = "son"; b.flash[:bildirim] }
p uygulama.istek("A") { |b| b.params[:sayfa] = 1 }
p uygulama.istek("A") { |b| b.flash[:bildirim] }
"son" 1 nil
İlk istekte iki yazış oldu ve okuma son değeri verdi. İkinci istek bildirimi hiç okumadı, yalnız başka bir iş yaptı. Üçüncü istek nil aldı. Bildirimin yaşaması, okunmasına değil, bir isteğin başlamasına bağlıdır: her yeni Baglam oturumdaki bildirimi alıp siler. Okunmayan bildirim, okunsaydı gideceği yerden aynı anda gider. ÇY16. Bu ölçüm üç istek ve tek bir anahtar içindir.
Aynı düzeneği çekirdeğin sınıflandırıcısıyla ölçmek, bildirimin kalışını bir sözcükle adlandırır.
# denetleyiciler.rb — bildirimin kalisi
require_relative "kalis"
yaz = ->(b) { b.flash[:bildirim] = "urun kaydedildi" }
oku = ->(b) { b.flash[:bildirim] }
p kalis(yaz, oku)
["sonraki", "A1 A2 -- --", "urun kaydedildi"]
A1 A2 -- -- dizisi bildirimin yazıldığı istekte ve bir sonrakinde görüldüğünü, üçüncü istekte ve başka istemcide görülmediğini söyler. Kalış sonraki istektir: aynı istemcinin yalnız bir sonraki isteğinde okunur. Bu, çekirdeğin on üç gözleminden yalnız birinin sonucudur; öteki gözlemlerin hiçbiri bu kalışa düşmedi.
Sınırlayıcı Ölçüm
Bildirimin sonraki isteğe taşınması düzeneğin değil, kullanılışın sonucudur. Okuyan kod değeri her okuduğunda yeniden yazarsa, aynı düzenek değeri oturum boyunca taşır.
# sinir.rb — ayni duzenek baska bir kalisa dusebilir
require_relative "kalis"
yaz = ->(b) { b.flash[:sayac] = 1 }
oku = ->(b) { v = b.flash[:sayac]; b.flash[:sayac] = v if v; v }
p kalis(yaz, oku)
["oturum", "A1 A2 A3 --", 1]
Bildirim bu kez ["oturum", "A1 A2 A3 --", 1] olarak sınıflandı: aynı istemcinin üçüncü isteğinde de okundu, başka istemcide okunmadı. Değiştirilen şey düzenek değil, okuyan kodun davranışıdır. Okurken yeniden yazan her istek değeri bir sonrakine geçirir; zincir ancak bir istek yazmadan okuduğunda kopar. ÇY17. Bu ölçüm yalnız bir anahtar ve üç istek içindir; zincirin başka bir sunucuda ne kadar sürdüğü ölçülmedi.
Özet
- Filtre eylemden önce koşar ve eylemin kullanacağı değeri hazırlar; filtre listesi sınıfa bağlıdır ve alt sınıfa geçmez.
- Denetleyici her istekte yeni bir örnek olarak kurulur; örnek değişkeni yalnız o istekte kalır.
- Kaydet, yönlendir, göster akışında bildirim mesajı (flash) yazıldığı istekten bir sonrakine taşınır ve orada tüketilir.
- Bu dersin kalışı sonraki istektir: bildirim yalnız bir sonraki istekte okunur.
- Aynı düzenek kullanılışa göre başka kalışa düşer: okurken yeniden yazan kod bildirimi oturuma çevirir.
Sonraki Adım
Denetleyici ürünü hazırladı ve bildirimi taşıdı, ama çıktıyı üretmedi. Bir sonraki ders görünümü ve şablonu ölçer: bir şablon kaç kez derlenir, yerel değerler nasıl kaçışlanır ve derlenmiş şablon hangi kalışta durur. Bildirim mesajı (flash) bu derste sonraki isteğe taşındı; şablonun derlenmiş hâli ise isteğe değil, uygulamaya bağlı bir yerde duracak ve başka bir kalışa düşecek.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.