İçeriğe geç
academia.sh

Ders 01 / 19

Mimari Kalıplar

Hesabın ekrandan ayrılmasının üç yolu ve altı denetimin aynı doğru ve kusurlu sürüme uygulanıp dört ayrımda ölçülmesi.

Yayın Güncelleme

Bu derste

  • Hesabın görünüm denetleyicisinde, görünüm modelinde ya da sunucu katmanında durmasının denetimi nasıl değiştirdiğini söyler
  • Bir denetimin doğru sürümü kusurlu sürümden ayırıp ayırmadığını iki yanıtın karşılaştırılmasıyla ölçer
  • Ayıran, kör, gürültülü ve ters denetimi tanır ve her birinin neden yanıltıcı olduğunu açıklar
  • Kayıtlı örneğin kusurlu sürümden alınmasının doğruyu reddedip kusurluyu kabul ettirdiğini gösterir
İçindekiler

Önceki kurslarda ekranı, ağı ve eşzamanlılığı kendi sorularıyla ölçtük. Bu kurs onların üstüne çıkar ve uygulamanın nasıl düzenlendiğine, nasıl sınandığına ve nasıl yayınlandığına bakar. Üç işin ortak bir noktası var: hepsi bir denetim koyar. Bir test bir dönüş değerine, bir arayüz testi ekrandaki bir metne, bir yayın hattı derlemenin geçip geçmediğine bakar. Her denetim ya “geçti” der ya “kaldı”. Ama geçmesi uygulamanın doğru olduğunu göstermez.

Bir denetimin işe yarayıp yaramadığı ancak aynı denetim kusurlu bir sürüme uygulandığında görünür. Bu yüzden kursun tek sorusu şudur: bu denetim doğru sürümü kusurlu sürümden ayırıyor mu? İki sürüm, bir denetim ve iki yanıt kurarız. Dört birleşim dört ayrım verir:

  • ayıran: doğru sürümde geçer, kusurlu sürümde kalır. Denetim kusuru yakalıyor.
  • kör: iki sürümde de geçer. Denetim kusurun dokunmadığı bir şeye bakıyor.
  • gürültülü: iki sürümde de kalır. Denetim kusurdan bağımsız bir nedenle kalıyor.
  • ters: doğru sürümde kalır, kusurlu sürümde geçer. Denetimin beklentisi kusurlu sürümden alınmış.

Çıktıda dört ad ASCII yazılır: ayiran, kor, gurultulu, ters. Ders kategori adını elle yazmaz. Kategoriyi kural verir: çekirdek, denetimin iki sürüm için verdiği iki yanıtı karşılaştırır.

// 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)")
    }
}

contrast aynı denetimi, yani check kapanışını, iki sürümün çıktısına uygular. Denetim tek bir kapanıştır ve iki sürüm için aynıdır; fark yalnız girdidir. Contrast iki soru sorar: denetim doğru sürümde geçiyor mu (passesCorrect), kusurlu sürümde geçiyor mu (passesFaulty). Output, CustomStringConvertible’a uyan her tip olabilir. countSeparations dört adı sayar.

Bu ölçünün bir sınırı baştan bilinmeli: ayrım tek bir kusura göre ölçülür. Bir denetimin “ayıran” çıkması her kusuru yakaladığını göstermez, yalnız dersin kurduğu kusuru yakaladığını gösterir. Her derste hangi kusurun kurulduğu adıyla söylenecek.

Hesabı Ekrandan Ayırmak

Ayrımı ölçebilmek için önce bir sürümü ayrı bir yerde çalıştırabilmek gerekir. Hesap ekranın içindeyse bu mümkün değildir. Mimari kalıpların bu kursta ilgilendiği ilk iş budur: hesabın nerede durduğu, onu denetleyebilmenin maliyetini belirler.

İlk varyant, hesabın görünüm denetleyicisinde durmasıdır. Sepet toplamı, indirim ve fiş metni, ekranı çizen sınıfın içinde hesaplanır.

import UIKit

// Hesap ekranin icinde — KAYNAK
final class CartViewController: UIViewController {
    private let totalLabel = UILabel()
    private var lines: [(name: String, price: Int)] = []

    override func viewDidLoad() {
        super.viewDidLoad()
        let sum = lines.reduce(0) { $0 + $1.price }
        let total = sum - sum * 10 / 100
        totalLabel.text = "Toplam: \(total) TL"
    }
}

Bu parça çalıştırılmaz; görünüm denetleyicisi platformun yaşam döngüsüne bağlıdır ve ders için çıktı iddia etmez. Önemli olan yapısıdır. total değerini denetlemek için viewDidLoad’ın çalışması, yani bir ekranın kurulması gerekir. Kusurlu sürümü (indirimi iki kez uygulayan) denetlemek için de ekranın iki ayrı kopyası gerekir. Hesabı denetlemek ekranı denetlemekle aynı iş olur.

İkinci varyant, hesabın görünüm modeline taşınmasıdır. Görünüm modeli ekranın bilmesi gereken değerleri üretir, ekran yalnız onları gösterir.

// Hesap gorunum modelinde — KAYNAK
struct CartViewModel {
    let lines: [Line]
    let percent: Int

    var total: Int { correctTotal(lines, percent: percent) }
    var receiptText: String { receipt(total) }
}

Görünüm modeli saf bir değer olduğu için bir ekran kurmadan çağrılır. Bu dersin ölçümü bu varyanta dayanır: aşağıdaki correctTotal ve faultyTotal, görünüm denetleyicisindeki hesabın ekransız kopyalarıdır. Hesabı ayırmak, aynı denetimin iki sürüme verilmesini ucuz kılar.

Üçüncü varyant, hesabın sunucu katmanına bırakılmasıdır: uygulama toplamı kendi hesaplamaz, bir istekle alır. Bu durumda hesabı denetlemek bir ağ çağrısı gerektirir. Ağa bağlı bir denetim ağ yoksa kalır, ağ varsa sunucunun o anki sürümüne bağlıdır. Bu sorunu dördüncü derste, bağımlılığı dışarıdan vermeyi anlatırken ölçeceğiz.

Üç varyantın hiçbiri tek başına doğru değildir. Hangisinin seçileceği, hangi denetimin ne kadara mal olduğuna bağlıdır. Bu ders yalnız bir şeyi sabitler: denetim ekrana ne kadar bağlıysa iki sürümü karşılaştırmak o kadar pahalıdır.

Altı Denetim, Dört Ayrım

Kusur şudur: indirim iki kez uygulanıyor. Doğru sürüm 100 TL’lik sepete yüzde 10 indirimle 90 TL verir, kusurlu sürüm 81 TL. Altı denetim aynı iki sürüme uygulanır. Denetimlerin adları ve doğruladıkları değerler, bir test hedefinde yazılacak testlerin adları ve beklenen değerleridir.

// main.swift — alti denetim, dort ayrim
struct Line {
    let name: String
    let price: Int
}

func correctTotal(_ lines: [Line], percent: Int) -> Int {
    let sum = lines.reduce(0) { $0 + $1.price }
    return sum - sum * percent / 100
}

func faultyTotal(_ lines: [Line], percent: Int) -> Int {
    let sum = lines.reduce(0) { $0 + $1.price }
    let discounted = sum - sum * percent / 100
    return discounted - discounted * percent / 100
}

func receipt(_ total: Int) -> String { "Toplam: \(total) TL" }

let cart = [Line(name: "defter", price: 40), Line(name: "kalem", price: 25), Line(name: "silgi", price: 35)]
let good = correctTotal(cart, percent: 10)
let bad = faultyTotal(cart, percent: 10)

let exact = contrast("toplam-esit", correct: good, faulty: bad) { $0 == 90 }.describe()
let discount = contrast("indirim-tutari", correct: 100 - good, faulty: 100 - bad) { $0 == 10 }.describe()
let positive = contrast("pozitif-toplam", correct: good, faulty: bad) { $0 > 0 }.describe()
let rows = contrast("satir-sayisi", correct: cart.count, faulty: cart.count) { $0 == 3 }.describe()
let text = contrast("fis-metni", correct: receipt(good), faulty: receipt(bad)) { $0 == "Toplam: 90,00 TL" }.describe()
let golden = contrast("kayitli-ornek", correct: good, faulty: bad) { $0 == 81 }.describe()

countSeparations([exact, discount, positive, rows, text, golden])
toplam-esit | dogru=90/gecti kusurlu=81/kaldi -> ayiran
indirim-tutari | dogru=10/gecti kusurlu=19/kaldi -> ayiran
pozitif-toplam | dogru=90/gecti kusurlu=81/gecti -> kor
satir-sayisi | dogru=3/gecti kusurlu=3/gecti -> kor
fis-metni | dogru=Toplam: 90 TL/kaldi kusurlu=Toplam: 81 TL/kaldi -> gurultulu
kayitli-ornek | dogru=90/kaldi kusurlu=81/gecti -> ters
ayiran: 2
kor: 2
gurultulu: 1
ters: 1

Çıktı Swift 6.4’te sandbox’ta üç kez koşturuldu ve üçünde de aynıdır. Altı denetim dört ayrımın hepsini verdi: 2 ayıran, 2 kör, 1 gürültülü, 1 ters.

toplam-esit ve indirim-tutari kusurun değiştirdiği değere bakar. İlki toplamın 90 olmasını, ikincisi indirimin 10 olmasını bekler; kusurlu sürüm 81 ve 19 verdiği için ikisi de kalır. Bunlar ayırandır.

pozitif-toplam kusurun bozmadığı bir özelliğe bakar: 81 de sıfırdan büyüktür. satir-sayisi ise kusurun hiç dokunmadığı bir değere bakar: indirim satır sayısını değiştirmez. İkisi de iki sürümde geçer, yani kördür. Kör denetim yanıltıcıdır, çünkü yeşil görünür. Bir kapsam raporu bu iki denetimi de koşan satır olarak sayardı; ayıran ile kör arasındaki farkı raporun sayısı göstermez, yalnız ayrım gösterir.

fis-metni beklenen metni 90,00 diye eski biçimde yazmıştır. Fiş Toplam: 90 TL basar, bu yüzden denetim doğru sürümde de kalır. Kusurlu sürüm de kalır. Denetim kusuru yakalamaz; kalması kusurdan değil, denetimin kendisindendir. Bu gürültülüdür. Gürültülü denetim bir süre sonra “bu test hep kırmızı” diye susturulur, ve susturulan denetim ileride gerçek bir kusuru da yakalayamaz.

kayitli-ornek beklenen değeri kusurlu sürümden kaydetmiştir: 81. Doğru sürüm 90 verir ve denetim onu reddeder, kusurlu sürüm 81 verir ve denetim onu kabul eder. Bu ters denetimdir ve dördünün en tehlikelisidir. Kusurlu sürüm yeşil geçer, doğru sürüm kırmızı. Tipik nedeni kayıtlı örneğin (altın dosyanın, anlık görüntünün) kusurlu bir sürümden alınmasıdır; kaydı alan kişi çıktıya bakıp “doğru görünüyor” demiştir.

Bu ayrımın bir bedeli de vardır: gürültülü ve ters, denetimi yazanın hatasıdır, kodun hatası değildir. Kusuru kodda aramak yanlış yere bakmaktır.

Beklenen Değer Nereden Gelir

Altı denetimin dördü aynı yerden hata verdi: beklenen değerin kaynağı. pozitif-toplam ve satir-sayisi beklentisini kusurun bozmadığı bir özellikten, fis-metni eski bir biçimden, kayitli-ornek kusurlu sürümün kendi çıktısından almıştı. İki ayıran denetim ise beklentisini koddan değil kuraldan almıştı: yüzde 10 indirimle 100 TL’lik sepet 90 TL eder. Bu fark ilk bakışta küçük görünür, ama uygulamada büyük sonuç verir. Kuraldan alınan beklenti koddan bağımsızdır; kodun değişmesi onu etkilemez, kodun bozulması onu kırar. Koddan alınan beklenti ise kodla birlikte bozulur.

Bu yüzden bir denetim yazarken sorulacak ilk soru “bu değer ne olmalı” olmalıdır, “bu değeri kod ne veriyor” değil. İkincisi, kayıtlı örnekte olduğu gibi, denetimi kodun bir aynasına çevirir ve ayna kusuru da yansıtır.

Kasıtlı olarak bozulan kodun test kümesince yakalanıp yakalanmadığını soran bozma denemesi bu ölçünün en yakın akrabasıdır. Fark şudur: bozma denemesi yalnız “yakalandı mı” diye sorar. Burada doğru sürümün sonucu da ölçülür; böylece gürültülü ve ters denetimler kör denetimden ayrı görünür. Üç denetim de yeşil ya da kırmızı olabilir, ama her birinin ardındaki neden ayrıdır ve her birinin çaresi ayrıdır: kör denetime ikinci bir girdi eklenir, gürültülü denetimin beklentisi düzeltilir, ters denetimin kaydı doğru sürümden yeniden alınır.

Varsayımlar

MM1. correctTotal ve faultyTotal, görünüm denetleyicisindeki hesabın ekransız kopyalarıdır. Gerçek ekranın aynı sonucu verdiğini bu ders ölçmez; ekran KAYNAK kalır.

MM2. Tutarlar tam sayı TL’dir ve bölme aşağı yuvarlar. Kuruş, yuvarlama modu ve para birimi tipi modele girmez; fis-metni denetiminin gürültüsü bu yüzden yalnız metin biçiminden gelir.

MM3. Denetimler gerçek bir test çerçevesinde koşmaz. Her denetim, bir testin yaptığı işin karşılığı olan bir kapanıştır; test adları ve beklenen değerler çerçevedeki testlerle aynı tutulur.

Sınırlayıcı Ölçüm

Aynı denetim aynı kusura karşı bir girdide kör, başka bir girdide ayıran olabilir. Burada kusur başkadır: yüzde, onluğa aşağı yuvarlanarak uygulanıyor.

func correctPrice(_ sum: Int, percent: Int) -> Int { sum - sum * percent / 100 }
func roundedPrice(_ sum: Int, percent: Int) -> Int { sum - sum * (percent / 10 * 10) / 100 }

let ten = contrast("yuzde-on", correct: correctPrice(100, percent: 10), faulty: roundedPrice(100, percent: 10)) { $0 == 90 }.describe()
let fifteen = contrast("yuzde-on-bes", correct: correctPrice(100, percent: 15), faulty: roundedPrice(100, percent: 15)) { $0 == 85 }.describe()
countSeparations([ten, fifteen])
yuzde-on | dogru=90/gecti kusurlu=90/gecti -> kor
yuzde-on-bes | dogru=85/gecti kusurlu=90/kaldi -> ayiran
ayiran: 1
kor: 1
gurultulu: 0
ters: 0

Üç koşumda çıktı aynıdır. Yüzde 10 kusurun görünmediği girdidir, çünkü 10’u onluğa yuvarlamak 10 verir. Aynı denetim bu girdide kör kalır. Yüzde 15’te kusur görünür: 15 onluğa 10’a iner ve denetim kalır. Denetimin kendisi değişmedi, değişen yalnız girdi. Bu ölçümün söylediği şudur: bir denetimin “kör” ya da “ayıran” olması denetim kadar girdinin de özelliğidir. Tek girdiyle yazılmış bir test, kusurun yaşadığı girdiyi hiç görmemiş olabilir.

Özet

  • Hesap görünüm denetleyicisindeyse onu denetlemek bir ekran kurmayı gerektirir; görünüm modeline taşınınca iki sürüm ekransız karşılaştırılır. Sunucu katmanındaki hesap ağa bağlıdır; bunu dördüncü derste ele alıyoruz.
  • Bir denetimin değeri, doğru ve kusurlu sürüme verdiği iki yanıttan çıkar: geçip geçmediği tek başına bir şey söylemez.
  • Altı denetim dört ayrımı verdi: 2 ayıran, 2 kör, 1 gürültülü, 1 ters.
  • Kör denetim yeşil görünür ve yanlış güven verir; ters denetim kusurlu sürümü geçirir, doğru sürümü kırar.
  • Aynı denetim bir girdide kör, başka bir girdide ayıran olabilir: yüzde 10’da kör, yüzde 15’te ayıran.
  • Bu dersin ayrımı: dördü (2/2/1/1).

Sonraki Adım

Hesabı ekrandan ayırmak yalnız hesaba yarar; ekranlar arasındaki geçiş hâlâ ekranların içinde duruyor olabilir. İkinci derste Koordinatör Kalıbı gezinmeyi görünümlerden alıp ayrı bir nesneye verecek. Orada denetim bir ekranın değerine değil, ödemeden sonra hangi ekranın açıldığına bakacak, ve ödemeden sonra yanlış ekranı açan bir kusur karşısında iki denetim ayrı yanıt verecek.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat