Ders 11 / 15
Temsilci Kalıbı
Zayıf tutulan temsilcinin bağlanmadığında ya da serbest bırakıldığında çağrının boşa düştüğünün ve ekranın bayat kaldığının, bağlanmamayla yöntemin yazmamasının aynı sonucu verdiğinin ölçümü.
Yayın Güncelleme
Bu derste
- Temsilcinin bir nesnenin işini başka bir nesneye devrettiği bir protokol ilişkisi olduğunu ayırır
- Zayıf referansın nesne serbest kalınca nil olduğunu ve çağrının boşa düştüğünü gerçekten ölçer
- Bağlı olmayan temsilcinin bıraktığı ekranı bayat olarak sayar
- Bağlanmamayla yöntemin hiçbir şey yazmamasının aynı bayat sonucu verdiğini doğrular
İçindekiler
Önceki derste, Koleksiyon Görünümleri, bir görünümün veriyi ve davranışı kendisi tutmayıp başka bir nesneye danıştığını gördük. O danışma ilişkisinin adı temsilci (delegate) kalıbıdır. Bir nesne, işin bir kısmını bir protokole uyan başka bir nesneye devreder: seçim yapıldığında, kayıt bittiğinde ya da bir satıra dokunulduğunda temsilcinin yöntemi çağrılır. Temsilci bağlı değilse çağrı hiçbir yere gitmez, ve bu sessiz bir kopmadır.
Ölçüm çekirdek dosyayla yapılır.
// Sync.swift — ekranda gosterilen her alani modelin o anki degeriyle karsilastirir
final class Screen {
let name: String
private(set) var shown: [String: String] = [:]
init(_ name: String) { self.name = name }
func set(_ key: String, _ value: String) { shown[key] = value }
func clear(_ key: String) { shown[key] = nil }
}
func verdict(model: String?, shown: String?) -> String {
switch (model, shown) {
case (nil, nil): return "olcum-disi"
case (nil, _?): return "artik"
case (_?, nil): return "eksik"
case let (wanted?, displayed?): return wanted == displayed ? "uyumlu" : "bayat"
}
}
func check(_ label: String, model: [String: String], screen: Screen, keys: [String]) -> [String] {
keys.map { key in
let result = verdict(model: model[key], shown: screen.shown[key])
print("\(label) | \(screen.name).\(key): model=\(model[key] ?? "-") ekran=\(screen.shown[key] ?? "-") -> \(result)")
return result
}
}
func countVerdicts(_ verdicts: [String]) {
for name in ["uyumlu", "bayat", "eksik", "artik"] {
print("\(name): \(verdicts.filter { $0 == name }.count)")
}
}
Çekirdek, ekranda gösterilen her alanı modelin o anki değeriyle karşılaştırır. Bu derste ayrıca, bir seçici ile onu dinleyen temsilcilerden oluşan saf Swift kodu ölçülür. Bu kod platform çerçevesi içermez, o yüzden gerçekten koşar. Model, kodun ekrana ne yazdığını kurar; çerçevenin kendisi değildir.
Zayıf Bağlanan Temsilci
Bir ödeme ekranında teslimat adresini bir seçici verir. Seçici adres seçildiğinde temsilciye haber verir; ekranın görevi bu adresi göstermektir. Temsilci seçicide zayıf referansla (weak) tutulur: seçici, ekrana sahip çıkarsa ekran da seçiciye sahip çıkar ve ikisi birbirini serbest bırakmayan bir tutma döngüsüne (retain cycle) girer. Zayıf referans bunu engeller, ama bedeli vardır: temsilci serbest kalırsa referans nil olur.
// AddressPicker.swift — adres secici ve onu dinleyen temsilciler
protocol AddressPickerDelegate: AnyObject {
func picker(_ picker: AddressPicker, didSelect address: String)
}
final class AddressPicker {
weak var delegate: AddressPickerDelegate?
func choose(_ address: String) {
delegate?.picker(self, didSelect: address)
}
}
final class CheckoutScreen: AddressPickerDelegate {
let screen: Screen
init(_ screen: Screen) { self.screen = screen }
func picker(_ picker: AddressPicker, didSelect address: String) {
screen.set("address", address)
}
}
final class SilentCheckout: AddressPickerDelegate {
func picker(_ picker: AddressPicker, didSelect address: String) {}
}
choose(_:) temsilciyi isteğe bağlı zincirle çağırır: temsilci nil ise hiçbir şey olmaz ve hata da çıkmaz. CheckoutScreen seçilen adresi ekrana yazar. SilentCheckout ise protokole uyar ama yöntemi boştur. İlk ölçümde seçici önce bağlanmadan kullanılır, sonra bağlanır.
// main.swift — baglanmayan temsilci ekrani bayat birakir, baglaninca uyumlu olur
let screen = Screen("Checkout")
screen.set("address", "Ankara")
var model = ["address": "Ankara"]
let picker = AddressPicker()
let checkout = CheckoutScreen(screen)
let start = check("baslangic", model: model, screen: screen, keys: ["address"])
model["address"] = "Izmir"
picker.choose("Izmir")
let unwired = check("temsilci-yok", model: model, screen: screen, keys: ["address"])
picker.delegate = checkout
picker.choose("Izmir")
let wired = check("temsilci-bagli", model: model, screen: screen, keys: ["address"])
countVerdicts(start + unwired + wired)
baslangic | Checkout.address: model=Ankara ekran=Ankara -> uyumlu temsilci-yok | Checkout.address: model=Izmir ekran=Ankara -> bayat temsilci-bagli | Checkout.address: model=Izmir ekran=Izmir -> uyumlu uyumlu: 2 bayat: 1 eksik: 0 artik: 0
baslangic adımında uyumlu. temsilci-yok adımında model Izmir, ekran Ankara ve sonuç bayat: seçici adresi seçti, ama kimseye iletmedi. temsilci-bagli adımında aynı çağrı bu kez ekrana ulaştı ve satır uyumlu. Sayım iki uyumlu, bir bayat.
DZ1. Bu ölçüm temsilcinin yönteminin eşzamanlı çağrıldığını varsayar. Çağrının başka bir kuyrukta mı koştuğu ölçülmedi.
Serbest Kalan Temsilci
Bağlanmamış bir temsilci akla ilk gelen kopmadır. Daha sinsi olanı, bağlanmış temsilcinin serbest kalmasıdır. Ekran kapanır ya da başka bir nesne onu tutmayı bırakır; zayıf referans nil olur ve seçici, bağlıymış gibi çağrı yapmaya devam eder. Bu derste nil olma bir tahmin değil, gerçekten ölçülür: referans aşağıda iki kez okunur.
// main.swift — zayif referans temsilci serbest kalinca nil olur, cagri bosa duser
let screen = Screen("Checkout")
screen.set("address", "Ankara")
let model = ["address": "Bursa"]
let picker = AddressPicker()
var handler: CheckoutScreen? = CheckoutScreen(screen)
picker.delegate = handler
print("temsilci bagli: \(picker.delegate != nil)")
handler = nil
print("temsilci bagli: \(picker.delegate != nil)")
picker.choose("Bursa")
let released = check("temsilci-birakildi", model: model, screen: screen, keys: ["address"])
countVerdicts(released)
temsilci bagli: true temsilci bagli: false temsilci-birakildi | Checkout.address: model=Bursa ekran=Ankara -> bayat uyumlu: 0 bayat: 1 eksik: 0 artik: 0
İlk satır true, ikinci satır false. Temsilci serbest bırakıldığında zayıf referans kendiliğinden nil oldu; sonraki choose çağrısı hiçbir yere gitmedi. Ekran Ankara olarak kaldı, model Bursa ve sonuç bayat. Sayım bir bayat.
İlk iki satır modelin bir varsayımı değil, Swift’in zayıf referans davranışının gerçek sonucudur. Üçüncü satırdaki bayat ise bu iki satırın sonucudur: çağrı boşa düştüğü için ekrana yazılan bir şey olmadı.
DZ2. Bu ölçüm yalnız handler değişkenini nil yapmanın serbest bırakma olduğunu varsayar. Başka bir güçlü referansın nesneyi tutmaya devam ettiği durum ölçülmedi.
KAYNAK tarafta aynı bağ, bir görünüm denetleyicisinin bir tablo görünümünü kurarken kendini temsilci yapmasıyla kurulur.
import UIKit
final class CheckoutController: UIViewController, AddressPickerDelegate {
let picker = AddressPicker()
override func viewDidLoad() {
super.viewDidLoad()
picker.delegate = self
}
}
picker.delegate = self satırı unutulursa derleyici uyarmaz; yalnız ekran bayat kalır.
Protokol Tasarımı
Temsilci kalıbında bir nesne, kendi adına karar vermesi gereken bir noktada başka bir nesneye sorar ya da haber verir. Soran nesne temsilcinin (delegate) kim olduğunu bilmez; yalnız bir protokole uyduğunu bilir. Bu sayede sorunun sahibi ile cevabın sahibi birbirinden ayrılır. Hazır görünümlerin çoğu bu kalıpla çalışır: tablo görünümü, seçim denetleyicileri ve metin alanları kendi davranışlarını temsilcilerine bırakır.
İyi bir temsilci protokolü üç özellik taşır. Birincisi, yordam adı olayı bir fiil kalıbıyla anlatır: bir şey olacak (will), olmakta (did) ya da onay gerekiyor (should). İkincisi, ilk parametre olayı gönderen nesnedir; böylece tek bir temsilci birden çok nesneye hizmet edebilir ve hangisinden geldiğini ayırt eder. Üçüncüsü, protokol küçüktür: her yordam bir karar ya da bildirim taşır, protokolün kendisi bir sınıfın bütün davranışını tarif etmez.
import UIKit
protocol AddressPickerDelegate: AnyObject {
func addressPicker(_ picker: AddressPicker, didSelect address: String)
func addressPickerDidCancel(_ picker: AddressPicker)
}
Protokol AnyObject ile sınırlanmıştır, yani yalnız sınıflar uyabilir. Bunun nedeni bir sonraki başlıktadır.
Zayıf Referans
Temsilci tutan nesne, temsilciyi zayıf referansla (weak reference) saklar. Nedeni bellek döngüsüdür: ekran seçiciyi tutar, seçici de ekranı temsilci olarak tutarsa iki nesne birbirini bırakmaz ve ikisi de hiç serbest kalmaz. Zayıf referans nesneyi ayakta tutmaz; temsilci başka yerde yok olursa referans kendiliğinden nil olur.
import UIKit
final class AddressPicker {
weak var delegate: AddressPickerDelegate?
func choose(_ address: String) {
delegate?.addressPicker(self, didSelect: address)
}
}
weak yalnız sınıf türleri için geçerlidir; protokolün AnyObject ile sınırlanması bu yüzden gereklidir. Çağrı, temsilci nil ise sessizce atlanır; bu da bu dersin asıl tuzağıdır. Temsilci bağlanmadıysa seçici çalışır, hata vermez ve hiçbir şey olmaz.
Bağlanmayan Temsilci
Temsilcinin hiç atanmaması, kalıbın en yaygın hatasıdır. Çağrı iyimser zincirleme ile yapıldığı için çökme yoktur, uyarı yoktur; ekran yalnızca tepki vermez. Bu dersin birinci ve ikinci ölçümleri bunu gösterir. Temsilci atanmadan yapılan seçim ekrana ulaşmaz ve satır bayat kalır. Atandıktan sonra aynı seçim uyumlu sonuç verir. İkinci ölçüm ayrıca zayıf referansı gerçekten sınar: ekran serbest kaldığında seçicinin temsilci bağı nil olur ve sonraki seçimler yine kimseye ulaşmaz.
Temsilci mi Kapanış mı
Aynı iş çoğu zaman bir kapanışla da yapılabilir. Seçici, seçim tamamlandığında çağıracağı bir kapanışı özellik olarak alabilir. Tek bir olay için bu daha kısadır ve protokol yazmayı gerektirmez. Birden çok ilişkili olay varsa, örneğin seçim, iptal ve doğrulama sorusu, protokol daha okunaklıdır ve gönderen nesne parametre olarak geçtiği için tek temsilci birden çok nesneye hizmet edebilir.
Kapanışlar da bellek döngüsü doğurabilir: kapanış ekranı, ekran seçiciyi tutuyorsa döngü kurulur ve zayıf referans kapanışın içinde ayrıca yazılmalıdır. Temsilci kalıbında bu risk tek bir yerde, weak anahtar sözcüğünde toplanır.
Sık Yapılan Hatalar
- Temsilciyi güçlü referansla tutmak. Döngü oluşur ve iki nesne de hiç serbest kalmaz.
- Temsilciyi atamayı unutmak. Çağrı sessizce atlanır; çıktı yoktur.
- Protokolü
AnyObjectile sınırlamamak. Zayıf referans yazılamaz ve derleyici hata verir. - Gönderen nesneyi parametre olarak geçmemek. Tek temsilci birden çok nesneye hizmet verdiğinde hangisinin çağırdığı bilinmez.
- Temsilcide ağır iş yapmak. Çağrı seçicinin akışında koşar ve uzun işler ekranı bloke eder.
- Temsilciye veri sormak için kullanılan protokol ile bildirim için kullanılan protokolü tek yerde toplamak. Birincisi cevap bekler, ikincisi beklemez; bu ayrım isimlendirmede korunur.
Ölçülen Sonucun Yorumu
İlk ölçümde başlangıçta ekran modelle uyumluydu. Model Izmir olunca, temsilci bağlı olmadığı için ekran Ankara kaldı ve satır bayat çıktı. Temsilci bağlanınca aynı seçim ekrana ulaştı ve satır uyumlu oldu. İkinci ölçümde zayıf referans gerçekten ölçüldü: bağ önce true, ekran serbest kalınca false basıldı ve sonraki seçim kimseye ulaşmadığı için satır bayat çıktı. Üçüncü ölçümde iki farklı ekran aynı sonuca vardı: biri temsilci bağlanmadığı için, öbürü temsilci bağlı olduğu hâlde seçimi ekrana yazmadığı için bayat kaldı. Çıktı ikisini ayırt etmez.
Sonuç şudur: temsilci kalıbı hata üretmeden başarısız olabilir. Bağın kopması da, bağlı temsilcinin işini yapmaması da aynı sessiz görüntüyü verir ve bunu görmenin tek yolu çıktıyı modelle karşılaştırmaktır.
Pratikte iki yoklama yeterlidir. Birincisi, temsilcinin atandığı yerin tek ve açık olmasıdır: ekran kurulurken seçici oluşturulur ve temsilci hemen atanır, böylece atama arada kalmaz. İkincisi, temsilci çağrısının arkasına sessiz bir yol bırakmamaktır. Çağrının sonucu önemliyse, temsilci yoksa uyarı veren bir koşul eklenir ya da geliştirme sırasında bir kontrol ile durdurulur. Sessiz atlama, kalıbın rahat yanıdır ama ölçümün gösterdiği gibi bir hatanın görünmesini de engeller. Bu iki alışkanlık, ölçümde görülen bayat satırların büyük bölümünü daha yazım aşamasında önler; çünkü bağın kurulduğu yer tek olduğunda, bağın kopması ya da hiç kurulmaması da tek bir yerde aranır ve her ekranda ayrı ayrı izlenmez.
Sınırlayıcı Ölçüm
Bayat sonucunun iki ayrı sebebi olabilir: temsilci hiç bağlanmamış olabilir ya da bağlıdır ama yöntem bir şey yazmıyordur. Biri bağlantıyı unutmaktır, öteki yöntemin içini boş bırakmaktır. Kural kimin unuttuğunu okumaz. Model yalnız ekranda ne yazıldığına bakar.
// main.swift — baglanmama ile yontemin yazmamasi ayni bayat sonucunu verir
let model = ["address": "Bursa"]
let unwiredScreen = Screen("UnwiredCheckout")
let silentScreen = Screen("SilentCheckout")
unwiredScreen.set("address", "Ankara")
silentScreen.set("address", "Ankara")
let unwiredPicker = AddressPicker()
unwiredPicker.choose("Bursa")
let silentPicker = AddressPicker()
let silentDelegate = SilentCheckout()
silentPicker.delegate = silentDelegate
silentPicker.choose("Bursa")
let unwiredResult = check("baglanmadi", model: model, screen: unwiredScreen, keys: ["address"])
let silentResult = check("yontem-yazmadi", model: model, screen: silentScreen, keys: ["address"])
countVerdicts(unwiredResult + silentResult)
baglanmadi | UnwiredCheckout.address: model=Bursa ekran=Ankara -> bayat yontem-yazmadi | SilentCheckout.address: model=Bursa ekran=Ankara -> bayat uyumlu: 0 bayat: 2 eksik: 0 artik: 0
İki satır da bayat, sayım iki bayat. Biri hiç çağrılmadı, öteki çağrıldı ve hiçbir şey yazmadı; çıktı ikisini ayırt etmiyor. Bu bir sınırdır: sonucun düzeltilmesi için çağrının nerede koptuğunun ayrıca izlenmesi gerekir, model bunu söylemez.
DZ3. Bu ölçüm iki sebebi aynı model değeriyle karşılaştırır. Temsilcinin yöntemi çağrıldığında geçirilen argümanın doğruluğu ayrıca ölçülmedi.
Özet
- Temsilci, bir nesnenin işinin bir kısmını bir protokole uyan başka bir nesneye devrettiği kalıptır.
- Temsilci zayıf tutulur ki tutma döngüsü oluşmasın; temsilci serbest kalınca zayıf referans
nilolur ve çağrı boşa düşer. - Bağlanmamış ya da serbest bırakılmış temsilci ekranı bayat bırakır; bağlanınca ekran uyumlu olur.
- Zayıf referansın
nilolması bu derste gerçekten ölçüldü. - Bağlanmamayla yöntemin hiçbir şey yazmaması aynı bayat sonucunu verir; kural kimin unuttuğunu okumaz.
- Bu dersin uyumu: bayat.
Sonraki Adım
Temsilci nesneler arası bağı kurar; çizim ise bir görünümün kendi yüzeyine ne yazdığını belirler. Sıradaki derste draw(_:) ile çizilen bir görünümün, yeniden çizim istenmediğinde eski değeri göstermeye devam ettiği ölçülecek.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.