Ders 03 / 19
Tek Yönlü Veri Akışı
Durumun tek bir merkezî depoda tutulup yalnız eylem ve indirgeyiciyle değiştirilmesi ve aynı eylem dizisinin iki indirgeyiciye verilerek denetimlerin ayrım gücünün ölçülmesi.
Yayın Güncelleme
Bu derste
- Durum, eylem ve indirgeyicinin tek yönlü veri akışındaki rollerini söyler
- İndirgeyiciyi saf bir işlev olarak kurup aynı eylem dizisiyle iki sürüme uygular
- Sonuç durumunu karşılaştıran denetimin indirgeyici kusurunu yakaladığını ölçer
- Kayıtlı eylem ve durum örneğinin kusurlu indirgeyiciden alınmasının denetimi tersine çevirdiğini gösterir
İçindekiler
İkinci derste gezinmeyi ekrandan aldık ve denetimin hedefinin değiştiğini gördük. Şimdi ekranın bağlı olduğu duruma bakıyoruz. Bir ekranda sepet birkaç yerden değişebilir: ürün ekleme düğmesi, kaldırma düğmesi, bir ağ yanıtı, bir bildirim. Her yer durumu kendi yoluyla değiştirirse, bir değerin neden o olduğunu söylemek için bütün yolları gezmek gerekir.
Tek yönlü veri akışı bu yolları teke indirir. Durum tek bir merkezî depoda durur. Ekran durumu okur, bir şey olduğunda depoya bir eylem gönderir; depo eylemi bir indirgeyiciye verir ve indirgeyici eski durumdan yeni durumu üretir. Akış hep aynı yöndedir: durum, ekran, eylem, indirgeyici, yeni durum. Bu dersin kusuru indirgeyicidedir: “ürünü kaldır” eylemi adedi bir azaltmak yerine kalemi tamamen siler. Çekirdek önceki derslerdeki 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)")
}
}
Bu derste de çekirdek yalnız kullanılır. contrast aynı denetimi iki sürümün çıktısına uygular ve dört ayrımdan birini verir.
Durum, Eylem, İndirgeyici
Merkezî depo üç parçadan kurulur. Durum uygulamanın o anki verisidir: sepetteki kalemler ve adetleri. Eylem bir şeyin olduğunu söyleyen küçük bir değerdir: “kalem ekle”, “kalem kaldır”. İndirgeyici eski durum ile eylemi alıp yeni durumu veren işlevdir. İndirgeyici saftır: aynı durum ve aynı eylem her zaman aynı yeni durumu verir, dışarıya bir şey yazmaz.
Platforma bağlı kısım ince bir kabuktur. Depo durumu tutar, eylemi indirgeyiciye geçirir ve sonucu saklar; görünüm durumu okur ve eylem gönderir.
import SwiftUI
// Depo ve gorunum — KAYNAK
final class CartStore: ObservableObject {
@Published private(set) var state = CartState()
func send(_ action: CartAction) {
state = reduceCart(state, action)
}
}
struct CartView: View {
@ObservedObject var store: CartStore
var body: some View {
Button("Kalem ekle") { store.send(.add("kalem")) }
}
}
Bu parça çalıştırılmaz; görünüm ve yayımlanan özellik platforma bağlıdır ve ders için çıktı iddia etmez. Önemli olan, kabuğun hiçbir karar vermemesidir. send yalnız indirgeyiciyi çağırır, görünüm yalnız eylem gönderir. Bütün karar reduceCart işlevindedir ve o işlev saf Swift’tir. Ölçüm de o işleve bakar.
İndirgeyicinin Modeli
Model dört şey tanımlar: durum, eylem, doğru ve kusurlu indirgeyici ve bir eylem dizisini sırayla uygulayan replayActions. İndirgeyici tek bir işlevdir; faulty bayrağı kusurlu sürümü seçer. Kusur remove dalındadır: doğru sürüm adedi 1’den büyükse azaltır, yoksa kalemi siler; kusurlu sürüm her zaman siler. Reducer.swift bir yardımcı dosyadır ve çıktı vermez.
// Reducer.swift — sepet durumu, eylemler ve indirgeyici
struct CartItem: Equatable {
let sku: String
var quantity: Int
}
struct CartState: Equatable, CustomStringConvertible {
var items: [CartItem] = []
var description: String {
items.isEmpty ? "bos" : items.map { "\($0.sku)x\($0.quantity)" }.joined(separator: ",")
}
}
enum CartAction {
case add(String)
case remove(String)
}
func reduceCart(_ state: CartState, _ action: CartAction, faulty: Bool = false) -> CartState {
var next = state
switch action {
case .add(let sku):
if let index = next.items.firstIndex(where: { $0.sku == sku }) {
next.items[index].quantity += 1
} else {
next.items.append(CartItem(sku: sku, quantity: 1))
}
case .remove(let sku):
guard let index = next.items.firstIndex(where: { $0.sku == sku }) else { return next }
if !faulty && next.items[index].quantity > 1 {
next.items[index].quantity -= 1
} else {
next.items.remove(at: index)
}
}
return next
}
func replayActions(_ actions: [CartAction], faulty: Bool) -> CartState {
actions.reduce(CartState()) { reduceCart($0, $1, faulty: faulty) }
}
func quantity(of sku: String, in state: CartState) -> Int {
state.items.first(where: { $0.sku == sku })?.quantity ?? 0
}
Dört denetim, iki eylem dizisine uygulanır. Birinci dizi iki kez ekler, bir kez kaldırır: doğru sürümde kalem 1 adet kalmalı, kusurlu sürümde hiç kalmaz. İkinci dizi bir kez ekler, bir kez kaldırır: adedi 1 olan bir ürün, iki sürümde de tamamen silinir.
// main.swift — ayni eylem dizisi, iki indirgeyici
let twoThenRemove: [CartAction] = [.add("kalem"), .add("kalem"), .remove("kalem")]
let oneThenRemove: [CartAction] = [.add("kalem"), .remove("kalem")]
let goodState = replayActions(twoThenRemove, faulty: false)
let badState = replayActions(twoThenRemove, faulty: true)
let goodSingle = replayActions(oneThenRemove, faulty: false)
let badSingle = replayActions(oneThenRemove, faulty: true)
let sameState = contrast("durum-karsilastir", correct: goodState, faulty: badState) { $0 == CartState(items: [CartItem(sku: "kalem", quantity: 1)]) }.describe()
let leftover = contrast("kalan-adet", correct: quantity(of: "kalem", in: goodState), faulty: quantity(of: "kalem", in: badState)) { $0 == 1 }.describe()
let notNegative = contrast("adet-negatif-degil", correct: quantity(of: "kalem", in: goodState), faulty: quantity(of: "kalem", in: badState)) { $0 >= 0 }.describe()
let singleLine = contrast("bir-adetli-urun", correct: goodSingle.items.count, faulty: badSingle.items.count) { $0 == 0 }.describe()
countSeparations([sameState, leftover, notNegative, singleLine])
durum-karsilastir | dogru=kalemx1/gecti kusurlu=bos/kaldi -> ayiran kalan-adet | dogru=1/gecti kusurlu=0/kaldi -> ayiran adet-negatif-degil | dogru=1/gecti kusurlu=0/gecti -> kor bir-adetli-urun | dogru=0/gecti kusurlu=0/gecti -> kor 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ı.
durum-karsilastir aynı eylem dizisinden çıkan sonuç durumunu beklenen durumla karşılaştırır. Doğru sürüm kalemx1, kusurlu sürüm bos verir; ikincisi beklentiyi karşılamaz. kalan-adet aynı diziyi tek bir sayıya indirger: kalemin adedi 1 olmalıdır, kusurlu sürümde 0 çıkar. İkisi de ayıran olur. Ortak nedenleri şudur: ikisi de kusurun değiştirdiği şeye, yani indirgeyicinin ürettiği duruma bakar.
adet-negatif-degil kusurun bozmadığı bir özelliğe bakar. Kusurlu sürümde kalem silinir ve adet 0 okunur; 0 da sıfırdan küçük değildir. Denetim iki sürümde de geçer ve kördür. bir-adetli-urun ise kusurun ikinci dizide hiç görünmediğini gösterir: adedi 1 olan ürünü kaldırmak iki sürümde de ürünü siler, ve denetim satır sayısına bakarken ikisinin de 0 verdiğini görür. Denetim bu dizi için kördür, ve kördür çünkü girdi kusuru saklamaktadır.
Akışın Denetime Kattığı
Bu ölçümü kolaylaştıran akışın kendisidir. İndirgeyici saf olduğu için aynı eylem dizisi iki sürüme verilebilir ve çıkan iki durum birebir karşılaştırılabilir. Ekranın ya da ağın araya girmesi gerekmez. Durumu birçok yerden değiştiren bir uygulamada aynı karşılaştırma için her yolu ayrı ayrı sürmek gerekirdi.
Ayrıca durum tek bir değer olduğu için, denetimin neye bakacağı seçilebilir: durumun tamamı, bir alanı ya da türetilmiş bir sayı. Dört denetimin üçü aynı iki durumu farklı yollarla okudu; yalnız ikisi kusuru gördü. Denetimin ne kadar güçlü olduğu, durumun ne kadarını okuduğuyla ilgili olabilir. durum-karsilastir durumun tamamını okur ve bu yüzden kusurun yalnız bir alana değil, her alana yansıdığı durumlarda da ayıran kalır. kalan-adet yalnız bir alanı okur; kusur o alana dokunmasaydı kör olurdu.
Eylem dizisi de bir girdidir. İki dizi aynı kusura karşı farklı yanıt verdi: ikisinde de aynı denetim vardı ve yalnız dizi değişti. Bu, birinci dersteki yüzde 10 ve yüzde 15 örneğinin eylem düzeyindeki karşılığıdır. İyi bir indirgeyici testi birden çok dizi dener, ve özellikle sınır dizileri dener: boş sepet, tek adetli ürün, aynı ürünün ardışık eklenmesi.
Varsayımlar
MM1. Depo ve görünüm KAYNAK kalır. Gerçek bir depo durumu yayımlarken görünümün yeniden çizilmesi bu dersin ölçüsünde değildir; model yalnız indirgeyicinin ürettiği durumu karşılaştırır.
MM2. İndirgeyici saf kabul edilir: dışarıdan bir şey okumaz, dışarıya bir şey yazmaz. Gerçek bir uygulamada yan etkiler (ağ isteği, kayıt) indirgeyicinin dışında ayrı bir katmandadır ve bu ders onları modellemez.
MM3. Ürün kimliği (sku) bir dizgidir ve bir kalem adıyla eşlenir. Stok, fiyat ve ürün bulunamadığında verilen hata modele girmez; bulunmayan kalemi kaldırma eylemi durumu değiştirmez.
Sınırlayıcı Ölçüm
Beklenen değeri bir kayıttan almak, kusuru kaydetmenin en kolay yoludur. Burada kayıtlı örnek, kusurlu indirgeyicinin birinci dizideki sonucundan alınmıştır: bos. Aynı kayıtlı örnek iki ayrı diziye karşı denenir.
// main.swift — kayitli eylem ve durum ornegi kusurlu indirgeyiciden alinmis
let recorded = "bos"
let twoThenRemove: [CartAction] = [.add("kalem"), .add("kalem"), .remove("kalem")]
let oneThenRemove: [CartAction] = [.add("kalem"), .remove("kalem")]
let onTwo = contrast("kayit-iki-ekle-bir-kaldir", correct: replayActions(twoThenRemove, faulty: false), faulty: replayActions(twoThenRemove, faulty: true)) { $0.description == recorded }.describe()
let onOne = contrast("kayit-bir-ekle-bir-kaldir", correct: replayActions(oneThenRemove, faulty: false), faulty: replayActions(oneThenRemove, faulty: true)) { $0.description == recorded }.describe()
countSeparations([onTwo, onOne])
kayit-iki-ekle-bir-kaldir | dogru=kalemx1/kaldi kusurlu=bos/gecti -> ters kayit-bir-ekle-bir-kaldir | dogru=bos/gecti kusurlu=bos/gecti -> kor ayiran: 0 kor: 1 gurultulu: 0 ters: 1
Üç koşumda çıktı aynıdır. Birinci dizide kayıt kusurlu sürümden alındığı için doğru sürüm kalemx1 verir ve reddedilir, kusurlu sürüm bos verir ve kabul edilir. Bu ters denetimdir ve kusurlu indirgeyiciyi yeşil geçirir. İkinci dizide iki sürüm de bos verdiği için aynı kayıt ikisini de kabul eder ve denetim kör kalır. Aynı kayıtlı örnek bir dizide ters, ötekinde kördür. Kayıtlı bir örnek ancak doğru sürümden alındığında anlamlıdır; kayıt bir kez kusurlu çıktıdan alınmışsa, indirgeyici düzeltilince bütün denetim kırmızıya döner ve kırmızıya bakan kişi denetimi düzeltmek yerine kaydı yenileyebilir, bu da kusuru kalıcılaştırır.
Özet
- Tek yönlü veri akışında durum merkezî depoda durur, değişiklik yalnız eylemle gelir ve indirgeyici eski durumdan yeni durumu üretir.
- İndirgeyici saf bir işlevdir; aynı eylem dizisi iki sürüme verilip çıkan iki durum karşılaştırılabilir.
- Sonuç durumunu ve kalan adedi karşılaştıran denetimler kusurlu indirgeyiciyi yakalar: dört denetimin ikisi ayıran, ikisi kör.
- Kusurun dokunmadığı bir özelliğe bakan denetim ve kusurun görünmediği bir diziye bakan denetim kör kalır.
- Kayıtlı eylem ve durum örneği kusurlu indirgeyiciden alınırsa aynı denetim bir dizide ters, ötekinde kör olur.
- Bu dersin ayrımı: ayıran.
Sonraki Adım
İndirgeyici saf olduğu için kolay ölçüldü; ama gerçek bir uygulamada durumun bir kısmı dışarıdan gelir. Dördüncü derste Bağımlılık Enjeksiyonu, ağ istemcisi gibi dış bağımlılıkların ekrana ya da depoya dışarıdan verilmesini işleyecek. Orada kusur bir istemcide olacak: boş yanıt geldiğinde önceki listeyi korumak yerine temizleyecek, ve gerçek ağa bağlı bir denetim iki sürümde de kalacak.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.