Ders 02 / 19
Koordinatör Kalıbı
Gezinmenin görünümlerden ayrı bir koordinatöre verilmesi ve ödemeden sonra yanlış ekranı açan bir kusura iki denetimin farklı yanıt vermesi.
Yayın Güncelleme
Bu derste
- Gezinme kararının görünüm denetleyicisinden koordinatöre taşınmasının neyi değiştirdiğini söyler
- Görünüm modelinin durumuna bakan denetimin gezinme kusuruna karşı kör kaldığını ölçer
- Rota dizisine bakan denetimin aynı kusuru yakaladığını gösterir
- Kusurun ara adımda olduğu durumda yalnız son ekrana bakan denetimin neden yetmediğini açıklar
İçindekiler
Birinci derste hesabı ekrandan ayırdık ve altı denetimin dört ayrımı verdiğini gördük. Hesap ayrıldıktan sonra ekranda başka bir sorumluluk kalır: bir sonraki ekranın hangisi olduğuna karar vermek. Sepet ekranı adres ekranını açar, adres ekranı ödeme ekranını açar, ödeme ekranı onay ekranını açar. Her görünüm denetleyicisi bir sonrakini tanıyorsa akış tek bir yerde okunamaz, ve akışın bir halkasını değiştirmek üç ekrana dokunur.
Koordinatör kalıbı bu kararı ekranlardan alıp ayrı bir nesneye verir. Ekran yalnız “ödeme bitti” der; sıradaki ekranı koordinatör seçer. Bu dersin sorusu kursun sorusudur: bu denetim doğru sürümü kusurlu sürümden ayırıyor mu? Kusur şudur: ödeme tamamlanınca koordinatör onay ekranı yerine kullanıcıyı sepete geri döndürüyor. Çekirdek birinci dersteki gibi birebir gelir.
// Separation.swift — bir denetimin dogru surumu kusurlu surumden ayirip ayirmadigini siniflandirir
struct Contrast<Output: CustomStringConvertible> {
let label: String
let correct: Output
let faulty: Output
let passesCorrect: Bool
let passesFaulty: Bool
var separation: String {
switch (passesCorrect, passesFaulty) {
case (true, false): return "ayiran"
case (true, true): return "kor"
case (false, false): return "gurultulu"
case (false, true): return "ters"
}
}
func describe() -> String {
let onCorrect = passesCorrect ? "gecti" : "kaldi"
let onFaulty = passesFaulty ? "gecti" : "kaldi"
print("\(label) | dogru=\(correct)/\(onCorrect) kusurlu=\(faulty)/\(onFaulty) -> \(separation)")
return separation
}
}
func contrast<Output>(_ label: String, correct: Output, faulty: Output, check: (Output) -> Bool) -> Contrast<Output> {
Contrast(label: label, correct: correct, faulty: faulty, passesCorrect: check(correct), passesFaulty: check(faulty))
}
func countSeparations(_ results: [String]) {
for name in ["ayiran", "kor", "gurultulu", "ters"] {
print("\(name): \(results.filter { $0 == name }.count)")
}
}
Çekirdek birinci dersteki çekirdeğin aynısıdır; bu derste yeniden tanımlanmaz, yalnız kullanılır. Ayrım adları değişmez: ayiran, kor, gurultulu, ters.
Gezinmeyi Ekrandan Almak
Koordinatörsüz akışta gezinme kararı görünüm denetleyicisinin içindedir. Ödeme ekranı onay ekranını kendisi kurar ve kendisi açar.
import UIKit
// Gezinme ekranin icinde — KAYNAK
final class PaymentViewController: UIViewController {
func paymentDidFinish() {
let next = ConfirmationViewController()
navigationController?.pushViewController(next, animated: true)
}
}
Bu ekran onay ekranını tanır. Ödeme ekranını başka bir akışta kullanmak istediğinizde, örneğin aboneliğin yenilenmesinde, onay ekranı orada da açılır. Akışı denetlemek de ekrana bağlıdır: ödemeden sonra ne açıldığını görmek için bir gezinme denetleyicisi ve bir ödeme ekranı kurmak gerekir.
Koordinatörle aynı karar ayrı bir nesnede durur ve ekran yalnız bir olay bildirir.
import UIKit
// Gezinme koordinatorde — KAYNAK
final class CheckoutFlowCoordinator {
private let navigation: UINavigationController
init(navigation: UINavigationController) {
self.navigation = navigation
}
func paymentCompleted() {
navigation.pushViewController(ConfirmationViewController(), animated: true)
}
}
İki parça da çalıştırılmaz; gezinme denetleyicisi platformun yaşam döngüsüne bağlıdır ve ders için çıktı iddia etmez. İkisini ayıran şey yapıdır. İkincisinde ödeme ekranı onay ekranını bilmez; ekran ile akış arasındaki bağ bir olay adıdır. Akışın tamamı tek bir sınıfta okunur ve ödemeden sonra ne açıldığı tek bir işlevde görülür. Bu dersin ölçümü de o işleve bakar.
Koordinatörün Modeli
Platform gezinmesi ölçülmez, ama koordinatörün aldığı kararın kendisi saf mantıktır: bir olay gelir, rota dizisi değişir. Model bunu üç parçada kurar: rota adları, ödeme ekranının görünüm modeli ve rota dizisini tutan koordinatör. Kusur koordinatörün içindedir: backToCart ödemeden sonra diziyi sepete indirir. extraNotice ise sınırlayıcı ölçümde kullanılacak ikinci kusurdur. Checkout.swift bir yardımcı dosyadır ve çıktı vermez.
// Checkout.swift — odeme akisinin modeli: gorunum modeli ve koordinator
enum Route: String {
case cart, address, payment, notice, confirmation
}
enum Fault {
case none, backToCart, extraNotice
}
struct PaymentViewModel {
private(set) var done = false
mutating func complete() { done = true }
}
struct CheckoutCoordinator {
private(set) var path: [Route] = [.cart, .address, .payment]
let fault: Fault
mutating func paymentCompleted() {
switch fault {
case .none: path.append(.confirmation)
case .backToCart: path = [.cart]
case .extraNotice: path.append(contentsOf: [.notice, .confirmation])
}
}
}
func runCheckout(_ fault: Fault) -> (done: Bool, path: [Route]) {
var model = PaymentViewModel()
var coordinator = CheckoutCoordinator(fault: fault)
model.complete()
coordinator.paymentCompleted()
return (model.done, coordinator.path)
}
func trail(_ path: [Route]) -> String { path.map { $0.rawValue }.joined(separator: ">") }
func finalRoute(_ path: [Route]) -> String { path.last?.rawValue ?? "yok" }
runCheckout iki şeyi yan yana üretir: görünüm modelinin “ödeme tamamlandı” durumu ve koordinatörün rota dizisi. Görünüm modeli ödemenin bittiğini bilir, ama sonrasında hangi ekranın açıldığını bilmez; o koordinatörün işidir. trail diziyi okunur bir dizgiye çevirir, finalRoute yalnız son rotayı verir.
Dört denetim aynı iki sürüme uygulanır. Doğru sürüm runCheckout(.none), kusurlu sürüm runCheckout(.backToCart).
// main.swift — odeme sonrasi hangi ekran acilir
let good = runCheckout(.none)
let bad = runCheckout(.backToCart)
let paid = contrast("odeme-tamamlandi", correct: good.done, faulty: bad.done) { $0 == true }.describe()
let last = contrast("son-ekran", correct: finalRoute(good.path), faulty: finalRoute(bad.path)) { $0 == "confirmation" }.describe()
let first = contrast("rota-basi", correct: good.path.first?.rawValue ?? "yok", faulty: bad.path.first?.rawValue ?? "yok") { $0 == "cart" }.describe()
let whole = contrast("tum-rota", correct: trail(good.path), faulty: trail(bad.path)) { $0 == "cart>address>payment>confirmation" }.describe()
countSeparations([paid, last, first, whole])
odeme-tamamlandi | dogru=true/gecti kusurlu=true/gecti -> kor son-ekran | dogru=confirmation/gecti kusurlu=cart/kaldi -> ayiran rota-basi | dogru=cart/gecti kusurlu=cart/gecti -> kor tum-rota | dogru=cart>address>payment>confirmation/gecti kusurlu=cart/kaldi -> ayiran ayiran: 2 kor: 2 gurultulu: 0 ters: 0
Çıktı Swift 6.4’te sandbox’ta üç kez koşturuldu ve üçünde de aynıdır. Dört denetimin ikisi ayıran, ikisi kör çıktı.
odeme-tamamlandi görünüm modelinin durumuna bakar. Ödeme gerçekten tamamlandı, bu yüzden durum iki sürümde de true. Kusur koordinatörün seçtiği ekrandadır ve görünüm modeli bu karara hiç katılmaz. Denetim kördür ve koordinatörü ayırmanın bir sonucunu gösterir: ekranın değerine bakan bir test, gezinme kusurunu göremez, çünkü gezinme artık ekranın değeri değildir. Kusur başka bir nesneye taşındıysa denetim de o nesneye taşınmalıdır.
rota-basi de kördür. Kusurlu sürüm diziyi [.cart] yapar ve dizinin ilk ögesi yine cart’tır. Denetim doğru bir şeye bakıyor, ama kusurun dokunmadığı bir şeye.
son-ekran rota dizisinin son ögesine bakar. Doğru sürümde confirmation, kusurlu sürümde cart çıkar; ikincisi beklentiyi karşılamaz. Denetim ayıran olur. tum-rota dizinin tamamını beklenen rotayla karşılaştırır ve o da aynı şekilde ayırır. Bu ikisinin ortak yanı şudur: ikisi de koordinatörün ürettiği rotaya bakar. Gezinmeyi koordinatöre vermek bu yüzden yalnız tasarımı değil, denetimin hedefini de değiştirir.
Koordinatör Neyi Çözer, Neyi Çözmez
Koordinatörün kazancı üç yerde görünür. Birincisi okunabilirliktir: akışın tamamı tek bir sınıfta durur. Ekranların hiçbiri kendinden sonraki ekranı tanımaz, bu yüzden bir ekran başka bir akışta yeniden kullanılabilir. İkincisi değiştirilebilirliktir: ödemeden sonra onay ekranı yerine bir teşekkür ekranı açmak istendiğinde yalnız paymentCompleted değişir. Üçüncüsü denetlenebilirliktir ve bu dersin konusu odur: rota kararı saf bir mantığa indiğinde, aynı denetim iki sürüme ekran kurmadan uygulanır.
Bedeli de vardır. Koordinatör ekrana referans tutan bir nesnedir; ekran da koordinatöre geri bildirim yapar. İki taraf birbirini güçlü referansla tutarsa ikisi de serbest kalmaz; bu bir tutma döngüsüdür ve bellek sızıntısına yol açar. Çaresi, ekranın koordinatörü zayıf referansla tutmasıdır. Bellek tarafı Bellek Sızıntısı Tespiti dersinde işlenecek; burada yalnız şunu not edelim: koordinatörün modeli bu döngüyü göstermez, çünkü model ekran tutmaz. Bu, modelin bilerek yok saydığı bir şeydir.
İç içe akışlar da bir bedeldir. Ödeme akışı bir alt koordinatörse, onun bitişini üst koordinatöre bildirmesi gerekir. Bildirim kaybolursa üst koordinatör alt akışın bittiğini öğrenmez ve ekran açık kalır. Bu davranış da modelin dışındadır; model tek bir koordinatörün tek bir olayına bakar.
Rota Dizisi Neyi Garanti Etmez
Tüm rotaya bakan denetim bu derste en güçlü denetimdi, ama güçlü olması her şeyi yakalaması demek değildir. Rota dizisi doğruysa bile gerçek gezinme denetleyicisi başka bir ekran açabilir: koordinatör confirmation diye bir rota üretir, ama o rotaya karşılık gelen ekranı kuran kod yanlış ekranı döndürebilir. Rota dizisi karar katmanını, ekran kurma kodu uygulama katmanını sınar. İkisi arasındaki eşleme ayrı bir denetim ister ve o denetim platforma bağlı olduğu için KAYNAK kalır.
Bu, birinci dersteki sınırın aynısıdır: bir denetim yalnız kurulduğu kusuru yakalar. tum-rota denetimi ödemeden sonra yanlış rota açılan kusuru yakaladı, ama doğru rotaya yanlış ekranı bağlayan bir kusuru yakalamaz. O kusur için kurulacak denetim ekranın türüne bakmak zorundadır ve bu, ölçülemeyen katmandadır. Ders bu yüzden “gezinme doğrulandı” demez; “gezinme kararı doğrulandı” der.
Bir başka sınır, olayın kendisidir. Model paymentCompleted işlevinin çağrıldığını varsayar. Ödeme ekranı olayı hiç bildirmezse koordinatör çalışmaz, rota dizisi ödeme adımında kalır ve tum-rota denetimi kalır, yani ayıran olarak yakalar. Ama bu, kusurun yeri hakkında bir şey söylemez: denetim yalnız dizinin yanlış olduğunu bilir, yanlışlığın ekranda mı koordinatörde mi olduğunu bilmez. Neden sorusu denetimin işi değildir; kusurun yerini bulmak başka bir adımdır ve kaynak koda bakmayı gerektirir.
Varsayımlar
MM1. Rota dizisi koordinatörün gezinme denetleyicisine ileteceği ekranların sırasıdır. Gerçek bir gezinme denetleyicisinin dizisi bu dizi ile birebir aynı davranır mı sorusunu bu ders ölçmez; gezinme denetleyicisi KAYNAK kalır.
MM2. Ödeme sonrasında kullanıcının ne gördüğü, animasyonu ve geçiş süresi modele girmez. Model yalnız hangi rotanın açıldığını söyler.
MM3. Görünüm modelinin “ödeme tamamlandı” durumu ile koordinatörün olay işlevi aynı anda çağrılır. Gerçek uygulamada araya bir ağ yanıtı girebilir; o bağımlılık dördüncü derste ele alınır.
Sınırlayıcı Ölçüm
Aynı iki denetim, kusur başka bir yerde olduğunda başka yanıt verebilir. Bu kez kusur sondaki ekranı bozmaz, ara adımı bozar: ödemeden sonra onay ekranından önce gereksiz bir bildirim ekranı açılır. Doğru sürüm runCheckout(.none), kusurlu sürüm runCheckout(.extraNotice).
// main.swift — kusur ara adimda
let goodNotice = runCheckout(.none)
let badNotice = runCheckout(.extraNotice)
let lastOnly = contrast("son-ekran-bildirim", correct: finalRoute(goodNotice.path), faulty: finalRoute(badNotice.path)) { $0 == "confirmation" }.describe()
let wholePath = contrast("tum-rota-bildirim", correct: trail(goodNotice.path), faulty: trail(badNotice.path)) { $0 == "cart>address>payment>confirmation" }.describe()
countSeparations([lastOnly, wholePath])
son-ekran-bildirim | dogru=confirmation/gecti kusurlu=confirmation/gecti -> kor tum-rota-bildirim | dogru=cart>address>payment>confirmation/gecti kusurlu=cart>address>payment>notice>confirmation/kaldi -> ayiran ayiran: 1 kor: 1 gurultulu: 0 ters: 0
Üç koşumda çıktı aynıdır. Yukarıdaki ölçümde ayıran olan son-ekran denetimi, aynı mantıkla kurulmasına rağmen bu kusura karşı kördür: iki sürüm de confirmation ile biter. Kusuru gören tek denetim rotanın tamamına bakan denetimdir. Denetimin kendisi değişmedi, değişen kusurun yeridir. Bir akış testi yalnız akışın sonunu denetliyorsa, sonu doğru olan her akış yeşil geçer; aradaki gereksiz ya da eksik adımlar hiç görünmez.
Özet
- Koordinatör gezinme kararını görünüm denetleyicilerinden alıp tek bir nesneye verir; ekran yalnız olay bildirir.
- Kararın yeri değişince denetimin hedefi de değişir: gezinme kusurunu görünüm modelinin durumu değil, koordinatörün rota dizisi gösterir.
- Görünüm modelinin “ödeme tamamlandı” durumuna bakan denetim, kusurlu koordinatöre karşı kördür.
- Son ekrana ve tüm rotaya bakan denetimler aynı kusuru yakalar: dört denetimin ikisi ayıran, ikisi kör.
- Kusur ara adıma kayınca yalnız son ekrana bakan denetim kör kalır; tüm rotaya bakan denetim ayıran kalır.
- Bu dersin ayrımı: kör.
Sonraki Adım
Gezinmeyi ve hesabı ayırdık; geriye ekranın bağlı olduğu durum kalıyor. Üçüncü derste Tek Yönlü Veri Akışı, durumu tek bir yerde tutup değişikliğin yalnız bir eylemle gelmesini sağlayacak. Orada kusur bir indirgeyicide olacak: bir ürünü kaldırma eylemi, adedi azaltmak yerine kalemi silecek, ve aynı eylem dizisiyle sonuç durumu karşılaştıran denetim bu kez kusuru yakalayacak.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.