---
title: 'Mimari, Test ve Yayınlama'
source: 'https://academia.sh/tr/kurslar/ios-uretim'
language: tr
updated: '2026-10-21T05:03:06+00:00'
license: 'CC BY-SA 4.0'
lessons: 19
outcomes:
    - 'Uygulama mimarisini test edilebilirlik ölçütüyle seçer'
    - 'Birim ve arayüz testlerini otomatikleştirir'
    - 'İmzalama ve mağaza yayın sürecini yürütür'
    - 'Bir denetimin doğru sürümü kusurlu sürümden ayırıp ayırmadığını, kör, gürültülü ya da ters olup olmadığını ayırt eder'
    - 'Bir denetimin ayrımını tek bir kusura ve tek bir girdiye göre ölçer ve ölçmediği kusuru adlandırır'
---

# Mimari, Test ve Yayınlama

Birim testi bir dönüş değerine bakar, arayüz testi ekrandaki bir metne bakar, kapsam raporu koşan satırları sayar, sürekli tümleştirme hattı derlemenin geçtiğini söyler, mağaza incelemesi bir kurallar listesini uygular. Kurs her düzeneği tek bir soruyla ölçer — bu denetim doğru sürümü kusurlu sürümden ayırıyor mu? Denetim kusuru yakalayan mı, kusurun dokunmadığı bir şeye bakan kör mü, iki sürümde de kalan gürültülü mü, yoksa doğruyu reddedip kusurluyu kabul eden ters mi? Mimari kalıplar ve koordinatör, tek yönlü veri akışı, bağımlılık enjeksiyonu ve modüller, birim ve arayüz testleri, kapsam, profil çıkarma, bellek sızıntısı ve başarım, kod kalitesi araçları, imzalama, sürekli tümleştirme, yayın otomasyonu, beta dağıtımı, mağaza incelemesi ve listelemesi, çökme raporlama aynı çerçeveden geçirilir.

## Mimari

Görünüm ile model ayrımının varyantları, gezinme sorumluluğunu ayıran koordinatör, durum, eylem ve indirgeyiciyle tek yönlü veri akışı, test edilebilirlik için bağımlılık enjeksiyonu ve paket sınırlarıyla modülerleştirme.

### Mimari Kalıplar

Source: https://academia.sh/tr/kurslar/ios-uretim/mimari-kaliplar

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.

Ö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.

```swift
// 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.

```swift
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.

```swift
// 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.

```swift
// 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])
```

```text
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.

```swift
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])
```

```text
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.

### Koordinatör Kalıbı

Source: https://academia.sh/tr/kurslar/ios-uretim/koordinator-kalibi

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.

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.

```swift
// 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.

```swift
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.

```swift
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.

```swift
// 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)`.

```swift
// 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])
```

```text
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)`.

```swift
// 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])
```

```text
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.

### Tek Yönlü Veri Akışı

Source: https://academia.sh/tr/kurslar/ios-uretim/tek-yonlu-veri-akisi

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.

İ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.

```swift
// 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.

```swift
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.

```swift
// 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.

```swift
// 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])
```

```text
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.

```swift
// 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])
```

```text
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.

### Bağımlılık Enjeksiyonu

Source: https://academia.sh/tr/kurslar/ios-uretim/bagimlilik-enjeksiyonu

Bir nesnenin ihtiyaç duyduğu bağımlılığı kendisi kurmak yerine dışarıdan alması ve sahte bağımlılığın neyi taklit ettiğinin denetimin ayrımını nasıl belirlediği.

Üçüncü derste durumu tek bir yere topladık ve indirgeyiciyi ekransız ölçtük. İndirgeyici dışarıdan hiçbir şey almadığı için ölçmek kolaydı. Çoğu nesne böyle değildir: bir ürün listesi görünüm modeli ürünleri bir ağ istemcisinden alır, bir sipariş nesnesi kaydı bir depodan okur. Bu bağımlılıklar nesnenin içinde kuruluyorsa nesneyi denetlemek, bağımlılığı da çalıştırmak demektir.

Bağımlılık enjeksiyonu bu bağı tersine çevirir: nesne ihtiyaç duyduğu bağımlılığı kendisi kurmaz, dışarıdan alır. Böylece denetimde gerçek bağımlılığın yerine ona benzeyen bir sahte bağımlılık verilebilir. Bu dersin kusuru ürün listesindedir: istemci boş bir yanıt aldığında önceki listeyi korumak yerine listeyi temizliyor. Çekirdek önceki derslerdeki gibi birebir gelir.

```swift
// 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. Ayrım adları aynıdır ve yalnız denetimlerin ne yaptığı değişir.

#### Bağımlılığı Dışarıdan Vermek

Enjeksiyonsuz görünüm modeli, istemcisini kendisi kurar. Bu sürümde denetim için iki yol vardır ve ikisi de kötüdür: gerçek ağa çıkmak ya da kurulan istemciyi değiştirmenin bir yolunu bulmak.

```swift
import Foundation

// Bagimlilik iceride kuruluyor — KAYNAK
final class ProductListViewModel {
    private let service = NetworkProductService(session: URLSession.shared)
    private(set) var items: [String] = []

    func refresh() async {
        guard let reply = await service.fetch() else { return }
        items = reply
    }
}
```

Bu parça çalıştırılmaz; oturum ve ağ platforma bağlıdır ve ders için çıktı iddia etmez. Yapısı yeter: `service` sabit olarak içeride kurulmuştur. Bu modelin `refresh` çağrısını denetlemek için ağın açık olması, sunucunun o anki yanıtı vermesi ve yanıtın denetimin beklediği olması gerekir. Üçü de denetimin elinde değildir.

Dışarıdan vermenin üç yolu vardır. En doğrudan olanı başlatıcı enjeksiyonudur: bağımlılık nesne kurulurken parametre olarak gelir. Özellik enjeksiyonunda nesne kurulduktan sonra atanır; işlev parametresi enjeksiyonunda yalnız o işlevin ihtiyacı olan çağrıda verilir. Bu derste başlatıcı kullanılır, çünkü bağımlılık nesnenin ömrü boyunca değişmez.

```swift
import Foundation

// Bagimlilik disaridan veriliyor — KAYNAK
protocol ProductService: Sendable {
    func fetch() async -> [String]?
}

struct NetworkProductService: ProductService {
    let session: URLSession

    func fetch() async -> [String]? {
        guard let url = URL(string: "https://example.com/products") else { return nil }
        guard let (data, _) = try? await session.data(from: url) else { return nil }
        return try? JSONDecoder().decode([String].self, from: data)
    }
}

final class InjectedListViewModel {
    private let service: any ProductService
    private(set) var items: [String] = []

    init(service: any ProductService) { self.service = service }
}
```

Bu parça da çalıştırılmaz. Önemli olan protokoldür: görünüm modeli artık gerçek istemciyi değil, `ProductService` sözleşmesini tanır. Gerçek istemci o sözleşmeye uyar, denetimdeki sahte bağımlılık da uyar. Görünüm modeli ikisini ayırt edemez.

#### Sahte Bağımlılık Neyi Taklit Eder

Model, protokolü ve iki sahte bağımlılığı saf Swift'le kurar. `OfflineService` ağın olmadığı durumu taklit eder: her çağrıda `nil` döner. `StubService` önceden verilen bir yanıtı döner; bu, çağrıldığında sabit yanıt veren bir güdüktür. Ürün listesi modeli `refresh` içinde üç yoldan birine girer: istek hata verirse liste korunur; yanıt boşsa doğru sürüm de listeyi korur, kusurlu sürüm listeyi temizler; yanıt doluysa liste yanıtla değişir. Kusur yalnız ikinci yoldadır. `Catalog.swift` bir yardımcı dosyadır ve çıktı vermez.

```swift
// Catalog.swift — urun listesi modeli ve sahte bagimliliklar
protocol ProductService: Sendable {
    func fetch() async -> [String]?
}

struct OfflineService: ProductService {
    func fetch() async -> [String]? { nil }
}

struct StubService: ProductService {
    let reply: [String]
    func fetch() async -> [String]? { reply }
}

struct ProductListModel {
    private(set) var items: [String]
    let service: any ProductService
    let faulty: Bool

    mutating func refresh() async {
        guard let reply = await service.fetch() else { return }
        if reply.isEmpty && !faulty { return }
        items = reply
    }
}

func refreshedItems(start: [String], service: any ProductService, faulty: Bool) async -> String {
    var model = ProductListModel(items: start, service: service, faulty: faulty)
    await model.refresh()
    return model.items.isEmpty ? "bos" : model.items.joined(separator: ",")
}
```

Üç denetim aynı iki sürüme uygulanır. Birincisi gerçek ağ istemcisine bağlı denetimin yerini tutar. Sandbox'ta ağ kapalı olduğu için bu denetimde istemci `OfflineService` ile modellenir ve denetim sunucunun üç ürünlük listesini bekler. İkincisi boş yanıt dönen bir güdükle çalışır. Üçüncüsü ağ hatasını taklit eder ve listenin korunmasını bekler.

```swift
// main.swift — uc denetim, uc farkli sahte bagimlilik
let start = ["defter", "kalem"]

let goodReal = await refreshedItems(start: start, service: OfflineService(), faulty: false)
let badReal = await refreshedItems(start: start, service: OfflineService(), faulty: true)
let goodEmpty = await refreshedItems(start: start, service: StubService(reply: []), faulty: false)
let badEmpty = await refreshedItems(start: start, service: StubService(reply: []), faulty: true)

let real = contrast("gercek-ag-istemcisi", correct: goodReal, faulty: badReal) { $0 == "defter,kalem,silgi" }.describe()
let empty = contrast("bos-yanit-guduk", correct: goodEmpty, faulty: badEmpty) { $0 == "defter,kalem" }.describe()
let failure = contrast("hata-yaniti-guduk", correct: goodReal, faulty: badReal) { $0 == "defter,kalem" }.describe()

countSeparations([real, empty, failure])
```

```text
gercek-ag-istemcisi | dogru=defter,kalem/kaldi kusurlu=defter,kalem/kaldi -> gurultulu
bos-yanit-guduk | dogru=defter,kalem/gecti kusurlu=bos/kaldi -> ayiran
hata-yaniti-guduk | dogru=defter,kalem/gecti kusurlu=defter,kalem/gecti -> kor
ayiran: 1
kor: 1
gurultulu: 1
ters: 0
```

Çıktı Swift 6.4'te sandbox'ta üç kez koşturuldu ve üçünde de aynıdır. Üç denetim üç ayrı ayrımı verdi: bir ayıran, bir kör, bir gürültülü.

`gercek-ag-istemcisi` sunucudan gelecek üç ürünlük listeyi bekler. Sandbox'ta ağ olmadığı için istemci yanıt veremez, liste olduğu gibi kalır ve iki sürümde de `defter,kalem` çıkar. Denetim iki sürümde de kalır, yani gürültülüdür. Kusuru yakalamaz; kalması kusurdan değil, denetimin ağa bağlı olmasındandır. Aynı gürültü gerçek bir ortamda da vardır: sunucu yavaşsa, erişilemiyorsa ya da başka bir yanıt veriyorsa denetim kırmızı olur ve kırmızıya bakan kişi kusuru koda değil, ağa yorar.

`bos-yanit-guduk` listeyi koruyup korumadığına bakar ve güdük boş bir yanıt döner. Doğru sürüm `defter,kalem` verir ve geçer; kusurlu sürüm listeyi temizler, `bos` verir ve kalır. Denetim ayıran olur. Burada fark bağımlılığın dışarıdan verilmesidir: kusurun yaşadığı yol, yani boş yanıt, ancak bağımlılığı kontrol eden bir denetimle tetiklenir.

`hata-yaniti-guduk` ağ hatasını taklit eder ve listenin korunmasını bekler. Hata dalında iki sürüm de listeyi korur, bu yüzden denetim iki sürümde de geçer ve kördür. Kusur boş yanıt dalındadır, hata dalında değil. Sahte bağımlılık yanlış yolu taklit ettiğinde denetim doğru görünür ve kusuru kaçırır.

#### Varsayımlar

**MM1.** Gerçek ağ istemcisi ölçülmez. Sandbox'ta ağ kapalıdır; gerçek istemci yerine `OfflineService` ile "yanıt gelmiyor" durumu modellenir ve bu durumun denetimi gürültülüdür. Gerçek bir ortamda aynı denetimin nasıl davranacağı sunucuya bağlıdır ve bu ders ona bir değer vermez.

**MM2.** `ProductService` protokolünün modeldeki hâli gerçek istemcinin sözleşmesiyle aynıdır: bir yanıt listesi ya da `nil`. Gerçek istemcide hata türleri, kimlik doğrulama ve yeniden deneme vardır; model bunları tek bir `nil`'e indirger.

**MM3.** Yanıt bir ürün adları listesidir. Sayfalama, sıralama ve ürün alanları modele girmez.

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

Sahte bağımlılığın neyi taklit ettiği, ayrımı belirler. Güdük her çağrıda aynı dolu yanıtı dönerse kusurlu yol hiç koşmaz. Aynı iki sürüm iki güdükle denenir: biri hep üç ürünlük dolu yanıt döner, öteki boş yanıt döner.

```swift
// main.swift — ayni iki surum, iki farkli guduk
let start = ["defter", "kalem"]
let full = ["defter", "kalem", "silgi"]

let goodFull = await refreshedItems(start: start, service: StubService(reply: full), faulty: false)
let badFull = await refreshedItems(start: start, service: StubService(reply: full), faulty: true)
let goodNone = await refreshedItems(start: start, service: StubService(reply: []), faulty: false)
let badNone = await refreshedItems(start: start, service: StubService(reply: []), faulty: true)

let onFull = contrast("dolu-yanit-guduk", correct: goodFull, faulty: badFull) { $0 == "defter,kalem,silgi" }.describe()
let onNone = contrast("bos-yanit-guduk", correct: goodNone, faulty: badNone) { $0 == "defter,kalem" }.describe()

countSeparations([onFull, onNone])
```

```text
dolu-yanit-guduk | dogru=defter,kalem,silgi/gecti kusurlu=defter,kalem,silgi/gecti -> kor
bos-yanit-guduk | dogru=defter,kalem/gecti kusurlu=bos/kaldi -> ayiran
ayiran: 1
kor: 1
gurultulu: 0
ters: 0
```

Üç koşumda çıktı aynıdır. Güdük hep dolu yanıt döndüğünde iki sürüm de listeyi yanıtla değiştirir ve denetim kördür; kusurun yaşadığı yol hiç çalışmaz. Güdük boş yanıt döndüğünde aynı denetim türü ayıran olur. Denetim değişmedi, sahte bağımlılığın taklit ettiği yanıt değişti. Bu, sahte bağımlılığın en sık yapılan hatasına işaret eder: güdüğü mutlu yol için kurmak. Mutlu yolda hiçbir şey kırılmaz, denetim yeşil kalır ve kusurlu yol bir sonraki gerçek boş yanıtta kullanıcıya ulaşır.

#### Özet

- Bağımlılık enjeksiyonu nesnenin ihtiyaç duyduğu bağımlılığı dışarıdan almasıdır; en yaygın yolu başlatıcıdan vermektir.
- Gerçek ağ istemcisine bağlı denetim ağa ve sunucuya bağlıdır; ağ yanıt vermeyince iki sürümde de kalır ve gürültülüdür.
- Boş yanıt dönen bir güdükle denetim, kusurun yaşadığı yolu tetikler ve ayıran olur.
- Hata yanıtını taklit eden güdük, kusur boş yanıt dalındaysa kördür: üç denetim bir ayıran, bir kör, bir gürültülü verdi.
- Güdük hep dolu yanıt dönerse kusurlu yol koşmaz ve denetim kör kalır.
- Bu dersin ayrımı: **gürültülü**.

#### Sonraki Adım

Bağımlılığı dışarıdan vermek, nesneleri birbirinden ayırdı; ama hepsi aynı hedefte duruyorsa her nesne her nesneyi görebilir. Beşinci derste Modülerleştirme, kodu paketlere böler ve hangi paketin hangisine bağlanabileceğini kurala bağlar. Orada kusur bir özellik modülünün başka bir özellik modülüne doğrudan bağlanması olacak, ve derlemenin geçip geçmediğine bakan denetim bu kusura karşı kör kalacak.

### Modülerleştirme

Source: https://academia.sh/tr/kurslar/ios-uretim/modulerlestirme

Uygulamanın paket sınırlarıyla modüllere bölünmesi, özellik modüllerinin yalnız çekirdek modüle bağlanması kuralı ve derlemenin geçmesine bakan denetimin sınır ihlaline karşı körlüğü.

Dördüncü derste bağımlılığı dışarıdan vermeyi öğrendik: nesneler birbirini kurmak yerine bir sözleşmeyle tanır. Aynı fikir bir basamak yukarıda da geçerlidir. Uygulamanın tamamı tek bir hedefte duruyorsa her dosya her dosyayı görebilir; hiçbir kural bir görünümün ağ katmanını doğrudan çağırmasını ya da bir özelliğin başka bir özelliğin iç tiplerine uzanmasını engellemez.

Modülerleştirme kodu paketlere böler ve her paketin yalnız dışarıya açtığı yüzeyi göstermesini sağlar. Hangi modülün hangisine bağlanabileceği ise kuraldır. Bu dersin kuralı şudur: özellik modülleri yalnız çekirdek modüle bağlanır, birbirine bağlanmaz. Kusur bu kuralın çiğnenmesidir: bir özellik modülü başka bir özellik modülüne doğrudan bağlanır. Çekirdek önceki derslerdeki gibi birebir gelir.

```swift
// 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 bu derste de yeniden tanımlanmaz. Ayrım adları aynıdır.

#### Paket Sınırı

Bir modül, paket yöneticisinin bir hedefidir. Hedefler arasındaki bağımlılıklar paket tanım dosyasında yazılır ve paket yöneticisi o dosyayı okuyarak hangi hedefin hangisini içe alabileceğine karar verir. Bir modülün `internal` bırakılan ögeleri başka modüllerden görünmez; `public` ya da `open` ile açılanlar sınırı geçer; aynı paketin modülleri arasında `package` da yeter.

```swift
// swift-tools-version: 6.0
import PackageDescription

let package = Package(
    name: "Shop",
    targets: [
        .target(name: "Core"),
        .target(name: "Feed", dependencies: ["Core"]),
        .target(name: "Profile", dependencies: ["Core"]),
        .target(name: "Settings", dependencies: ["Core"]),
        .target(name: "App", dependencies: ["Feed", "Profile", "Settings", "Core"]),
    ]
)
```

Bu paket tanımı çalıştırılmaz; paket yöneticisi ve derleme platformun aracıdır ve ders için çıktı iddia etmez. Yapısı kuralı gösterir: `Feed`, `Profile` ve `Settings` yalnız `Core`'a bağlanır, `App` hepsini birleştirir. Kusurlu sürümde tek bir satır değişir.

```swift
        .target(name: "Feed", dependencies: ["Core", "Profile"]),
```

`Feed` artık `Profile`'ı da içe alabilir. Derleme bu satırı reddetmez: `Profile` bir hedef olarak vardır, aralarında döngü yoktur. Bu dersin sorusu tam olarak buradadır: derleyicinin kabul ettiği bir bağımlılık, kuralın kabul ettiği bir bağımlılık olmayabilir.

#### Bağımlılık Grafiği

Model paket tanımını bir grafiğe indirger: her modülün adı ve bağlandığı modüllerin listesi. Grafik üç soruya yanıt verir: her bağımlılık gerçekten bir modüle işaret ediyor mu, grafikte döngü var mı ve bir modül değişince hangi modüller yeniden derlenmek zorunda. Üçüncü soru sınırlayıcı ölçümde kullanılır. `Modules.swift` bir yardımcı dosyadır ve çıktı vermez.

```swift
// Modules.swift — modul bagimlilik grafigi
struct ModuleGraph {
    let edges: [String: [String]]

    var resolves: Bool {
        edges.values.allSatisfy { deps in deps.allSatisfy { edges[$0] != nil } }
    }

    var isAcyclic: Bool {
        func visit(_ name: String, _ seen: Set<String>) -> Bool {
            if seen.contains(name) { return false }
            return (edges[name] ?? []).allSatisfy { visit($0, seen.union([name])) }
        }
        return edges.keys.allSatisfy { visit($0, []) }
    }

    func dependents(of module: String) -> [String] {
        var found: Set<String> = []
        var queue = [module]
        while let current = queue.popLast() {
            for (name, deps) in edges where deps.contains(current) && !found.contains(name) {
                found.insert(name)
                queue.append(name)
            }
        }
        return found.sorted()
    }
}

func shopGraph(faulty: Bool) -> ModuleGraph {
    ModuleGraph(edges: [
        "core": [],
        "feed": faulty ? ["core", "profile"] : ["core"],
        "profile": ["core"],
        "settings": ["core"],
        "app": ["feed", "profile", "settings", "core"],
    ])
}

func featureViolations(_ graph: ModuleGraph) -> String {
    let broken = ["feed", "profile", "settings"].flatMap { feature in
        (graph.edges[feature] ?? []).filter { $0 != "core" }.map { "\(feature)->\($0)" }
    }
    return broken.isEmpty ? "yok" : broken.joined(separator: ",")
}
```

`resolves` her bağımlılığın grafikte bir modül olarak bulunduğunu söyler, `isAcyclic` döngüsüzlüğü, `featureViolations` ise kuralı uygular: özellik modüllerinin çekirdek dışında bağlandığı her modülü `özellik->hedef` biçiminde listeler, hiç yoksa `yok` döner. Üç denetim aynı iki grafiğe uygulanır; ilk ikisi derlemenin bakacağı şeye bakar, üçüncüsü kurala bakar.

```swift
// main.swift — derleme mi, kural mi
let good = shopGraph(faulty: false)
let bad = shopGraph(faulty: true)

let compiles = contrast("derleme-geciyor", correct: good.resolves, faulty: bad.resolves) { $0 }.describe()
let acyclic = contrast("dongu-yok", correct: good.isAcyclic, faulty: bad.isAcyclic) { $0 }.describe()
let rule = contrast("ozellik-yalniz-cekirdege-bagli", correct: featureViolations(good), faulty: featureViolations(bad)) { $0 == "yok" }.describe()

countSeparations([compiles, acyclic, rule])
```

```text
derleme-geciyor | dogru=true/gecti kusurlu=true/gecti -> kor
dongu-yok | dogru=true/gecti kusurlu=true/gecti -> kor
ozellik-yalniz-cekirdege-bagli | dogru=yok/gecti kusurlu=feed->profile/kaldi -> ayiran
ayiran: 1
kor: 2
gurultulu: 0
ters: 0
```

Çıktı Swift 6.4'te sandbox'ta üç kez koşturuldu ve üçünde de aynıdır. Üç denetimin biri ayıran, ikisi kör çıktı.

`derleme-geciyor` bağımlılıkların çözülüp çözülmediğine bakar. `feed -> profile` bağı gerçek bir modüle işaret ettiği için çözülür ve iki sürümde de `true` çıkar: denetim kördür. `dongu-yok` döngü arar. `feed -> profile` ve `profile -> core` bir döngü kurmaz, bu yüzden bu da iki sürümde geçer ve kördür. İkisi de yeşildir ve ikisi de yanıltıcıdır: kusur grafiğin geçerliliğinde değil, grafiğin kurala uygunluğundadır.

`ozellik-yalniz-cekirdege-bagli` kuralı uygular. Doğru sürümde ihlal yoktur ve `yok` çıkar. Kusurlu sürümde `feed->profile` listelenir ve denetim kalır. Bu ayıran olur, ve ayırmasının nedeni derleme kadar geniş bir soru sormak yerine kuralın kendi sorusunu sormasıdır. Derleme "bu çalışır mı" der; kural "bu izinli mi" der. Modülerleştirmenin değeri ikinci sorudadır.

#### Varsayımlar

**MM1.** Model paket tanım dosyasını bir sözlüğe indirger. Gerçek bir paket yöneticisinin hedef çözümlemesini, sürüm kısıtlarını ve platform koşullarını bu ders ölçmez; paket tanımı KAYNAK kalır.

**MM2.** "Derleme geçiyor" denetimi, bağımlılıkların çözülmesi ve döngü olmaması olarak modellenir. Gerçek derleme bundan fazlasını da kontrol eder (tip hataları, erişim denetimi); onlar bu modelin dışındadır.

**MM3.** Derleme süresi ölçülmez. Süre makineye, önbelleğe ve yüke bağlıdır; bu derste yerine bir iş birimi sayılır: değişen modülün yeniden derlenmesi gereken bağımlılarının sayısı.

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

Aynı kuralın bir bedeli de yeniden derlemedir. Bir modül değişince onu doğrudan ya da dolaylı bağlanan her modül yeniden derlenir. Her değişen modül için bir bütçe koyarız: dersin kuralı olarak `profile` değişince en çok bir modül yeniden derlenmeli, `core` değişince en çok dört. Sayı süre değil, iş birimidir.

```swift
// main.swift — degisen modulun yeniden derlenecek bagimlilari
let good = shopGraph(faulty: false)
let bad = shopGraph(faulty: true)

let profileBudget = contrast("profile-butcesi", correct: good.dependents(of: "profile").count, faulty: bad.dependents(of: "profile").count) { $0 <= 1 }.describe()
let coreBudget = contrast("core-butcesi", correct: good.dependents(of: "core").count, faulty: bad.dependents(of: "core").count) { $0 <= 4 }.describe()

countSeparations([profileBudget, coreBudget])
```

```text
profile-butcesi | dogru=1/gecti kusurlu=2/kaldi -> ayiran
core-butcesi | dogru=4/gecti kusurlu=4/gecti -> kor
ayiran: 1
kor: 1
gurultulu: 0
ters: 0
```

Üç koşumda çıktı aynıdır. Doğru sürümde `profile` değişince yalnız `app` yeniden derlenir. Kusurlu sürümde `feed` de `profile`'a bağlandığı için `feed` ve `app` yeniden derlenir ve bütçe aşılır. Denetim ayıran olur. `core` ise iki sürümde de dört bağımlıya sahiptir: `feed`, `profile`, `settings` ve `app`. Kusur çekirdeğin bağımlı sayısını değiştirmez, bu yüzden aynı türden bir bütçe denetimi `core` için kördür. Aynı denetim bir modülde ayıran, ötekinde kör olabilir. Sınır ihlalinin bedeli yalnız onu içeren modülün çevresinde ödenir.

#### Özet

- Modülerleştirme kodu paket sınırlarıyla böler; hangi modülün hangisine bağlanabileceği paket tanımında yazılır.
- Dersin kuralı: özellik modülleri yalnız çekirdek modüle bağlanır, birbirine bağlanmaz.
- Derlemenin geçmesine ve döngü olmamasına bakan denetimler sınır ihlaline karşı kördür; üç denetimin ikisi kör, biri ayıran çıktı.
- Kuralı uygulayan denetim, ihlali `feed->profile` olarak adıyla bulur ve ayıran olur.
- Değişen modülün yeniden derlenecek bağımlı sayısı iş birimidir ve bütçeye bağlanabilir; `profile` bütçesinde ayıran, `core` bütçesinde kör kalır.
- Bu dersin ayrımı: **kör**.

#### Sonraki Adım

Mimari bölümü bitti: hesap ekrandan, gezinme görünümlerden, durum dağınıklıktan, bağımlılık içeriden ve özellikler birbirinden ayrıldı. Sıradaki konu testlerdir. İlk derste Birim Testleri, bu parçaların birini bir test hedefinde sınayacak. Orada kusur bir eşikte olacak: kargo ücreti, sepet tutarı eşiğin tam üstündeyken yanlış hesaplanacak, ve eşiğin altından bir değerle yazılmış test bu kusura karşı kör kalacak.

## Test ve Başarım

Test hedefi ve iddialarla birim testleri, akış otomasyonu ve kararlılıkla arayüz testleri, test planları ve kapsam raporunun yorumu, profil çıkarma araçları, tutma döngüleriyle bellek sızıntısı, başlangıç ve kaydırma başarımı, biçimlendirme ve statik çözümleme araçları.

### Birim Testleri

Source: https://academia.sh/tr/kurslar/ios-uretim/birim-testleri

Test hedefi, iddia ve sahte bağımlılıkla birim testinin kurulması; aynı sınır denetiminin doğru ve kusurlu sürümü ayırıp ayırmadığının dört kategoride ölçümü.

Önceki derste (Modülerleştirme) bir modülün sınırını aşan bağımlılığın nasıl görünür olduğuna baktık. Bu ders sınırın başka bir türüne geçer: bir değerin kural değiştirdiği sınıra. Bir sepette ücretsiz kargo yüz birimden başlıyorsa, kodun en kolay bozulduğu yer tam yüz değeridir. Birim testi bu yerleri denemek için vardır.

Önceki kurs (Veri, Ağ ve Eşzamanlılık) bir işlemin iki koşumunu karşılaştırdı: işlem yinelenirse ya da sırası değişirse sonuç değişiyor mu. Bu kursun sorusu başkadır. Burada tek bir **denetime** bakarız ve onu iki sürüme uygularız: kodun doğru sürümüne ve içine bilerek bir kusur koyduğumuz sürümüne. Denetim doğru sürümde geçip kusurlu sürümde kalıyorsa işe yarıyordur. İkisinde de geçiyorsa hiçbir şey söylemiyordur.

Ölçü ekseni **ayrımdır**. Dört durum çıkar ve çıktıda ASCII adlarıyla görünür. **Ayıran** denetim doğru sürümde geçer, kusurlu sürümde kalır. **Kör** denetim ikisinde de geçer; kusuru göremez. **Gürültülü** denetim ikisinde de kalır; doğru kodu bile reddeder. **Ters** denetim doğru sürümde kalır, kusurlu sürümde geçer; yani kusuru ödüllendirir.

| Kategori | Doğru sürüm | Kusurlu sürüm |
|---|---|---|
| ayıran | geçer | kalır |
| kör | geçer | geçer |
| gürültülü | kalır | kalır |
| ters | kalır | geçer |

Aşağıdaki çekirdek bu dört durumu sınıflar. Etiketi biz vermeyiz; `separation` yalnız iki doğruluk değerinden çıkar.

```swift
// 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` iki sürümün çıktısını ve denetimin o çıktılar üzerindeki kararını tutar. `contrast` işlevi aynı `check` kapanışını (closure) önce doğru, sonra kusurlu çıktıya uygular. `describe` kararı tek satıra yazar. Kural nedeni: bir denetimi tek sürümde çalıştırmak ona hiçbir şey sormaz. Denetimin değeri, iki sürümü ayırabilmesindedir.

#### Test Hedefi, Test Sınıfı ve İddia

Bir birim testi üç parçadan oluşur. **Test hedefi** (test target) uygulamanın kodundan ayrı derlenen pakettir; testler orada yaşar ve uygulama kodunu içe alır. **Test sınıfı** bir `XCTestCase` altsınıfıdır; içindeki her `test` ile başlayan yöntem tek bir deneydir. **İddia** (assertion) deneyin sonucunu beklenen değerle karşılaştıran çağrıdır; tutmazsa test kalır.

Aşağıdaki örnek bir sepetin kargo ücretini denetler. Kural şudur: ara toplam yüz ya da daha fazlaysa kargo ücretsizdir, altındaysa on beş birim tutar.

```swift
// kargo ucreti testi — KAYNAK
import XCTest

final class ShippingFeeTests: XCTestCase {
    func testFeeAtThreshold() {
        XCTAssertEqual(shippingFee(for: 100), 0)
    }

    func testFeeFarAboveThreshold() {
        XCTAssertEqual(shippingFee(for: 500), 0)
    }

    func testFeeBelowThreshold() {
        XCTAssertEqual(shippingFee(for: 40), 15)
    }
}
```

Bu parça çalıştırılmaz; test çerçevesi geliştirme ortamının test koşucusu içinde çalışır. `XCTAssertEqual(_:_:)` iki değeri karşılaştırır. Her yöntem bağımsız koşar; bir yöntem kalsa bile öbürleri çalışır. Değerleri hazırlamak için `setUp()` her yöntemden önce, `tearDown()` sonra çağrılır.

Aynı üç deney ikinci bir biçimde de yazılabilir. Bu biçimde yöntem yerine `@Test` ile işaretli bir işlev, iddia yerine `#expect(_:)` makrosu kullanılır.

```swift
// kargo ucreti testi, ikinci bicim — KAYNAK
import Testing

@Test func feeAtThreshold() {
    #expect(shippingFee(for: 100) == 0)
}

@Test func feeFarAboveThreshold() {
    #expect(shippingFee(for: 500) == 0)
}
```

`#expect` iddiası tutmadığında testi durdurmaz; sonraki iddialar da koşar. Durdurması gerekiyorsa `#require(_:)` kullanılır. Eski biçimde `#expect`in karşılığı `XCTAssertTrue(_:)`, `#require`ın karşılığı `XCTUnwrap(_:)` çağrısıdır. İki biçim aynı soruyu sorar; bu derste yalnız bu ortak soruyu ölçeriz.

#### Kargo Ücreti Modeli

Aşağıdaki **model**, denetimin iki sürüme ne dediğini kurar; test çerçevesinin kendisi değildir. Modelde `shippingFee(for:)` iki sürümle vardır. Doğru sürüm eşikte ve üstünde sıfır döndürür. Kusurlu sürümde karşılaştırma `>=` yerine `>` yazılmıştır, yani tam yüzde ücret hâlâ alınır. Bu, birim testlerinin en bilinen kusurudur: bir sınırın bir yanı yanlış sayılır.

```swift
// Fees.swift — kargo ucretinin dogru ve kusurlu surumu ile denetim yardimcisi
func correctFee(for subtotal: Int) -> Int {
    subtotal >= 100 ? 0 : 15
}

func faultyFee(for subtotal: Int) -> Int {
    subtotal > 100 ? 0 : 15
}

func feeCheck(_ label: String, subtotal: Int, expected: Int) -> String {
    contrast(label, correct: correctFee(for: subtotal), faulty: faultyFee(for: subtotal), check: { $0 == expected }).describe()
}
```

`feeCheck` yukarıdaki iddialardan birinin modelidir: iki sürümden aynı ara toplamla ücret ister ve `XCTAssertEqual(…, expected)` yerine `$0 == expected` karşılaştırmasını yapar. Etiketler testlerin adlarının Türkçe ASCII karşılığıdır. Üç gerçek deneyin yanına dördüncü bir deney ekledik: beklenen değeri yanlış yazılmış bir test. Böylece üç kategori birlikte görünür.

```swift
// main.swift — dort denetim, iki surum
let atThreshold = feeCheck("esikte-ucret", subtotal: 100, expected: 0)
let farAbove = feeCheck("cok-ustunde-ucret", subtotal: 500, expected: 0)
let below = feeCheck("altinda-ucret", subtotal: 40, expected: 15)
let wrongSpec = feeCheck("yanlis-beklenen", subtotal: 40, expected: 10)

countSeparations([atThreshold, farAbove, below, wrongSpec])
```

```
esikte-ucret | dogru=0/gecti kusurlu=15/kaldi -> ayiran
cok-ustunde-ucret | dogru=0/gecti kusurlu=0/gecti -> kor
altinda-ucret | dogru=15/gecti kusurlu=15/gecti -> kor
yanlis-beklenen | dogru=15/kaldi kusurlu=15/kaldi -> gurultulu
ayiran: 1
kor: 2
gurultulu: 1
ters: 0
```

`esikte-ucret` ayıran: doğru sürüm sıfır döndürür ve denetim geçer; kusurlu sürüm on beş döndürür ve denetim kalır. `cok-ustunde-ucret` ile `altinda-ucret` kör: ikisi de iki sürümde geçer, çünkü iki sürüm yüz değerinden uzakta aynı davranır. `yanlis-beklenen` gürültülü: beklenen değer on olduğu için doğru sürüm bile kalır.

Burada dikkat çeken şey, üç testin de yeşil görünebilecek olmasıdır. Üç gerçek denetimden ikisi kusurlu sürümde de geçti. Sınır değerini denemeyen bir test takımı kusuru taşıyan kodu yeşil raporlayabilir. Bu, kapsamın ve test sayısının tek başına güvence vermediğinin ilk işaretidir; kapsamı üçüncü derste ayrıca ölçeceğiz.

**TS1.** Bu ölçüm tek bir kusuru, `>=` yerine `>` yazılmasını denedi. Kod başka biçimde bozulsaydı, örneğin eşik yüz yerine doksan dokuz olsaydı, hangi denetimin ayırdığı değişirdi.

**TS2.** Modelin `faultyFee` işlevi kusuru biz koyduğumuz için biliyoruz. Gerçek bir kodda kusurun nerede olduğu önceden bilinmez; bu yüzden ayrım ölçüsü, kusuru bilinen bir bozma denemesiyle (kodu bilerek bozup testin tepki verdiğine bakmak) birlikte anlamlıdır.

#### Sınır Değeri Neden Ayırır

Ayıran denetimle kör denetim arasındaki fark bir kural gibi okunabilir: **kuralın değiştiği değeri dene**. Eşik yüzse sınır değerleri doksan dokuz, yüz ve yüz birdir. Yüz, kuralın iki tarafını ayırdığı için en çok bilgi taşır. Bir `>=` ile `>` farkı, yalnız bu değerde ortaya çıkar; başka hiçbir girdi iki sürümü birbirinden ayıramaz.

Kör denetimler değersiz değildir. `altinda-ucret` ücretin on beş olduğunu söyler ve bu bir davranış sözleşmesidir; ileride biri ücreti değiştirirse test yakalar. Ama kusuru bulmak isteyen bir denetim olarak kördür. İki rol ayrıdır: bir test davranışı sabitleyebilir, yine de belirli bir kusuru ayıramaz. Ayrım ölçüsü ikincisine bakar.

Bir test yazarken kendine sorulacak soru şudur: bu denetimi kusurlu sürüme uygulasam kalır mıydı. Cevap evetse denetim ayırır. Hayırsa kör olduğunu bilerek yazarsın. Bu soruyu cevaplamanın en ucuz yolu, testi yazdıktan sonra koddaki karşılaştırmayı bir dakikalığına bozup testin kızıp kızmadığına bakmaktır.

#### Sahte Bağımlılıklar

Gerçek bir sepet kodu eşiği ve ücreti kendisi hesaplamaz; bir fiyat servisinden, bir saatten ya da bir ağ istemcisinden alır. Birim testi bu bağımlılıkları gerçeğiyle çalıştırmaz, çünkü test hızlı ve belirlenimli olmalıdır. **Bağımlılık enjeksiyonu** (dependency injection) bağımlılığı dışarıdan vermektir; test de yerine bir **sahte bağımlılık** koyar.

Sahte bağımlılığın üç yaygın biçimi vardır. **Güdük** hazır bir cevap döndürür. **Casus nesne** çağrıldığını kaydeder, sonra çağrıya bakılır. **Kukla nesne** yalnız bir yeri doldurur ve hiç çağrılmaz. Bu üçüne topluca **sahte nesne** de denir.

```swift
// sahte bagimlilik — KAYNAK
protocol RateProviding {
    var freeShippingThreshold: Int { get }
}

struct StubRates: RateProviding {
    let freeShippingThreshold = 100
}

final class SpyReceipt {
    private(set) var printedTotals: [Int] = []
    func print(total: Int) { printedTotals.append(total) }
}
```

`StubRates` güdüktür: eşiği sabit bir değer olarak verir ve test eşiğin yüz olduğundan emin olur. `SpyReceipt` casus nesnedir: fişe hangi toplamların yazıldığını saklar ve test sonradan `printedTotals` dizisine bakar. Bu parça da KAYNAK kalır; yalnız tür bildirimleridir, ölçülecek bir çıktısı yoktur.

Sahte bağımlılığın sınırı şudur: güdük sabit bir değer verdiğinde, kodun gerçek bağımlılıkla davranışı ölçülmez. Eşik yüz olarak sabitlendiği için eşiğin sunucudan yanlış geldiği bir kusur bu testten çıkmaz. Birim testi kodun mantığını ölçer; bileşenlerin birlikte çalışmasını başka bir test katmanı ölçer.

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

Birim testinin en sinsi kusuru beklenen değerin kendisindedir. Bir test yazılırken beklenen değer, **test kâhini** (test oracle) denen bir kaynaktan gelir: şartname, elle yapılmış bir hesap ya da başka bir bağımsız yol. Bazen kâhin yoktur. Yazan kişi kodu çalıştırır, çıkan değeri görür ve iddiaya yapıştırır. Değer o anda geçer, çünkü iddia kodun kendi çıktısıdır.

Aşağıdaki ölçüm bunu yapar. Sınır değerini deneyen aynı `esikte-ucret` iddiası alınır ama beklenen değer kusurlu sürümün çıktısından kopyalanır.

```swift
// main.swift — beklenen deger kusurlu koddan kopyalandi
let pasted = faultyFee(for: 100)
let copied = feeCheck("yapistirilmis-beklenen", subtotal: 100, expected: pasted)

countSeparations([copied])
```

```
yapistirilmis-beklenen | dogru=0/kaldi kusurlu=15/gecti -> ters
ayiran: 0
kor: 0
gurultulu: 0
ters: 1
```

Sınır değerini deneyen aynı iddia önceki ölçümde ayırandı. Şimdi **ters**: doğru sürüm kalır, kusurlu sürüm geçer. Denetimin kendisi değişmedi; değişen, beklenen değerin nereden geldiğidir. Kusurlu kodun çıktısı kâhin yapıldığında test kusuru onaylar ve ileride kod düzeltildiğinde kırmızıya döner. Takım testi suçlar, kusuru değil.

Bu yüzden ayrım yalnız denetimin ne yaptığına bağlı değildir; beklenen değerin kaynağına da bağlıdır. İyi bir kâhin kodu okumadan önce vardır: sepet şartnamesi, elle hesaplanmış bir örnek ya da önceki sürümün belgelenmiş çıktısı.

**TS3.** Bu ölçüm kopyalamayı kusurlu sürümden yapar. Doğru sürümden kopyalanan bir değer iddiayı ayıran yapardı; ama bu durumda da değer ancak doğruluğu önceden biliniyorsa güvenilirdir.

**TS4.** Ölçüm tek bir sınırı, yüzü, denetler. Üç sınır değerinin hepsi aynı ters sonucu verirdi, ama bunu ayrıca ölçmedik.

#### Sık Yapılan Hatalar

- Yalnız eşikten uzak değerleri denemek. Bu denetimler kör kalır.
- Beklenen değeri kodu çalıştırıp kopyalamak. Kâhin kodun kendisi olursa test kusuru onaylar.
- Bir testte birden çok davranışı denetlemek. Test kalınca hangi davranışın bozulduğu anlaşılmaz.
- Sahte bağımlılığı gerçeğe benzetmeye çalışıp testi karmaşıklaştırmak. Güdük yalnız testin ihtiyacı kadar davranmalıdır.
- Testi hiç kırmızı görmeden yeşil kabul etmek. Bir test en az bir kez bilerek bozulmuş kodda kalmış olmalıdır.

#### Özet

- Birim testi test hedefinde yaşar; test sınıfındaki her yöntem tek bir deneydir ve iddia sonucu beklenen değerle karşılaştırır.
- Bir denetimin geçmesi kodun doğru olduğunu göstermez; ayrım, denetimin doğru sürümü kusurlu sürümden ayırıp ayırmadığıyla ölçülür.
- Dört denetimden biri ayıran, ikisi kör, biri gürültülü çıktı; sınır değerini deneyen denetim ayıran tek denetimdi.
- Beklenen değer kusurlu koddan kopyalandığında aynı denetim ters oldu: doğru sürümü reddetti, kusurlu sürümü kabul etti.
- Sahte bağımlılıklar testi hızlı ve belirlenimli tutar ama gerçek bağımlılıkla davranışı ölçmez.
- Bu dersin ayrımı: **ayıran**.

#### Sonraki Adım

Birim testi kodun mantığını sınar; kullanıcının gördüğü ekranı sınamaz. Sıradaki derste Arayüz Testleri, bir düğmeye basan sürücünün hangi denetimlerinin kusuru bulduğunu, hangilerinin yalnız metin değişince kırıldığını ölçecek.

### Arayüz Testleri

Source: https://academia.sh/tr/kurslar/ios-uretim/arayuz-testleri

Uygulamayı ayrı bir süreç olarak sürüp düğmeye basan arayüz testlerinin kurulması ve beklenmesi; üç denetimin ve bekleme anının doğru ve kusurlu sürümü ayırıp ayırmadığının ölçümü.

Birim testi bir işlevi çağırır ve dönen değere bakar. Kullanıcı ise işlev çağırmaz; bir düğmeye basar. Düğmenin arkasında doğru kod çalışsa bile düğme ekrana bağlanmamış olabilir. Önceki derste (Birim Testleri) bir sınır kusurunun yalnız sınır değerini deneyen denetimle yakalandığını gördük. Bu derste kusur başka bir yerde: "Siparişi Onayla" düğmesi görünüyor, basılıyor, ama sipariş göndermiyor.

Ölçü ekseni yine **ayrımdır**: aynı denetimi doğru sürüme ve kusurlu sürüme uygularız. **Ayıran** denetim doğru sürümde geçer, kusurlu sürümde kalır. **Kör** denetim ikisinde de geçer. **Gürültülü** denetim ikisinde de kalır. **Ters** denetim doğru sürümde kalır, kusurlu sürümde geçer. Dört adı çıktıda ASCII olarak göreceğiz. Çekirdek önceki dersteki çekirdeğin aynısıdır ve yeniden yazılmıştır, çünkü her ders kendi başına çalışır.

```swift
// 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` iki sürümün çıktısını ve denetimin o çıktılar üzerindeki kararını tutar; `separation` etiketi yalnız iki doğruluk değerinden türetir. `contrast` işlevi aynı kapanışı iki çıktıya uygular, `countSeparations` dört kategoriyi sayar. **Kural nedeni:** denetimin değeri geçmesinde değil, iki sürümü ayırmasında yatar.

#### Uygulamayı Sürmek

Arayüz testi uygulamanın kodunu çağırmaz. Test hedefi uygulamayı ayrı bir süreç olarak başlatır ve ekrana dışarıdan bir kullanıcı gibi dokunur. Bu yüzden testin içinden uygulamanın tür ve işlevlerine erişilmez; yalnız ekranda görünen öğelere erişilir. Sürücü `XCUIApplication` nesnesidir.

```swift
// siparis onayi arayuz testi — KAYNAK
import XCTest

final class CheckoutUITests: XCTestCase {
    override func setUpWithError() throws {
        continueAfterFailure = false
    }

    func testConfirmButtonExists() {
        let app = XCUIApplication()
        app.launchArguments = ["--reset-orders"]
        app.launch()
        XCTAssertTrue(app.buttons["confirmButton"].exists)
    }

    func testConfirmAddsOrder() {
        let app = XCUIApplication()
        app.launchArguments = ["--reset-orders"]
        app.launch()
        app.buttons["confirmButton"].tap()
        XCTAssertTrue(app.tables["orderList"].cells.element(boundBy: 0).waitForExistence(timeout: 5))
    }
}
```

Bu parça çalıştırılmaz; bir simülatör ya da gerçek aygıt ister. `continueAfterFailure = false` ilk iddia tutmayınca testi durdurur; aksi hâlde boş bir ekrana tıklamaya devam eden test art arda anlamsız hatalar üretir. `launchArguments` uygulamaya testin başlangıç durumunu söyler: burada sipariş listesini boşaltan bir bayrak. Her test aynı durumdan başlamazsa öncekinin bıraktığı kayıt sonuçları değiştirir.

Öğeler üç yolla bulunur. Görünen metinle (`app.buttons["Siparisi Onayla"]`), sıra numarasıyla (`element(boundBy:)`) ya da **erişilebilirlik tanımlayıcısıyla** (accessibility identifier). Tanımlayıcı, öğeye uygulama kodunda verilmiş sabit bir addır ve metin değişse de aynı kalır. Yukarıdaki parçada düğme `confirmButton` tanımlayıcısıyla bulunuyor.

#### Beklemek

Ekran anında değişmez. Düğmeye basıldığında sipariş gönderilir, yanıt gelir, liste yenilenir; bu zincir bir süre alır. `waitForExistence(timeout:)` öğenin ekranda belirmesini en çok verilen süre kadar bekler ve `Bool` döndürür. Beklemeden doğrudan `exists` okumak, öğe henüz çizilmemişken yanlış cevap alma riskidir.

Bu derste süreleri ölçmeyeceğiz ve bir testin zaman aşımına ne kadar yaklaştığını göstermeyeceğiz. Zamanlama makineden makineye değişir; ona bağlı bir sayı basmak tekrarlanabilir olmaz. Bunun yerine modelde bekleme iki duruma ayrılır: düğmeye basıldıktan hemen sonraki ekran ve ekranın oturduktan sonraki hâli. Gerçek bir testte bu iki an arasında kaç milisaniye geçeceği ölçülmez; modelde yalnız "önce" ve "sonra" vardır.

#### Onay Ekranı Modeli

Aşağıdaki **model**, denetimlerin iki sürüme ne dediğini kurar; arayüz test çerçevesinin kendisi değildir. Ekran bir yapıdır. Düğmenin başlığı, bekleyen siparişler ve ekranda listelenmiş siparişler tutulur. `tapConfirm` düğmeye basmayı, `settle` ekranın oturmasını temsil eder. Doğru sürümde basış bir sipariş ekler; kusurlu sürümde düğmeye basmak hiçbir şey yapmaz.

```swift
// Checkout.swift — onay ekraninin durumu, dogru ve kusurlu surumu
struct CheckoutScreen {
    let confirmTitle = "Siparisi Onayla"
    private(set) var pending: [String] = []
    private(set) var orders: [String] = []

    mutating func tapConfirm(sending: Bool) {
        if sending { pending.append("siparis-1") }
    }

    mutating func settle() {
        orders += pending
        pending = []
    }
}

func screenAfterTap(sending: Bool, settled: Bool) -> CheckoutScreen {
    var screen = CheckoutScreen()
    screen.tapConfirm(sending: sending)
    if settled { screen.settle() }
    return screen
}
```

Ekran oturmuş durumda üç denetim uygulanır. İlki **metne bağlıdır**: düğmenin başlığını, uygulamanın eski sürümündeki yazımla ("Siparisi onayla") karşılaştırır. Metin sonradan bir yazı düzeltmesiyle büyük harfle değişmiştir ve iki sürüm de yeni metni taşır. İkincisi **varlığa bağlıdır**: düğme ekranda var mı. Üçüncüsü **sonuca bağlıdır**: düğmeye basıldıktan sonra sipariş listesi bir sipariş uzamış mı.

```swift
// main.swift — uc denetim, iki surum
let correct = screenAfterTap(sending: true, settled: true)
let faulty = screenAfterTap(sending: false, settled: true)

let oldTitle = contrast("eski-baslik-metni", correct: correct.confirmTitle, faulty: faulty.confirmTitle, check: { $0 == "Siparisi onayla" }).describe()
let exists = contrast("dugme-var", correct: !correct.confirmTitle.isEmpty, faulty: !faulty.confirmTitle.isEmpty, check: { $0 }).describe()
let listGrew = contrast("siparis-listesi-uzadi", correct: correct.orders.count, faulty: faulty.orders.count, check: { $0 == 1 }).describe()

countSeparations([oldTitle, exists, listGrew])
```

```
eski-baslik-metni | dogru=Siparisi Onayla/kaldi kusurlu=Siparisi Onayla/kaldi -> gurultulu
dugme-var | dogru=true/gecti kusurlu=true/gecti -> kor
siparis-listesi-uzadi | dogru=1/gecti kusurlu=0/kaldi -> ayiran
ayiran: 1
kor: 1
gurultulu: 1
ters: 0
```

`eski-baslik-metni` gürültülü: iki sürüm de "Siparisi Onayla" başlığını taşır ve denetim eski yazımı bekler, o yüzden ikisini de reddeder. Düğme çalışıyordur, test kızar. `dugme-var` kör: düğme iki sürümde de ekranda. `siparis-listesi-uzadi` ayıran: doğru sürümde liste bir sipariş taşır, kusurlu sürümde sıfır taşır.

Bu üç denetimin üçü de arayüz testinde yazılabilir ve üçü de bir test raporunda aynı görünür. Biri uygulamadaki kopyalama değişikliğinde boş yere kırılır, biri düğme hiç çalışmasa da yeşil kalır, biri ise düğmenin işini yaptığını söyler. Fark, denetimin neye baktığıdır: metne mi, varlığa mı, sonuca mı.

**TS1.** Ekran modeli tek bir sipariş ekler. Birden çok sipariş, silme ya da iptal ayrıca ölçülmelidir.

**TS2.** `eski-baslik-metni` denetimini gürültülü yapan şey iki sürümün de yeni metni taşımasıdır. Metin yalnız kusurlu sürümde değişseydi aynı denetim ters ya da ayıran olabilirdi.

#### Metne Bağlı Denetim

Metne bağlı bir test düğmeyi görünen metniyle bulur. Bu rahattır, çünkü ekranda ne yazıyorsa testte de odur. Ama metin pazarlamanın, çevirinin ya da yazı düzeltmesinin elindedir. Başlık değiştiğinde uygulama doğru çalışmaya devam eder, test ise kızar. Bu tür bir kırılma bir kusuru göstermez; bakım yükü yaratır.

Üstelik çok dilli bir uygulamada aynı düğmenin metni dile göre değişir. Metne bağlı bir test yalnız yazıldığı dilde çalışır. Test planının ikinci bir dil yapılandırması eklendiğinde metne bağlı testler ikinci dilde kalır; bu konunun sonraki dersinde ölçeceğiz. Tanımlayıcıyla bulunan düğme ise her dilde aynı adı taşır.

Metni testten tamamen atmak da çözüm değildir. Bazı testler metnin kendisini sınar: sipariş onaylandığında ekranda "Siparişiniz alındı" yazması bir davranıştır. Fark şuradadır: bir öğeyi bulmak için metin kullanmak bakım yükü getirir, bir davranışı doğrulamak için kullanmak getirmez.

#### Sonuca Bağlı Denetim

`siparis-listesi-uzadi` ayırdı, çünkü düğmenin ürettiği etkiye baktı. Düğme gerçekten çalışıyorsa liste uzar; çalışmıyorsa uzamaz. Bir arayüz testi yazarken ilk soru "düğme var mı" değil "düğmeye basınca ne olmalı" olmalıdır.

Sonuca bağlı denetimin bedeli vardır. Sipariş listesinin ekranda göründüğü yer değişirse, yeni bir sekmeye taşınırsa ya da tablo bir koleksiyona dönüşürse test yine kırılır. Bu yüzden tanımlayıcı sabit tutulur ve ekranın yapısı değişirken tanımlayıcı taşınır. Sonuca bağlı denetim ayırır ama bakımı da ister.

Arayüz testleri yavaştır. Her biri uygulamayı başlatır, ekranı sürer ve bitirir. Bu yüzden sayıları az, kapsamları geniş tutulur: birim testleri kod mantığını sınar, arayüz testleri uçtan uca birkaç temel yolu sınar. Bir sınır kusuru için arayüz testi yazmak yanlış araçtır; bir bağlantı kusuru için birim testi yazmak da öyle.

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

Aynı sonuca bağlı denetim, bekleme anına göre farklı davranır. Aşağıda `siparis-listesi-uzadi` denetimi iki ekranda uygulanır: düğmeye basılır basılmaz bakılan ekranda ve ekran oturduktan sonra bakılan ekranda. İlkine `anlik-bakis`, ikincisine `bekledikten-sonra` adını verdik.

```swift
// main.swift — ayni denetim, iki bakis ani
let immediateCorrect = screenAfterTap(sending: true, settled: false)
let immediateFaulty = screenAfterTap(sending: false, settled: false)
let settledCorrect = screenAfterTap(sending: true, settled: true)
let settledFaulty = screenAfterTap(sending: false, settled: true)

let immediate = contrast("anlik-bakis", correct: immediateCorrect.orders.count, faulty: immediateFaulty.orders.count, check: { $0 == 1 }).describe()
let afterWait = contrast("bekledikten-sonra", correct: settledCorrect.orders.count, faulty: settledFaulty.orders.count, check: { $0 == 1 }).describe()

countSeparations([immediate, afterWait])
```

```
anlik-bakis | dogru=0/kaldi kusurlu=0/kaldi -> gurultulu
bekledikten-sonra | dogru=1/gecti kusurlu=0/kaldi -> ayiran
ayiran: 1
kor: 0
gurultulu: 1
ters: 0
```

Aynı denetim, ekran oturmadan uygulandığında gürültülü oldu: doğru sürüm bile sıfır sipariş gösterir ve test kalır. Bekledikten sonra aynı denetim ayırandır. Denetimin değeri yalnız neye baktığına değil, ne zaman baktığına da bağlıdır.

Modelde iki an kesin ve belirlenimlidir: önce ve sonra. Gerçek bir uygulamada oturma anı değişir; test bazı koşumlarda beklemeyi yetiştirir, bazılarında yetiştiremez. Böyle sonuçları belirsiz olan teste **kırılgan test** (flaky test) denir ve bu dersin modelinde basılmaz; çünkü model her koşumda aynı sonucu verir ve belirsizliği temsil etmez. Kırılgan bir testi çözmek için `waitForExistence(timeout:)` gibi açık beklemeler kullanılır; keyfi bir süre uyumak ise sorunu gizler.

**TS3.** Model oturmayı tek bir adım sayar. Gerçekte liste birden çok adımda yenilenebilir ve ara durumlar görülebilir.

**TS4.** `anlik-bakis` denetiminin gürültülü çıkması modelin iki ekranı da aynı anda boş saymasından gelir. Gerçek bir koşumda ekran bazen hazır olabilir ve sonuç değişir.

#### Sık Yapılan Hatalar

- Düğmeyi yalnız metniyle bulmak. Başlık değiştiğinde ya da uygulama başka dilde açıldığında test kırılır.
- Yalnız düğmenin ekranda olduğunu denetlemek. Kör kalır ve işlevsiz düğmeyi kaçırır.
- Sabit süre uyuyarak beklemek. Süre kısaysa test kırılgan olur, uzunsa gereksiz yavaşlar.
- Her testi uygulamanın önceki koşumdan kalan durumundan başlatmak. Başlangıç durumu bayraklarla sıfırlanmalıdır.
- Bir sınır kusurunu arayüz testiyle aramak. Bunun için birim testi daha hızlı ve daha kesindir.

#### Özet

- Arayüz testi uygulamayı ayrı bir süreç olarak sürer ve ekrana yalnız görünen öğeler üzerinden erişir.
- Öğeler metinle, sıra numarasıyla ya da erişilebilirlik tanımlayıcısıyla bulunur; tanımlayıcı metin değişse de aynı kalır.
- Üç denetimden biri gürültülü, biri kör, biri ayıran çıktı; yalnız sonuca bağlı denetim düğmenin sipariş gönderdiğini gösterdi.
- Aynı denetim, ekran oturmadan uygulandığında gürültülü oldu; bekleme anı denetimin kategorisini değiştirdi.
- Sonuç belirsiz olan kırılgan testler modelde basılmaz; çözümleri açık beklemedir, keyfi süre değil.
- Bu dersin ayrımı: **gürültülü**.

#### Sonraki Adım

Bu derste denetimleri tek tek yazdık. Sıradaki derste Test Planları ve Kapsam, tüm testleri bir plan altında toplayıp hangi satırların koştuğunu sayacak ve koşan bir satırın doğru olduğunu söylemediğini ölçecek.

### Test Planları ve Kapsam

Source: https://academia.sh/tr/kurslar/ios-uretim/test-planlari-ve-kapsam

Test planı yapılandırmaları ve kapsam raporunun okunması; koşan satır sayısını denetleyen kapsam kuralının ve iki dil yapılandırmasının doğru ve kusurlu sürümü ayırıp ayırmadığının ölçümü.

Önceki derste (Arayüz Testleri) bir düğmenin sonucunu denetleyen testin ayırdığını, yalnız varlığını denetleyen testin ayırmadığını gördük. Bu ders bir üst katmana çıkar: yüzlerce testi bir araya getiren plana ve o testlerin kodun ne kadarına dokunduğunu söyleyen kapsam raporuna.

Ölçü ekseni **ayrımdır**: aynı denetimi önce doğru, sonra kusurlu sürüme uygularız. **Ayıran** denetim doğru sürümde geçer, kusurlu sürümde kalır. **Kör** denetim ikisinde de geçer. **Gürültülü** denetim ikisinde de kalır. **Ters** denetim doğru sürümde kalır, kusurlu sürümde geçer. Çekirdek önceki derslerle aynıdır; her ders kendi başına çalıştığı için yeniden yazıyoruz.

```swift
// 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` iki çıktıyı ve denetimin her biri hakkındaki kararını tutar. `separation` etiketi bu iki karardan türer; `contrast` aynı kapanışı (closure) iki çıktıya uygular; `countSeparations` dört kategoriyi sayar. **Kural nedeni:** bir denetimin değeri geçmesinde değil, doğru ile kusurlu sürümü ayırmasında yatar.

#### Test Planı

Bir uygulamanın birden çok test hedefi olabilir: birim testleri, arayüz testleri, başarım testleri. **Test planı** (test plan) bunları tek bir dosyada toplar. Plan hangi test hedeflerinin koşacağını, hangi testlerin atlanacağını ve testlerin hangi **yapılandırmalarla** koşacağını söyler. Yapılandırma, aynı testlerin değişen bir ayarla ikinci kez koşmasıdır: ekran dili, bölge, kapsam toplama, rastgele sıra gibi.

Plan bir JSON dosyasıdır ve geliştirme ortamı onu bir tablo olarak gösterir. Komut satırından `xcodebuild test` komutuna `-testPlan` bayrağıyla verilir; böylece aynı plan geliştirme makinesinde de otomatik denetim sunucusunda da aynı testleri koşar.

```json
{
  "configurations": [
    { "name": "Turkce", "options": { "language": "tr", "region": "TR" } },
    { "name": "Ingilizce", "options": { "language": "en", "region": "US" } }
  ],
  "defaultOptions": { "codeCoverage": true },
  "testTargets": [
    { "target": { "name": "ShopTests" } }
  ]
}
```

Bu bir yapılandırma dosyasıdır; ders onu çalıştırmaz. İki yapılandırma var: Türkçe ve İngilizce. `codeCoverage` seçeneği açıkken her koşumda hangi satırların çalıştığı kaydedilir. Plandaki her test iki yapılandırmada da koşar; yani aynı test iki kez sınanır ve iki ayrı sonucu olabilir.

#### Kapsam Neyi Ölçer

**Kapsam** (code coverage) bir test takımı koşarken kodun hangi satırlarına ya da dallarına girildiğini sayar. Rapor satır satır "koştu" ya da "koşmadı" der ve oran verir. Bu bilgi değerlidir: hiç koşmayan bir satır kesinlikle denenmemiştir.

Ama kapsamın söylemediği bir şey var. Bir satırın koşması, sonucunun doğru olduğunu söylemez. Test fonksiyonu çağırır, dönen değere hiç bakmazsa satır koşmuş sayılır ve kapsam yükselir. Kapsam yüksekliğinin **güvence** olduğunu düşünmek buradan doğan yaygın bir yanılgıdır. Düşük kapsam bir eksiği gösterir; yüksek kapsam bir eksiğin olmadığını göstermez.

Aşağıdaki iki test bu farkı gösterir. İlki indirim dalını koşturur ama dönen değeri atar. İkincisi aynı dalı koşturur ve sonucu 180 ile karşılaştırır.

```swift
// indirim testleri — KAYNAK
import XCTest

final class PricingTests: XCTestCase {
    func testDiscountBranchRuns() {
        _ = price(100, coupon: true)
        _ = price(100, coupon: false)
    }

    func testDiscountedTotal() {
        XCTAssertEqual(price(200, coupon: true), 180)
    }
}
```

İki test de kapsam raporunda indirim satırını yeşil gösterir. Fakat yalnız ikincisi indirimin doğru hesaplanıp hesaplanmadığını sorar. İlki, fonksiyonun çökmediğini söylemekten fazlasını söylemez.

#### İndirim Modeli

Aşağıdaki **model**, kapsam sayımının ve denetimin iki sürüme ne dediğini kurar; kapsam aracının kendisi değildir. Gerçek bir araç satır sayılarını derleyiciden alır. Model aynı işi küçük ölçekte yapar: işlevin her dalına bir işaret koyar ve koşan işaretleri bir kümede toplar. Üç işaret var: `giris`, `indirim` ve `duz`.

Doğru sürümde kuponlu sepetten yüzde on düşülür. Kusurlu sürümde bölme yüze çevrilmiştir ve yalnız yüzde bir düşülür. İkisi de aynı dallardan geçer; fark yalnız hesapta.

```swift
// Pricing.swift — indirim kuralinin dogru ve kusurlu surumu, kosan satir sayaci
func correctPrice(_ subtotal: Int, coupon: Bool, hits: inout Set<String>) -> Int {
    hits.insert("giris")
    if coupon {
        hits.insert("indirim")
        return subtotal - subtotal / 10
    }
    hits.insert("duz")
    return subtotal
}

func faultyPrice(_ subtotal: Int, coupon: Bool, hits: inout Set<String>) -> Int {
    hits.insert("giris")
    if coupon {
        hits.insert("indirim")
        return subtotal - subtotal / 100
    }
    hits.insert("duz")
    return subtotal
}

func executedCoverage(_ price: (Int, Bool, inout Set<String>) -> Int) -> String {
    var hits = Set<String>()
    _ = price(100, true, &hits)
    _ = price(100, false, &hits)
    return "\(hits.count) satirdan 3"
}

func discountedTotal(_ price: (Int, Bool, inout Set<String>) -> Int) -> Int {
    var hits = Set<String>()
    return price(200, true, &hits)
}
```

`executedCoverage` ilk testin modelidir: iki çağrıyı yapar, sonucu atar ve kaç işaretin koştuğunu yazar. `discountedTotal` ikinci testin modelidir: aynı dalı koşturur ve sonucu döndürür. Böylece iki denetimi yan yana koyabiliriz. Kapsam denetimi "üç satırdan üçü koştu mu" sorusudur; sonuç denetimi "toplam 180 mi" sorusudur.

```swift
// main.swift — kapsam denetimi ve sonuc denetimi
let coverage = contrast("kapsam-denetimi", correct: executedCoverage(correctPrice), faulty: executedCoverage(faultyPrice), check: { $0 == "3 satirdan 3" }).describe()
let outcome = contrast("indirim-sonucu", correct: discountedTotal(correctPrice), faulty: discountedTotal(faultyPrice), check: { $0 == 180 }).describe()

countSeparations([coverage, outcome])
```

```
kapsam-denetimi | dogru=3 satirdan 3/gecti kusurlu=3 satirdan 3/gecti -> kor
indirim-sonucu | dogru=180/gecti kusurlu=198/kaldi -> ayiran
ayiran: 1
kor: 1
gurultulu: 0
ters: 0
```

`kapsam-denetimi` kör: iki sürümde de üç satırdan üçü koştu ve kapsam eşiği geçti. Kusurlu sürümün kapsamı doğru sürümle aynıdır, çünkü kusur hangi satırın koştuğunda değil, o satırın ne hesapladığındadır. `indirim-sonucu` ayıran: doğru sürüm 180, kusurlu sürüm 198 verir ve denetim yalnız 180'i kabul eder.

Bu iki satır tek başına bir kapsam eşiği kararını sorgular. Bir takım "kapsam yüzde yüz olmadan birleştirme yok" kuralı koyduğunda, kusurlu sürüm bu kuralı geçer. Kural eksik testleri yakalar ama yanlış testleri yakalamaz.

**TS1.** Model üç işaretli tek bir işlevi sayar. Gerçek kodda binlerce satır ve dal vardır; ama tek bir işlevdeki fark, büyük bir tabloda da aynı biçimde gizlenir.

**TS2.** Kapsam denetimi burada satır işaretlerine bakar. Dal kapsamı ya da koşul kapsamı daha ince ölçer, ama onlar da hesabın doğruluğunu ölçmez.

#### Kapsam Eşiği Nasıl Kullanılır

Kapsamın işe yarayan kullanımı bir tabandır, hedef değildir. Düşük kapsamlı bir dosya, hiç denenmemiş kodu gösterir; oraya test yazmak sonuç denetimi eklemenin başlangıcıdır. Yüksek kapsam ise bir sonuç değildir. Kapsamı yükseltmek için iddiasız testler yazmak ölçüyü bozar: sayı artar, güvence artmaz.

Bu yüzden bir test kodu gözden geçirilirken iki soru sorulur. Birincisi, test hangi satırlara giriyor. İkincisi, test hangi sonuçları denetliyor. Kapsam yalnız birinciyi cevaplar. İkincisi için aynı testin kusurlu sürüme uygulanmış hâline bakmak gerekir; bu dersin ayrım ölçüsü tam olarak onu yapar.

Bir de yanlış güven sorunu var. Ekipler çoğu zaman kapsam yüzdesini bir kalite göstergesi gibi panoya koyar. Gösterge yükseldikçe ekip testlerin yeterli olduğuna inanır ve yeni test yazma çabası düşer. Kapsamın gerçek işlevi, testlerin hiç dokunmadığı kod parçalarını görünür kılmaktır.

#### Yapılandırmalar ve Beklenti

Test planının ikinci işlevi yapılandırmadır. Aynı test iki dilde koşar. Para tutarı Türkçe bölgede `180,00`, İngilizce bölgede `180.00` biçiminde yazılır; tarih biçimi de benzer biçimde değişir. Bir test beklenen metni yalnız bir dile göre yazdıysa, öbür yapılandırmada doğru sürüm bile kalır.

Modelde biçimlendirme gerçek bir biçimlendirici değil, iki dil için sabit bir kuraldır: Türkçede ondalık ayırıcı virgül, İngilizcede nokta. Gerçek bir uygulamada bu iş platformun biçimlendiricisine bırakılır.

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

Aşağıdaki ölçüm `indirim-sonucu` denetiminin metin biçimini iki yapılandırmada uygular. Beklenen metin `180,00` olarak Türkçe yapılandırmaya göre yazılmıştır. İki sürüm de her yapılandırmada ayrı ayrı biçimlendirilir.

```swift
// main.swift — ayni beklenti, iki yapilandirma
func formattedTotal(_ total: Int, language: String) -> String {
    language == "tr" ? "\(total),00" : "\(total).00"
}

let correctTotal = discountedTotal(correctPrice)
let faultyTotal = discountedTotal(faultyPrice)

let turkish = contrast("tr-yapilandirmasi", correct: formattedTotal(correctTotal, language: "tr"), faulty: formattedTotal(faultyTotal, language: "tr"), check: { $0 == "180,00" }).describe()
let english = contrast("en-yapilandirmasi", correct: formattedTotal(correctTotal, language: "en"), faulty: formattedTotal(faultyTotal, language: "en"), check: { $0 == "180,00" }).describe()

countSeparations([turkish, english])
```

```
tr-yapilandirmasi | dogru=180,00/gecti kusurlu=198,00/kaldi -> ayiran
en-yapilandirmasi | dogru=180.00/kaldi kusurlu=198.00/kaldi -> gurultulu
ayiran: 1
kor: 0
gurultulu: 1
ters: 0
```

Türkçe yapılandırmada denetim ayırır. İngilizce yapılandırmada aynı denetim gürültülü olur: doğru sürüm de `180.00` ürettiği için kalır. Plan iki yapılandırmayı birlikte koştuğunda rapor kırmızıdır ve kırmızıya sebep kusur değildir. Takım bu kırmızıyı bir süre sonra alışılmış görür ve gerçek bir kusur çıktığında onu da aynı alışkanlıkla geçer.

Çözüm beklentiyi dile bağlı yazmamaktır: ya beklenen metin yapılandırmanın diline göre üretilir ya da denetim metne değil sayısal değere bakar. Bu ölçüm yalnız iki yapılandırmayı karşılaştırır; ikisi de aynı kusuru taşıyan iki sürümle koşar.

**TS3.** Gerçek bir kapsam raporunun yüzdesini bu dersin hiçbir yerinde ölçmedik. Model koşan işaretleri kendisi sayar; yüzde bir ürün çıktısıdır ve bu dersin ölçüsü değildir.

**TS4.** Modeldeki biçimlendirme iki dil için sabit bir kuraldır. Gerçek biçimlendirici daha çok bölge ve para birimi ayrıntısı taşır.

#### Sık Yapılan Hatalar

- Kapsam yüzdesini kalite göstergesi saymak. Yüzde, sonucun doğruluğunu söylemez.
- Kapsamı yükseltmek için sonucu denetlemeyen testler yazmak.
- Beklenen metni tek dile göre yazıp planı iki dille koşmak. İkinci yapılandırma sürekli kırmızı kalır.
- Yapılandırmadaki kırmızıyı alışılmış sayıp bakmamak.
- Plana her testi ve her yapılandırmayı eklemek. Plan hızlı kalmazsa takım onu koşmaz.

#### Özet

- Test planı test hedeflerini ve yapılandırmaları tek dosyada toplar; aynı test her yapılandırmada ayrı koşar.
- Kapsam hangi satırın koştuğunu sayar, sonucun doğruluğunu sormaz.
- İki denetimden biri kör, biri ayıran çıktı: kapsam denetimi iki sürümde de geçti, sonuç denetimi yalnız doğru sürümde geçti.
- Beklenti Türkçe biçime göre yazılınca aynı denetim ikinci yapılandırmada gürültülü oldu.
- Kapsam bir tabandır; eksik testi gösterir, yanlış testi göstermez.
- Bu dersin ayrımı: **kör**.

#### Sonraki Adım

Testler kodun doğruluğunu sınar; hızını sınamaz. Sıradaki derste Profil Çıkarma Araçları, bir liste hücresinin her çizimde yaptığı işi sayacak ve iş miktarına bakan denetimin ne zaman ayırdığını ölçecek.

### Profil Çıkarma Araçları

Source: https://academia.sh/tr/kurslar/ios-uretim/profil-cikarma-araclari

Profil aracının zaman çizelgeleri ve işaretlerle uygulamanın nerede zaman harcadığını göstermesi; iş birimi bütçesinin ve çizilen metin denetiminin doğru ve kusurlu sürümü ayırıp ayırmadığının ölçümü.

Önceki derste (Test Planları ve Kapsam) bir testin hangi satıra girdiğinin, o satırın doğru olduğunu söylemediğini gördük. Bu derste doğru olup yavaş olan koda bakıyoruz. Bir liste hücresi her çizildiğinde aynı biçimlendiriciyi baştan kuruyorsa, ekranda görünen yazı doğrudur ve test yeşil kalır; ama kaydırma sırasında uygulama gereğinden çok iş yapar.

Ölçü ekseni yine **ayrımdır**: aynı denetimi doğru sürüme ve kusurlu sürüme uygularız. **Ayıran** denetim doğru sürümde geçer, kusurlu sürümde kalır. **Kör** denetim ikisinde de geçer. **Gürültülü** denetim ikisinde de kalır. **Ters** denetim doğru sürümde kalır, kusurlu sürümde geçer. Çekirdeği, her ders kendi başına çalıştığı için, yeniden veriyoruz.

```swift
// 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` iki sürümün çıktısını ve denetimin her biri üzerindeki kararını tutar; `separation` etiketi bu iki karardan türer. `contrast` aynı kapanışı (closure) iki çıktıya uygular, `countSeparations` dört kategoriyi sayar. **Kural nedeni:** bir denetimin değeri geçmesinde değil, iki sürümü ayırmasındadır.

#### Profil Çıkarma Nedir

**Profil çıkarma** (profiling) çalışan bir uygulamanın zamanını, belleğini ve çizim işini ölçmektir. Test bir soruya evet ya da hayır der; profil aracı "nerede" sorusuna cevap verir. Tek bir isteği, tek bir kaydırmayı ya da tek bir ekranı kaydedersiniz ve araç kaydın zaman çizelgesini gösterir.

Profil çıkarmanın iki kuralı vardır. Birincisi, ölçüm yayın yapılandırmasıyla alınır; hata ayıklama yapılandırması eniyileştirmeleri kapatır ve sayıları çarpıtır. İkincisi, ölçüm mümkünse gerçek aygıtta alınır; simülatör masaüstü işlemcisinde koşar ve gerçek aygıtın bellek ve çizim sınırlarını taşımaz.

Profil aracı birden çok çizelge sunar. **İşlemci çizelgesi** hangi işlevlerin zaman harcadığını çağrı ağacı olarak gösterir; en ağır çağrı çoğu zaman aranan yerdir. **Bellek çizelgesi** ayrılan nesneleri ve bunların ömrünü izler. **Çizim çizelgesi** her karenin ne kadar sürede hazırlandığını gösterir. Her çizelge farklı bir soruya cevap verir: işlemci "ne çalışıyor", bellek "ne birikiyor", çizim "ekran yetişiyor mu".

#### İşaretlerle Ölçmek

Araç tüm uygulamayı gösterir, ama sizin ilgilendiğiniz bölüm küçük olabilir. **İşaret** (signpost) koda koyduğunuz bir başlangıç ve bitiş çiftidir; araç bu çifti zaman çizelgesinde adlı bir aralık olarak gösterir.

```swift
// hucre cizim isareti — KAYNAK
import os

let signposter = OSSignposter(subsystem: "com.example.shop", category: "list")

func configure(cell: PriceCell, price: Decimal) {
    let state = signposter.beginInterval("configure")
    cell.show(price)
    signposter.endInterval("configure", state)
}
```

Bu parça çalıştırılmaz; profil aracının içinde anlam taşır. Aralığın adı ve kategorisi çizelgede süzgeç olarak kullanılır. İşaret koymanın bedeli düşüktür ve işaretler yayın yapılandırmasında bırakılabilir. Eski, işlev tabanlı biçimi `os_signpost` ile yazılır; yeni biçim `OSSignposter` nesnesini kullanır. İkisi de aynı şeyi yapar.

Şimdi kusurlu hücreyi görelim. Her çağrıda yeni bir biçimlendirici kurmak bir fiyat hücresinde en sık görülen yavaşlıklardan biridir, çünkü biçimlendirici pahalı bir nesnedir.

```swift
// hucre yapilandirmasi — KAYNAK
import UIKit

final class PriceCell: UITableViewCell {
    func show(_ price: Decimal) {
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        textLabel?.text = formatter.string(from: price as NSDecimalNumber)
    }
}
```

`show(_:)` her çağrıldığında bir `NumberFormatter` kurar. Liste kaydırılırken yüzlerce hücre çizilir, yani yüzlerce biçimlendirici kurulur. Düzeltme biçimlendiriciyi bir kez kurmak ve her hücrede paylaşmaktır.

```swift
// hucre yapilandirmasi, duzeltilmis — KAYNAK
import UIKit

final class PriceCell: UITableViewCell {
    private static let formatter: NumberFormatter = {
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        return formatter
    }()

    func show(_ price: Decimal) {
        textLabel?.text = Self.formatter.string(from: price as NSDecimalNumber)
    }
}
```

İki sürüm ekranda aynı metni gösterir. Fark yalnız kaç kez nesne kurulduğundadır.

#### İş Birimi Modeli

Aşağıdaki **model**, profil aracının ne ölçtüğünü kurar; aracın kendisi değildir. Süre ölçmeyeceğiz: milisaniye makineden makineye değişir ve burada basılan bir sayı tekrarlanamaz. Bunun yerine her işi bir **iş birimi** olarak sayarız. Bir biçimlendirici kurmak bir birim, bir fiyatı biçimlendirmek bir birimdir. Hücre sayısı aynıyken kurulan nesne sayısı iş birimini belirler.

Doğru sürüm biçimlendiriciyi bir kez kurar ve her satırda yeniden kullanır. Kusurlu sürüm her satırda yeniden kurar. Yirmi satır çizilir.

```swift
// Cells.swift — fiyat hucresinin dogru ve kusurlu surumu, is birimi sayaci
struct Rendered {
    let firstRow: String
    let units: Int
}

func formatPrice(_ cents: Int) -> String {
    let rest = cents % 100
    return "\(cents / 100),\(rest < 10 ? "0" : "")\(rest)"
}

func renderRows(count: Int, rebuildsFormatter: Bool) -> Rendered {
    var units = 0
    var first = ""
    if !rebuildsFormatter { units += 1 }
    for row in 1...count {
        if rebuildsFormatter { units += 1 }
        units += 1
        if row == 1 { first = formatPrice(1000 + row * 25) }
    }
    return Rendered(firstRow: first, units: units)
}
```

`renderRows` yirmi satırı çizer. Doğru sürümde biçimlendirici bir kez kurulur (bir birim), sonra her satır bir birim harcar. Kusurlu sürümde her satır iki birim harcar. `firstRow` ilk satırın metnidir; iki sürümde de aynı fiyat çıkar.

İki denetim uygulanır. Birincisi **çizilen metne** bakar: ilk satır `10,25` mi. İkincisi **iş birimi bütçesine** bakar: toplam birim yirmi beşi aşıyor mu. Yirmi beş bu dersin kuralıdır; gerçek bir bütçe değildir.

```swift
// main.swift — cizilen metin ve is birimi butcesi
let correct = renderRows(count: 20, rebuildsFormatter: false)
let faulty = renderRows(count: 20, rebuildsFormatter: true)

let text = contrast("cizilen-metin", correct: correct.firstRow, faulty: faulty.firstRow, check: { $0 == "10,25" }).describe()
let budget = contrast("is-birimi-butcesi", correct: correct.units, faulty: faulty.units, check: { $0 <= 25 }).describe()

countSeparations([text, budget])
```

```
cizilen-metin | dogru=10,25/gecti kusurlu=10,25/gecti -> kor
is-birimi-butcesi | dogru=21/gecti kusurlu=40/kaldi -> ayiran
ayiran: 1
kor: 1
gurultulu: 0
ters: 0
```

`cizilen-metin` kör: iki sürüm de aynı fiyatı gösterir. `is-birimi-butcesi` ayıran: doğru sürüm yirmi bir birim harcar ve bütçeye sığar, kusurlu sürüm kırk birim harcar ve sığmaz. Kırk, yirmi birin neredeyse iki katıdır ve fark tam olarak her satırda yeniden kurulan biçimlendiriciden gelir.

Burada önemli bir sonuç var: görünen davranışı denetleyen hiçbir test bu kusuru bulamaz. Birim testi de, arayüz testi de, kapsam eşiği de bu kusurlu hücreyi onaylar. Kusuru yalnız iş miktarına bakan bir denetim ya da profil aracının çizelgesi görür.

**TS1.** Model biçimlendiriciyi kurmakla bir fiyatı biçimlendirmeyi aynı maliyette sayar. Gerçek maliyet oranı ölçülmedi ve bu dersin konusu değildir; yalnız kaç kez kurulduğu sayılır.

**TS2.** Model yirmi satır çizer. Satır sayısı arttıkça iki sürüm arasındaki fark büyür; bu dersin ölçüsü yalnız yirmi satırı kapsar.

#### Çizelgeyi Okumak

Bir profil kaydı açıldığında okumanın bir sırası vardır. Önce zaman çizelgesinde yavaşlığın göründüğü aralığı bulun; işaretler bunu kolaylaştırır. Sonra o aralıkta işlemci çizelgesindeki çağrı ağacına bakın ve en çok zaman harcayan çağrıdan başlayın. Çağrı ağacını ters çevirmek, yani en alttaki çağrıları üstte görmek, harcamayı yapan işlevi hızla gösterir.

Yukarıdaki hücrede çağrı ağacı `NumberFormatter` kurulumunu ağır çağrılar arasında gösterebilirdi. Ağacın kendisi bir kusur teşhisi değildir; "burada zaman gidiyor" der. Zamanın neden gittiğini anlamak ve gerekip gerekmediğine karar vermek yine okuyanın işidir.

Profil çıkarırken bir başka tuzak ölçümü ölçmeye bağımlı kılmaktır. Araç kaydı uygulamayı yavaşlatır ve çok sık işaret koymak ölçülen şeyi değiştirir. Bu yüzden kayıt kısa tutulur ve tek bir senaryo ölçülür.

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

İş birimi bütçesi ayıran bir denetimdi. Ama bütçenin sınırı denetimin kategorisini belirler. Aşağıda aynı iki sürüm üç farklı bütçeyle denetlenir: yirmi beş, elli ve on.

```swift
// main.swift — ayni sayac, uc butce
let correct = renderRows(count: 20, rebuildsFormatter: false)
let faulty = renderRows(count: 20, rebuildsFormatter: true)

let tight = contrast("siki-butce", correct: correct.units, faulty: faulty.units, check: { $0 <= 10 }).describe()
let fitting = contrast("uygun-butce", correct: correct.units, faulty: faulty.units, check: { $0 <= 25 }).describe()
let loose = contrast("gevsek-butce", correct: correct.units, faulty: faulty.units, check: { $0 <= 50 }).describe()

countSeparations([tight, fitting, loose])
```

```
siki-butce | dogru=21/kaldi kusurlu=40/kaldi -> gurultulu
uygun-butce | dogru=21/gecti kusurlu=40/kaldi -> ayiran
gevsek-butce | dogru=21/gecti kusurlu=40/gecti -> kor
ayiran: 1
kor: 1
gurultulu: 1
ters: 0
```

On birim çok sıkıdır: doğru sürüm bile aşar ve denetim gürültülü olur. Elli birim çok gevşektir: kusurlu sürüm de sığar ve denetim kör olur. Yalnız yirmi beş doğru sürümün üstünde, kusurlu sürümün altında durur. Aynı sayaç, aynı denetim türü; yalnız eşik değişti ve kategori üç kez değişti.

Bütçe böyle bir denetimin kendisidir. Bütçeyi koyan kişi doğru sürümün harcadığını ölçmelidir; ölçmeden koyulan bir bütçe ya ilk günden kırmızıdır ya da hiçbir zaman kırmızı olmaz. Ve doğru sürüm yavaşlayıp kusurlu sürüme yaklaştıkça bütçenin payı da değişir.

**TS3.** Modelin eşikleri dersin kuralıdır. Gerçek bir bütçenin sayısı yalnız ölçümle bulunur ve her uygulamada başkadır.

**TS4.** Üç ölçüm de aynı yirmi satırlık çizimi kullanır. Başka bir satır sayısında en uygun bütçe başka bir sayı olurdu.

#### Sık Yapılan Hatalar

- Hata ayıklama yapılandırmasında profil çıkarmak. Eniyileştirmeler kapalı olduğu için sayılar yanıltır.
- Yalnız simülatörde ölçmek. Gerçek aygıtın sınırları farklıdır.
- Her şeye işaret koymak. İşaret çizelgeyi doldurur ve ölçülen işi değiştirir.
- Görünen çıktıyı denetleyen testlerin yeşil olmasını başarım garantisi saymak.
- Bütçeyi ölçmeden, tahminle koymak.

#### Özet

- Profil çıkarma "nerede" sorusuna cevap verir; test evet ya da hayır der.
- İşlemci, bellek ve çizim çizelgeleri farklı sorulara karşılık gelir; işaretler ilgilendiğiniz aralığı çizelgede adlandırır.
- İki denetimden biri kör, biri ayıran çıktı: çizilen metin iki sürümde aynıydı, iş birimi bütçesi yalnız doğru sürümde geçti.
- Aynı bütçe denetimi eşiğe göre gürültülü, ayıran ya da kör oldu; bütçe ölçülerek konmalıdır.
- Başarım kusurunu görünen çıktıya bakan testler bulamaz.
- Bu dersin ayrımı: **ayıran**.

#### Sonraki Adım

Zaman kaybı bir başarım sorunudur; bellekte kalan nesne ise ayrı bir kusurdur. Sıradaki derste Bellek Sızıntısı Tespiti, bir ekran kapandıktan sonra hâlâ yaşayan nesneyi tutma döngüsü üzerinden ölçecek.

### Bellek Sızıntısı Tespiti

Source: https://academia.sh/tr/kurslar/ios-uretim/bellek-sizintisi-tespiti

Tutma döngüsünün nasıl oluştuğu, zayıf referansla nasıl kırıldığı ve nesnenin gerçekten serbest kalıp kalmadığının sınanması; iki sızıntı denetiminin doğru ve kusurlu sürümü ayırıp ayırmadığının ölçümü.

Önceki derste (Profil Çıkarma Araçları) görünen çıktısı doğru olan ama gereğinden çok iş yapan bir hücreyi ölçtük. Bu derste kusur zamanda değil bellekte. Bir ekran kapanır, kullanıcı ona bir daha dönmez, ama ekranın modeli bellekte kalır. Ekran doğru veriyi göstermiştir ve hiçbir test kızmaz; yalnız uygulamanın bellek kullanımı her ekran açılışında biraz büyür.

Ölçü ekseni **ayrımdır**: aynı denetimi doğru sürüme ve kusurlu sürüme uygularız. **Ayıran** denetim doğru sürümde geçer, kusurlu sürümde kalır. **Kör** denetim ikisinde de geçer. **Gürültülü** denetim ikisinde de kalır. **Ters** denetim doğru sürümde kalır, kusurlu sürümde geçer. Çekirdek önceki derslerle aynıdır; her ders kendi başına çalıştığı için yeniden veriyoruz.

```swift
// 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` iki sürümün çıktısını ve denetimin her biri hakkındaki kararını tutar; `separation` etiketi bu iki karardan türer. `contrast` aynı kapanışı (closure) iki çıktıya uygular, `countSeparations` dört kategoriyi sayar. **Kural nedeni:** bir denetimin değeri geçmesinde değil, iki sürümü ayırmasında yatar.

#### Tutma Döngüsü

Swift'te nesnelerin ömrünü **referans sayımı** belirler. Her nesne kaç yerden güçlü tutulduğunu sayar ve sayı sıfıra inince bellek serbest kalır. Bir nesneyi başka bir nesne güçlü tutuyorsa ve ikincisi de birincisini güçlü tutuyorsa, ikisinin sayısı da hiçbir zaman sıfıra inmez. Buna **tutma döngüsü** (retain cycle) denir. Dışarıdan kimse nesnelere ulaşamaz, ama nesneler birbirini tutmaya devam eder: **bellek sızıntısı** budur.

Bir kapanış, kendi içinde kullandığı nesneleri varsayılan olarak güçlü yakalar. Bir ekran modeli kapanışı kendi özelliğinde saklıyor ve kapanış `self`'i kullanıyorsa, model kapanışı, kapanış da modeli tutar.

```swift
// ekran modeli — KAYNAK
final class OrderDetailViewModel {
    var onUpdate: (() -> Void)?

    func bind(service: OrderService) {
        service.observe { order in
            self.apply(order)
        }
        onUpdate = {
            self.refresh()
        }
    }
}
```

`onUpdate` kapanışı `self.refresh()` çağırdığı için `self`'i güçlü yakalar; `self` de `onUpdate`'i güçlü tutar. Ekran kapansa bile sayı sıfıra inmez. Çözüm kapanışa bir **yakalama listesi** vermektir ve `self`'i zayıf yakalamaktır.

```swift
// ekran modeli, duzeltilmis — KAYNAK
final class OrderDetailViewModel {
    var onUpdate: (() -> Void)?

    func bind(service: OrderService) {
        service.observe { [weak self] order in
            self?.apply(order)
        }
        onUpdate = { [weak self] in
            self?.refresh()
        }
    }
}
```

`[weak self]` kapanışa `self`'in **zayıf referansını** (weak reference) verir. Zayıf referans nesnenin sayısını artırmaz; nesne serbest kalınca referans kendiliğinden `nil` olur. Bu yüzden kapanışın içinde `self` isteğe bağlı bir değerdir. Bir de `unowned` var: o da sayıyı artırmaz ama nesne gittiyse kullanımı çöker. Nesnenin kapanıştan uzun yaşayacağı kesinse kullanılır; emin olunmadığında `weak` seçilir.

Her kapanış döngü yaratmaz. Bir işlevin parametresi olarak verilen ve hemen çalışıp biten bir kapanış nesneyi bir yerde saklamaz. Döngü, kapanışın saklandığı ve saklayanın da yakalanan nesne olduğu durumlarda oluşur.

#### Sızıntıyı Bulmak

Sızıntıyı bulmanın iki yolu var. Birincisi geliştirme ortamının hata ayıklama alanındaki **bellek grafiği** görünümüdür: koşan uygulamayı durdurur, canlı nesneleri ve aralarındaki bağları oklarla gösterir. İkincisi test içinde yapılır. Test bir nesne kurar, ona zayıf bir gözlemci bağlar, nesnenin güçlü referansını bırakır ve gözlemcinin `nil` olduğunu denetler.

```swift
// sizinti testi — KAYNAK
import XCTest

final class LeakTests: XCTestCase {
    func testViewModelIsReleased() {
        var model: OrderDetailViewModel? = OrderDetailViewModel()
        weak var observer = model
        model?.bind(service: StubOrderService())
        model = nil
        XCTAssertNil(observer)
    }
}
```

Bu parça çalıştırılmaz; yukarıdaki türler ve `StubOrderService` burada tanımlı değildir. Test bir model kurar, ona `weak var observer` ile ikinci bir referans verir ve `model = nil` ile tek güçlü referansı bırakır. Döngü yoksa nesne o anda serbest kalır ve `observer` `nil` olur. Döngü varsa nesne yaşar ve `XCTAssertNil` kalır. Birden çok testte aynı denetimi tekrar etmek için `addTeardownBlock(_:)` kullanılabilir: test bittiğinde nesnenin serbest kalıp kalmadığını denetleyen bir kapanış her testin sonunda koşar.

#### Sızıntı Modeli

Aşağıdaki **model**, serbest kalmanın iki sürüme ne dediğini kurar; bellek grafiği görünümünün kendisi değildir. Model gerçek nesneler ve gerçek referans sayımı kullanır: tutma döngüsü bu ölçümde gerçekten oluşur ve gerçekten kırılır. Bayt, adres ya da bellek miktarı basılmaz; yalnız bir nesnenin serbest kalıp kalmadığı ve kaç nesnenin canlı olduğu sayılır.

Ekran modeli `bind` ile bir kapanış saklar. Doğru sürüm kapanışta `[weak self]` kullanır. Kusurlu sürüm `self`'i güçlü yakalar.

```swift
// Models.swift — ekran modelinin dogru ve kusurlu surumu, canli nesne sayaci
final class LiveCount {
    var screens = 0
    var loaders = 0
}

final class LoaderModel {
    let counts: LiveCount
    var completion: (() -> Void)?

    init(counts: LiveCount) {
        self.counts = counts
        counts.loaders += 1
    }

    deinit { counts.loaders -= 1 }

    func arm(leaking: Bool) {
        if leaking {
            completion = { _ = self.counts }
        } else {
            completion = { [weak self] in _ = self?.counts }
        }
    }
}

final class DetailViewModel {
    let title: String
    let counts: LiveCount
    var onUpdate: (() -> Void)?

    init(title: String, counts: LiveCount) {
        self.title = title
        self.counts = counts
        counts.screens += 1
    }

    deinit { counts.screens -= 1 }

    func bind(captureStrongly: Bool) {
        if captureStrongly {
            onUpdate = { _ = self.title }
        } else {
            onUpdate = { [weak self] in _ = self?.title }
        }
    }

    func startLoading(leaking: Bool) {
        let loader = LoaderModel(counts: counts)
        loader.arm(leaking: leaking)
    }
}

func shownTitle(captureStrongly: Bool) -> String {
    let model = DetailViewModel(title: "siparis", counts: LiveCount())
    model.bind(captureStrongly: captureStrongly)
    return model.title
}

func closeScreen(captureStrongly: Bool) -> Bool {
    weak var observer: DetailViewModel?
    do {
        let model = DetailViewModel(title: "siparis", counts: LiveCount())
        model.bind(captureStrongly: captureStrongly)
        observer = model
    }
    return observer == nil
}

func closeAndCount(leaking: Bool) -> LiveCount {
    let counts = LiveCount()
    do {
        let model = DetailViewModel(title: "siparis", counts: counts)
        model.startLoading(leaking: leaking)
    }
    return counts
}
```

`closeScreen` testin modelidir: `weak var observer` zayıf gözlemcidir, `do` bloğunun bitişi ekranın kapanışıdır ve dönen değer gözlemcinin `nil` olup olmadığıdır. `shownTitle` ekranın gösterdiği başlığı verir. Birincisi **serbest kalmaya**, ikincisi **görünen veriye** bakar. `LiveCount` ve `startLoading` bu dersin ikinci ölçümü içindir.

```swift
// main.swift — gorunen veri ve serbest kalma
let title = contrast("ekran-verisi", correct: shownTitle(captureStrongly: false), faulty: shownTitle(captureStrongly: true), check: { $0 == "siparis" }).describe()
let released = contrast("ekran-serbest-kaldi", correct: closeScreen(captureStrongly: false), faulty: closeScreen(captureStrongly: true), check: { $0 }).describe()

countSeparations([title, released])
```

```
ekran-verisi | dogru=siparis/gecti kusurlu=siparis/gecti -> kor
ekran-serbest-kaldi | dogru=true/gecti kusurlu=false/kaldi -> ayiran
ayiran: 1
kor: 1
gurultulu: 0
ters: 0
```

`ekran-verisi` kör: iki sürüm de aynı başlığı gösterir. Ekranın görünen davranışı iki sürümde birebir aynıdır. `ekran-serbest-kaldi` ayıran: doğru sürümde zayıf gözlemci `nil` olur, kusurlu sürümde nesne yaşamaya devam eder ve gözlemci hâlâ bir nesneyi gösterir.

Bu yüzden bellek sızıntısı sıradan davranış testlerine görünmez. Ekranın ne gösterdiğine bakan hiçbir denetim, ekranın kapandıktan sonra nerede olduğunu söylemez. Döngü modelde de gerçektir: kusurlu sürümde nesne süreç bitene dek kalır, çünkü onu serbest bırakacak kimse yoktur.

**TS1.** Model tek bir kapanış yakalamasını ölçer. Aynı döngü iki nesnenin birbirine verdiği güçlü özelliklerle ya da bir zamanlayıcıyla da oluşabilir; bunlar ayrıca ölçülmelidir.

**TS2.** `closeScreen` kusurlu sürümde gerçekten sızıntı bırakır. Ölçüm tek bir süreçte, kısa ömürlü olduğu için bir zarar vermez; gerçek bir uygulamada her ekran açılışı bir sızıntı daha ekler.

#### Neyin Sayıldığı

Test içinde sızıntıyı bulmanın bir ikinci biçimi, **canlı nesne sayısına** bakmaktır: ekran açılmadan önce sıfır, kapandıktan sonra yine sıfır olmalıdır. Sayaç basit ve güçlü görünür. Ama sayacın neyi saydığı sonucu belirler.

Kapanışlar yalnız ekranın kendisini yakalamaz. Ekran bir yükleyici nesne başlatabilir ve yükleyicinin tamamlanma kapanışı yükleyiciyi yakalıyor olabilir. Bu durumda ekran serbest kalır ama yükleyici sızar.

Aşağıdaki ölçüm bu durumu kurar. Ekran `startLoading` ile bir yükleyici başlatır. Kusurlu sürümde yükleyicinin kapanışı yükleyiciyi güçlü yakalar; doğru sürümde zayıf yakalar. Ekran iki sürümde de kendi başına serbest kalır. Aynı sayaçtan iki değer okunur: canlı ekran sayısı ve canlı yükleyici sayısı.

```swift
// main.swift — ayni sayac, iki sayilan nesne
let correct = closeAndCount(leaking: false)
let faulty = closeAndCount(leaking: true)

let screens = contrast("sayilan-ekran", correct: correct.screens, faulty: faulty.screens, check: { $0 == 0 }).describe()
let loaders = contrast("sayilan-yukleyici", correct: correct.loaders, faulty: faulty.loaders, check: { $0 == 0 }).describe()

countSeparations([screens, loaders])
```

```
sayilan-ekran | dogru=0/gecti kusurlu=0/gecti -> kor
sayilan-yukleyici | dogru=0/gecti kusurlu=1/kaldi -> ayiran
ayiran: 1
kor: 1
gurultulu: 0
ters: 0
```

Canlı ekran sayısı iki sürümde de sıfırdır, çünkü ekran serbest kaldı. Bu denetim kör: sızıntıyı göremez. Canlı yükleyici sayısı doğru sürümde sıfır, kusurlu sürümde birdir ve denetim ayırır. Sayaç aynı, kusur aynı; yalnız sayılan nesne değişti.

Burada ders şudur: bir sızıntı testi, sızdıran nesneyi saymıyorsa sızıntıyı bulamaz. Hangi nesnenin sızabileceğini önceden bilmek zordur; bu yüzden bellek grafiği görünümü, tüm canlı nesnelere bakarak testin atladığı yerleri gösterir. Test sorulan soruyu cevaplar, görünüm sorulmamış soruyu da gösterebilir.

**TS3.** Model yükleyiciyi ekranın bir yardımcısı olarak kurar. Gerçek uygulamada sızıntı çoğunlukla ekranın hiç bilmediği bir katmanda doğar.

**TS4.** Sayaç yalnız iki nesne türünü sayar. Başka türde bir sızıntı bu ölçümde görünmez.

#### Sık Yapılan Hatalar

- Kapanışta `self`'i yakalama listesi vermeden kullanmak ve kapanışı özellikte saklamak.
- Her kapanışa `[weak self]` yazmak. Hemen çalışıp biten bir kapanışta gereksizdir.
- Nesnenin ömrü belirsizken `unowned` kullanmak. Nesne gittiyse uygulama çöker.
- Yalnız ekranın serbest kaldığına bakıp ekranın başlattığı yardımcı nesneleri atlamak.
- Sızıntıyı yalnız davranış testleriyle aramak. Davranış doğru kalırken bellek büyür.

#### Özet

- Tutma döngüsü iki nesnenin birbirini güçlü tutmasıdır; referans sayısı sıfıra inmez ve bellek sızar.
- Kapanış `self`'i güçlü yakalar; `[weak self]` döngüyü kırar, `unowned` ise nesne gittiyse çöker.
- İki denetimden biri kör, biri ayıran çıktı: görünen veri iki sürümde aynıydı, serbest kalma yalnız doğru sürümde gerçekleşti.
- Aynı canlı nesne sayacı, ekranı sayınca kör, sızdıran yükleyiciyi sayınca ayıran oldu.
- Sızıntıyı bulan test, sızdıran nesneyi saymalıdır; bellek grafiği görünümü atlanan nesneleri gösterir.
- Bu dersin ayrımı: **ayıran**.

#### Sonraki Adım

Sızıntı bir kusurdu; başlangıçta yapılan gereksiz iş ise başka bir kusur. Sıradaki derste Başlangıç ve Kaydırma Başarımı, kayıtlı bir temel değerle karşılaştırılan başarım denetiminin hangi sürümden alındığına göre nasıl tersine dönebildiğini ölçecek.

### Başlangıç ve Kaydırma Başarımı

Source: https://academia.sh/tr/kurslar/ios-uretim/baslangic-ve-kaydirma-basarimi

Açılışta yapılan işi ertelemek ve satır nesnelerini yeniden kullanmak; kayıtlı bir temel değerle karşılaştıran başarım denetiminin temel değerin hangi sürümden alındığına göre ayırıp ters dönebildiğinin ölçümü.

Önceki derste (Bellek Sızıntısı Tespiti) kusur, ekran kapandıktan sonra görünen bir nesnenin yaşamaya devam etmesiydi. Bu derste kusur kullanıcının ilk gördüğü iki yerde: uygulama açılırken ve liste kaydırılırken. İkisinde de çıktı doğrudur; ekran açılır, satırlar çizilir. Fark, o sonuca varmak için yapılan işin miktarında.

Başarım denetimleri çoğunlukla bir **temel değerle** (baseline) karşılaştırır: önceki bir koşuda ölçülen değer kaydedilir ve yeni koşunun değeri onu aşmamalıdır. Bu dersin sorusu, temel değerin hangi sürümden alındığının denetimi ne yaptığıdır.

Ölçü ekseni **ayrımdır**: aynı denetimi doğru sürüme ve kusurlu sürüme uygularız. **Ayıran** denetim doğru sürümde geçer, kusurlu sürümde kalır. **Kör** denetim ikisinde de geçer. **Gürültülü** denetim ikisinde de kalır. **Ters** denetim doğru sürümde kalır, kusurlu sürümde geçer. Çekirdek önceki derslerle aynıdır; her ders kendi başına çalıştığı için yeniden veriyoruz.

```swift
// 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` iki sürümün çıktısını ve denetimin her biri hakkındaki kararını tutar; `separation` etiketi bu iki karardan türer. `contrast` aynı kapanışı iki çıktıya uygular, `countSeparations` dört kategoriyi sayar. **Kural nedeni:** bir denetimin değeri geçmesinde değil, iki sürümü ayırmasında yatar.

#### Açılışta Yapılan İş

Kullanıcı simgeye dokunduğunda ilk kare çizilene kadar geçen süre, uygulamanın ilk izlenimidir. Bu sürede çalışan her iş kullanıcıyı bekletir. Açılışta yapılan işin hepsi ilk kare için gerekli değildir: bir analiz kütüphanesini başlatmak, önbelleği ısıtmak ya da ikinci bir ekranın verisini hazırlamak ilk kareden sonraya bırakılabilir.

```swift
// uygulama girisi — KAYNAK
@main
struct ShopApp: App {
    init() {
        configureTheme()
        restoreSession()
        // Ilk kare icin gerekmiyor; sonraya kalir.
        // warmImageCache()
        // startAnalytics()
    }

    var body: some Scene {
        WindowGroup { RootView() }
            .task { await deferredStartup() }
    }
}
```

Bu parça çalıştırılmaz; `configureTheme`, `restoreSession` ve `deferredStartup` burada tanımlı değildir. Görünümün `task` değiştiricisi, görünüm belirmeden önce eşzamansız bir görev başlatır; görev ilk `await` noktasında askıya alınır ve ertelenen işin geri kalanı ilk kareyi bekletmez. Kural basit: ilk kareyi çizmek için gereken iş girişte, gerisi sonrada.

Gerçek uygulamalarda açılış süresi ve kaydırma akıcılığı kullanıcıların telefonlarından da toplanır. Platformun `MetricKit` çerçevesi bu verileri günlük özetler olarak uygulamaya teslim eder.

```swift
// toplanan olcumler — KAYNAK
import MetricKit

final class MetricsReceiver: NSObject, MXMetricManagerSubscriber {
    func didReceive(_ payloads: [MXMetricPayload]) {
        for payload in payloads {
            if let launch = payload.applicationLaunchMetrics {
                record(launch.histogrammedTimeToFirstDraw)
            }
        }
    }
}
```

Özetler gerçek kullanıcıların gerçek koşullarındaki dağılımı verir; geliştirme sırasında alınan tek bir ölçüm bunu veremez. Bu dersin ölçümleri ise koşullara bağlı olmayan, **sayılan işe** dayanır: kaç adım çalıştı, kaç nesne oluşturuldu. Süre ölçülmez ve yazılmaz.

#### Kaydırmada Oluşturulan Nesneler

Uzun bir listede ekranda aynı anda yalnız birkaç satır görünür. Satır ekrandan çıktığında nesnesi atılmaz; bir havuza konur ve yeni görünen satır için yeniden kullanılır. Bu, satır başına nesne oluşturma ve bellek ayırma işini ortadan kaldırır. Yeniden kullanım yoksa her satır için yeni bir nesne oluşturulur ve kaydırma uzadıkça iş büyür.

```swift
// satir olusturma — KAYNAK
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "Row", for: indexPath)
    cell.textLabel?.text = items[indexPath.row].title
    return cell
}
```

`dequeueReusableCell(withIdentifier:for:)` havuzdan boşta bir satır alır, yoksa yenisini oluşturur. Kusurlu sürüm her seferinde yeni bir satır nesnesi kurar.

Aşağıdaki **model**, bunun saf sayım karşılığıdır; bir liste görünümü değildir. Yüz satırlık bir listeyi, aynı anda on satırın göründüğü bir pencereden kaydırır ve kaç satır nesnesi oluşturulduğunu sayar. Doğru sürüm ekrandan çıkan satırı havuza koyar ve sonraki satırda onu kullanır. Kusurlu sürüm havuz kullanmaz.

```swift
// Scroll.swift — kaydirma ve acilis modeli, temel degerli olcum
struct Measured: CustomStringConvertible {
    let value: Int
    let baseline: Int
    var description: String { "\(value) temel=\(baseline)" }
}

func cellsCreated(rows: Int, visible: Int, reuse: Bool) -> Int {
    var pool = 0
    var created = 0
    var onScreen = 0
    for _ in 0..<rows {
        if onScreen == visible {
            onScreen -= 1
            if reuse { pool += 1 }
        }
        if reuse && pool > 0 { pool -= 1 } else { created += 1 }
        onScreen += 1
    }
    return created
}

func drawnRows(rows: Int, reuse: Bool) -> Int {
    rows
}

func launchWork(deferHeavy: Bool) -> Int {
    let light = 3
    let heavy = 5
    return deferHeavy ? light : light + heavy
}
```

`cellsCreated` kaydırmayı döngüyle yürütür: pencere dolunca en eski satır ekrandan çıkar, yeniden kullanım açıksa havuza girer. `drawnRows` iki sürümde de çizilen satır sayısıdır. `launchWork` açılışta ilk kareden önce çalışan adımları sayar: üç hafif adım her zaman, beş ağır adım yalnız ertelenmediğinde. `Measured` bir değeri kaydedilmiş temel değeriyle birlikte tutar.

##### Temel Değerin Kaynağı

Üç denetimi aynı kaydırmaya uygularız. Birincisi çizilen satır sayısına bakar. İkincisi "değer temel değere eşit olmalı" der ve temel değeri **kusurlu** sürümden alır. Üçüncüsü "değer temel değeri aşmamalı" der ve temel değeri **doğru** sürümden alır.

```swift
// main.swift — uc denetim, temel degerin kaynagi
let drawn = contrast("cizilen-satir", correct: drawnRows(rows: 100, reuse: true), faulty: drawnRows(rows: 100, reuse: false), check: { $0 == 100 }).describe()

let faultyBaseline = cellsCreated(rows: 100, visible: 10, reuse: false)
let equalToFaulty = contrast("esit-temel", correct: Measured(value: cellsCreated(rows: 100, visible: 10, reuse: true), baseline: faultyBaseline), faulty: Measured(value: cellsCreated(rows: 100, visible: 10, reuse: false), baseline: faultyBaseline), check: { $0.value == $0.baseline }).describe()

let correctBaseline = cellsCreated(rows: 100, visible: 10, reuse: true)
let notAboveCorrect = contrast("asmaz-temel", correct: Measured(value: cellsCreated(rows: 100, visible: 10, reuse: true), baseline: correctBaseline), faulty: Measured(value: cellsCreated(rows: 100, visible: 10, reuse: false), baseline: correctBaseline), check: { $0.value <= $0.baseline }).describe()

countSeparations([drawn, equalToFaulty, notAboveCorrect])
```

```
cizilen-satir | dogru=100/gecti kusurlu=100/gecti -> kor
esit-temel | dogru=10 temel=100/kaldi kusurlu=100 temel=100/gecti -> ters
asmaz-temel | dogru=10 temel=10/gecti kusurlu=100 temel=10/kaldi -> ayiran
ayiran: 1
kor: 1
gurultulu: 0
ters: 1
```

`cizilen-satir` kör: iki sürüm de yüz satırı çizer, kullanıcının gördüğü liste aynıdır. `esit-temel` ters: temel değer kusurlu sürümden, yani yüz nesneden alınmıştır; doğru sürüm on nesneyle bu değere eşit olmadığı için denetim kalır, kusurlu sürüm ise kendi değerine eşit olduğu için geçer. İyileştirme kırmızı, kusur yeşil görünür. `asmaz-temel` ayıran: temel değer doğru sürümden alınmıştır, doğru sürüm geçer ve kusurlu sürüm aşar.

Ters denetim, ölçülen şey ile temel değer arasındaki ilişkiyi tersine çevirdiği için kör olmaktan daha kötüdür. Kör denetim bir şey söylemez; ters denetim yanlış şey söyler ve ekibi doğru sürümü geri almaya iter. Bu yüzden temel değer kayda alınırken hangi sürümden alındığı da kaydedilir ve "eşit" yerine "aşmaz" denir: bir başarım iyileşmesi hata sayılmamalıdır.

**TS1.** Temel değerin kusurlu sürümden alınması bir tasarım hatasıdır, ama gerçekte sık olur: ekip kusuru bilmeden bir değer kaydeder ve sonra düzeltir.

**TS2.** Model yalnız oluşturulan nesne sayısını ölçer. Gerçek bir listede satırın içeriği, boyutu ve görüntüsü de kaydırmayı etkiler; bunlar sayılmaz.

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

Temel değerin bir sorunu daha var: yenilenmesi. Bazı ekipler denetimin kırmızıya dönmemesi için temel değeri her koşuda mevcut değere çeker. "Değer temel değeri aşmamalı" denetimi bu durumda her zaman geçer.

Aşağıdaki ölçüm üç şeyi yan yana koyar: sabit temel değerle "aşmaz" denetimini, her koşuda yenilenen temel değerle aynı denetimi ve açılışta ilk kareden önce çalışan işe uygulanan sabit temel değerli denetimi.

```swift
// main.swift — sabit ve yenilenen temel deger, acilis
let correctCells = cellsCreated(rows: 100, visible: 10, reuse: true)
let faultyCells = cellsCreated(rows: 100, visible: 10, reuse: false)

let fixed = contrast("sabit-temel", correct: Measured(value: correctCells, baseline: correctCells), faulty: Measured(value: faultyCells, baseline: correctCells), check: { $0.value <= $0.baseline }).describe()
let refreshed = contrast("yenilenen-temel", correct: Measured(value: correctCells, baseline: correctCells), faulty: Measured(value: faultyCells, baseline: faultyCells), check: { $0.value <= $0.baseline }).describe()

let correctLaunch = launchWork(deferHeavy: true)
let launch = contrast("ilk-kare-oncesi-is", correct: Measured(value: correctLaunch, baseline: correctLaunch), faulty: Measured(value: launchWork(deferHeavy: false), baseline: correctLaunch), check: { $0.value <= $0.baseline }).describe()

countSeparations([fixed, refreshed, launch])
```

```
sabit-temel | dogru=10 temel=10/gecti kusurlu=100 temel=10/kaldi -> ayiran
yenilenen-temel | dogru=10 temel=10/gecti kusurlu=100 temel=100/gecti -> kor
ilk-kare-oncesi-is | dogru=3 temel=3/gecti kusurlu=8 temel=3/kaldi -> ayiran
ayiran: 2
kor: 1
gurultulu: 0
ters: 0
```

`sabit-temel` önceki ölçümün ayıran denetimidir. `yenilenen-temel` kör: kusurlu sürümün temel değeri de kendi değerine çekildiği için yüz nesne yüz nesneyi aşmaz. Denetim hiçbir zaman kırılmadığı için bir şeyi koruduğunu sanmak yanıltıcıdır. `ilk-kare-oncesi-is` ayıran: ertelenmemiş açılış sekiz adım çalıştırır ve sabit üç adımlık temel değeri aşar.

Temel değeri yenilemek kendi başına yanlış değildir; bilinçli bir iyileşmeden sonra yeni bir temel kaydetmek gerekir. Yanlış olan, bunu otomatik ve her koşuda yapmaktır. Temel değer bir kararla değişir, bir koşuyla değil.

**TS3.** Model "adım" ve "nesne" sayar. Gerçek bir açılışta adımların süresi eşit değildir; tek bir ağır adım sekizin hepsinden uzun sürebilir.

**TS4.** Açılış ölçümü yalnız ilk kareden önceki işi sayar. İlk kareden sonra ertelenen işin de bir bedeli vardır; ertelemek onu ortadan kaldırmaz.

#### Sık Yapılan Hatalar

- Temel değeri kusurlu ya da bilinmeyen bir sürümden kaydetmek ve "eşit" demek.
- Temel değeri her koşuda mevcut değere çekmek. Denetim hiçbir zaman kırılmaz.
- Açılışta ilk kare için gerekmeyen işi girişte yapmak.
- Her satır için yeni nesne oluşturmak; yeniden kullanım havuzunu atlamak.
- Tek bir yerel ölçümü kullanıcıların koşullarının temsili saymak.

#### Özet

- İlk kareden önceki iş kullanıcıyı bekletir; yalnız ilk kare için gerekenler girişte yapılır, gerisi ertelenir.
- Satır nesnelerinin yeniden kullanımı, kaydırmada oluşturulan nesne sayısını görünen satır sayısında tutar.
- Temel değer kusurlu sürümden alınırsa "eşit" denetimi ters olur: iyileştirme kırmızı, kusur yeşil görünür.
- Temel değer doğru sürümden alınırsa "aşmaz" denetimi ayırır; her koşuda yenilenen temel değer ise denetimi kör bırakır.
- Temel değer bir kararla değişir, bir koşuyla değil.
- Bu dersin ayrımı: **ters**.

#### Sonraki Adım

Başarım denetimi bir kural olarak yazılabilir; kod kalitesi denetimleri de öyle. Sıradaki derste Kod Kalitesi Araçları, biçim denetiminin ve kaynak metni tarayan bir kuralın aynı kusuru ayırıp ayıramadığını ölçecek.

### Kod Kalitesi Araçları

Source: https://academia.sh/tr/kurslar/ios-uretim/kod-kalitesi-araclari

Biçimlendirici ile statik çözümleme kurallarının görevleri ve sınırları; biçim denetiminin ve kaynak metni tarayan bir kuralın aynı kusuru ayırıp ayıramadığının, yanlış alarmın denetimi nasıl bozduğunun ölçümü.

Önceki derste (Başlangıç ve Kaydırma Başarımı) kusur, ölçülen bir işin miktarındaydı. Bu derste kusur kaynak metnin kendisinde: kod derlenir, testler geçer, ama bir satır uygun koşullarda uygulamayı çökertir. Kod kalitesi araçları kodu çalıştırmadan, metnine bakarak denetler.

İki tür araç vardır. **Biçimlendirici** boşlukları, girintiyi ve satır sonlarını düzenler; kodun ne yaptığına bakmaz. **Statik çözümleme** aracı ise kaynak metni okur ve tanımlı kurallara uymayan yapıları bulur: kullanılmayan değişken, çok uzun işlev, zorla açılan isteğe bağlı değer. İkisi de ekibin ortak bir düzende yazmasını sağlar; ama hangi kusuru yakaladıkları farklıdır.

Ölçü ekseni **ayrımdır**: aynı denetimi doğru sürüme ve kusurlu sürüme uygularız. **Ayıran** denetim doğru sürümde geçer, kusurlu sürümde kalır. **Kör** denetim ikisinde de geçer. **Gürültülü** denetim ikisinde de kalır. **Ters** denetim doğru sürümde kalır, kusurlu sürümde geçer. Çekirdek önceki derslerle aynıdır; her ders kendi başına çalıştığı için yeniden veriyoruz.

```swift
// 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` iki sürümün çıktısını ve denetimin her biri hakkındaki kararını tutar; `separation` etiketi bu iki karardan türer. `contrast` aynı kapanışı iki çıktıya uygular, `countSeparations` dört kategoriyi sayar. **Kural nedeni:** bir denetimin değeri geçmesinde değil, iki sürümü ayırmasında yatar.

#### Zorla Açma

Swift'te isteğe bağlı bir değer `nil` olabilir. Ünlem işaretiyle zorla açmak, değerin `nil` olmadığını varsaymaktır; varsayım yanlışsa uygulama o satırda çöker. Kusurlu sürüm bir metni sayıya çevirir ve sonucu zorla açar. Doğru sürüm `guard let` ile `nil` durumunu ele alır.

```swift
// kusurlu ve duzeltilmis — KAYNAK
func total(of text: String) -> Int {
    let value = Int(text)!
    return value
}

func total(of text: String) -> Int {
    guard let value = Int(text) else { return 0 }
    return value
}
```

Kullanıcının girdiği "12" metni iki sürümde de on ikiye çevrilir. Girdi "on iki" olduğunda kusurlu sürüm çöker, doğru sürüm sıfır döner. Test bu girdiyi hiç denemediyse iki sürüm de geçer; kusur üretimde bir kullanıcının beklenmedik girdisinde ortaya çıkar.

#### Biçim Denetimi ve Kural Denetimi

Aşağıdaki **model**, iki aracın görevini kaynak metin üzerinde kurar; gerçek bir aracın kendisi değildir. Kaynak, satırların bir dizisi olarak tutulur. Model kodu çalıştırmaz; yalnız metni tarar, tıpkı bir statik çözümleme aracı gibi.

`formatViolations` biçim denetimidir: tab, satır sonunda boşluk ve dört boşluğun katı olmayan girinti sayar. `forceUnwraps` kural denetimidir: bir isim, `)` ya da `]` hemen ardından gelen ve `=` ile devam etmeyen ünlem işaretlerini sayar. Eşit değildir karşılaştırması ve bir değeri olumsuzlayan başa gelen işaret zorla açma değildir ve sayılmaz. `allowLiterals` açıkken, bağımsız değişkeni yalnız bir sabit metin olan çağrının zorla açılmasına izin verilir.

```swift
// Lint.swift — bicim denetimi, zorla acma kurali, kaynak satirlari
func formatViolations(in lines: [String]) -> Int {
    var count = 0
    for line in lines {
        let indent = line.prefix(while: { $0 == " " }).count
        if line.contains("\t") || line.last == " " || indent % 4 != 0 {
            count += 1
        }
    }
    return count
}

func isLiteralCall(_ chars: [Character], closeIndex: Int) -> Bool {
    var depth = 0
    var open = closeIndex
    while open >= 0 {
        if chars[open] == ")" { depth += 1 }
        if chars[open] == "(" { depth -= 1 }
        if depth == 0 { break }
        open -= 1
    }
    guard open >= 0 else { return false }
    let inner = chars[(open + 1)..<closeIndex]
    let afterLabel = inner.firstIndex(of: ":").map { inner[($0 + 1)...] } ?? inner[...]
    let argument = afterLabel.drop(while: { $0 == " " })
    return argument.first == "\"" && argument.last == "\""
}

func forceUnwraps(in lines: [String], allowLiterals: Bool) -> Int {
    var count = 0
    for line in lines {
        let chars = Array(line)
        for index in chars.indices where chars[index] == "!" && index > 0 {
            let previous = chars[index - 1]
            let next: Character = index + 1 < chars.count ? chars[index + 1] : " "
            let postfix = previous.isLetter || previous.isNumber || previous == "_" || previous == ")" || previous == "]"
            if !postfix || next == "=" { continue }
            if allowLiterals && previous == ")" && isLiteralCall(chars, closeIndex: index - 1) { continue }
            count += 1
        }
    }
    return count
}

let correctLines = [
    "func total(of text: String) -> Int {",
    "    guard let value = Int(text) else { return 0 }",
    "    return value",
    "}"
]

let faultyLines = [
    "func total(of text: String) -> Int {",
    "    let value = Int(text)!",
    "    return value",
    "}"
]

let correctLiteralLines = [
    "func home() -> URL {",
    "    return URL(string: \"https://example.com\")!",
    "}"
]

let faultyLiteralLines = [
    "func link(_ input: String) -> URL {",
    "    return URL(string: input)!",
    "}"
]
```

Dört kaynak vardır. `correctLines` ve `faultyLines` aynı biçimde yazılmıştır; yalnız ikincisinin ikinci satırı zorla açar. `correctLiteralLines` sabit bir adresi zorla açar: bu adres derleme anında bellidir ve geçerlidir, yani çökme olasılığı yoktur. `faultyLiteralLines` girdiden gelen bir metni zorla açar ve geçersiz adreste çöker.

##### Biçim Kör, Kural Ayırır

İlk ölçüm iki denetimi `total` işlevinin iki sürümüne uygular.

```swift
// main.swift — bicim denetimi ve zorla acma kurali
let format = contrast("bicim-denetimi", correct: formatViolations(in: correctLines), faulty: formatViolations(in: faultyLines), check: { $0 == 0 }).describe()
let rule = contrast("zorla-acma-kurali", correct: forceUnwraps(in: correctLines, allowLiterals: false), faulty: forceUnwraps(in: faultyLines, allowLiterals: false), check: { $0 == 0 }).describe()

countSeparations([format, rule])
```

```
bicim-denetimi | dogru=0/gecti kusurlu=0/gecti -> kor
zorla-acma-kurali | dogru=0/gecti kusurlu=1/kaldi -> ayiran
ayiran: 1
kor: 1
gurultulu: 0
ters: 0
```

`bicim-denetimi` kör: iki sürüm de düzgün girintilidir, satır sonunda boşluk yoktur. Kodun çökebileceğini biçim söylemez. `zorla-acma-kurali` ayıran: doğru sürümde ünlem yoktur, kusurlu sürümde bir tane vardır.

Bu, biçimlendiricinin gereksiz olduğu anlamına gelmez; farklı bir soruyu yanıtlar. Biçim, kodun okunabilirliğini ve inceleme sırasında gereksiz fark çıkmamasını sağlar. Kusuru bulmak ise kural denetiminin işidir. Biri diğerinin yerine geçmez; "biçim denetimi temiz" cümlesi kodun doğru olduğu hakkında bir şey söylemez.

**TS1.** Model satırları metin olarak tarar ve dizgi içindeki bir ünlemi ayırt etmez. Gerçek bir araç sözdizimi ağacı üzerinde çalışır ve bu yanlış eşleşmeleri eler.

**TS2.** Model yalnız tek bir kuralı sayar. Gerçek bir araç onlarca kural çalıştırır ve her kuralın ayrımı ayrıca ölçülmelidir.

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

Bir kural kusuru bulabilir ve yine de kötü bir denetim olabilir: güvenli kullanımı da kusur sayarsa. Sabit bir adresi zorla açmak çökmez; kural bunu da işaretlerse ekip uyarıları görmezden gelmeyi öğrenir.

Aşağıdaki ölçüm sıkı kuralı ve sabit metin istisnalı kuralı, sabit adresi zorla açan doğru sürüme ve girdiden gelen metni zorla açan kusurlu sürüme uygular.

```swift
// main.swift — yanlis alarm ve istisna
let strict = contrast("siki-kural", correct: forceUnwraps(in: correctLiteralLines, allowLiterals: false), faulty: forceUnwraps(in: faultyLiteralLines, allowLiterals: false), check: { $0 == 0 }).describe()
let exempt = contrast("istisnali-kural", correct: forceUnwraps(in: correctLiteralLines, allowLiterals: true), faulty: forceUnwraps(in: faultyLiteralLines, allowLiterals: true), check: { $0 == 0 }).describe()

countSeparations([strict, exempt])
```

```
siki-kural | dogru=1/kaldi kusurlu=1/kaldi -> gurultulu
istisnali-kural | dogru=0/gecti kusurlu=1/kaldi -> ayiran
ayiran: 1
kor: 0
gurultulu: 1
ters: 0
```

`siki-kural` gürültülü: doğru sürüm de işaretlenir, çünkü kural sabit adresle girdiden gelen metni ayırt etmez. İki sürüm de kalır ve denetim bir şey söylemez. `istisnali-kural` ayıran: bağımsız değişkeni sabit metin olan çağrı serbesttir, girdiden gelen metin işaretlenir.

İstisna tanımlamak da bir karardır. Çok geniş bir istisna kusuru tekrar gizler; çok dar olan yanlış alarmı sürdürür. Burada istisna tek bir koşula, bağımsız değişkenin sabit metin olmasına bağlıdır ve bu koşul kuralın kendisinde yazılıdır; satırın üstüne eklenen "bu satırı atla" yorumları ise her biri ayrı bir denetim kaybıdır.

**TS3.** İstisna yalnız tek bir çağrı biçimini kapsar. Sabit bir metnin gerçekten geçerli olduğu kuralın dışında, ayrıca doğrulanmalıdır.

**TS4.** Model çökmeyi çalıştırmaz. Kusurlu sürümün girdiyle çöktüğü kaynak metnin yapısından bilinir, ölçülmemiştir.

#### Sık Yapılan Hatalar

- Biçim denetiminin temiz çıkmasını kodun doğru olduğu diye okumak.
- Kuralları açıp sonuçlarını incelememek; yanlış alarm veren bir kuralı sessizce kapatmak.
- Tüm zorla açmaları işaretleyip güvenli olanları ayırmamak; ekip uyarıyı görmezden gelmeye alışır.
- Her uyarıyı satır yorumuyla susturmak. Bu, kuralı o satırda devre dışı bırakır.
- Statik çözümlemeyi testin yerine koymak. Kural yalnız yazdığı kalıbı bulur, davranışı sınamaz.

#### Özet

- Biçimlendirici boşluğu ve girintiyi düzenler; statik çözümleme kuralı belirli bir yapıyı arar. İkisi farklı sorulara cevap verir.
- Zorla açma derlenir ve testlerde çalışır, ama beklenmedik girdide uygulamayı çökertir.
- Biçim denetimi iki sürümde de geçti ve kör kaldı; kural denetimi doğru sürümü geçirip kusurluyu kaldırdı ve ayırdı.
- Güvenli kullanımı da işaretleyen sıkı kural iki sürümde de kaldı ve gürültülü oldu; sabit metin istisnası ayrımı geri getirdi.
- Bir kuralın değeri, kusuru bulmasında ve güvenli kullanıma karışmamasında yatar.
- Bu dersin ayrımı: **kör**.

#### Sonraki Adım

Test ve başarım denetimlerinin ardından kod, kullanıcıya ulaşmak üzere paketlenir. Sıradaki konu Dağıtım, ilk dersiyle İmzalama ve Sağlama Profilleri ile uygulamanın kimliğinin nasıl kanıtlandığını anlatır.

## Dağıtım

Kimlik ve yetki dosyalarıyla imzalama ve sağlama profilleri, otomatik derleme ve testle sürekli tümleştirme, sürüm numaralama ve yükleme adımlarıyla yayın otomasyonu, sınırlı kitleye beta dağıtımı, mağaza inceleme süreci ve listelemesi, çökme raporlama ve analitik.

### İmzalama ve Sağlama Profilleri

Source: https://academia.sh/tr/kurslar/ios-uretim/imzalama-ve-saglama-profilleri

Bir paketin taşıdığı yetkilerin kayıtlı listeyle karşılaştırılmasının kusurlu sürümü geçirip doğru sürümü reddettiğinin, sertifika ve profil denetimlerinin ise kör kaldığının ölçümü.

Önceki derse kadar denetimler kodu okuyordu: testler, otomatik denetleyiciler, biçimlendiriciler. Bu dersle birlikte denetimler kodun paketlendikten sonraki hâline geçer. Bir paket aygıta ya da mağazaya ulaşmadan önce üç şeyle eşleştirilir. **Sertifika**, paketi kimin imzaladığını söyler. **Sağlama profili** (provisioning profile), hangi paket kimliğinin hangi yetkilerle ve hangi sertifikalarla çalışabileceğini sıralayan imzalı bir dosyadır. **Yetkilendirme dosyası** ise paketin kendisinin hangi yetkileri istediğini yazar. İmzalama bu üçünün tutarlı olup olmadığına bakar.

Bu kursun sorusu bir denetimin geçmesinin ya da kalmasının ne söylediği değildir. Soru, **aynı denetimi doğru sürüme ve kusurlu sürüme uygulayınca iki yanıtın farklı olup olmadığıdır.** Ölçü adı **ayrım**dır; dört kategorisi vardır: doğru sürümün geçip kusurlu sürümün kaldığı **ayıran**, ikisinin de geçtiği **kör**, ikisinin de kaldığı **gürültülü** ve doğru sürümün kalıp kusurlu sürümün geçtiği **ters**. Çıktıda bunlar `ayiran`, `kor`, `gurultulu` ve `ters` olarak görünür. Bu ders çekirdek dosyayla ölçer.

```swift
// 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 dersin kusuru şudur: uygulama uzak bildirim yetkisi ister, ama paket bu yetkiyi taşımaz. Yetkilendirme dosyasında tek bir giriş eksiktir ve paket yine de derlenir, imzalanır ve açılır. Eksiklik uygulama uzak bildirime kaydolmaya çalıştığında ortaya çıkar. Gerçek bir yetkilendirme dosyası şuna benzer; model yetkileri kendi adlarıyla tutar, gerçek dosyadaki anahtar adları değildir.

```xml
<dict>
    <key>aps-environment</key>
    <string>production</string>
</dict>
```

Paketin gerçekte hangi yetkileri taşıdığı `codesign -d --entitlements :- Shop.app` komutuyla okunur. Bu ders komutun çıktısını ölçmez; ölçülen şey, imzalama kararının saf Swift ile kurulmuş modelidir. Model, denetimin iki sürüme ne dediğini kurar; imza aracının kendisi değildir.

#### İmzalamanın Modeli

Model üç değeri tutar: paketin kimliği ve yetkileri, profilin izin verdikleri ve bitiş sayacı, sertifikanın bitiş sayacı. Süre saatle değil **sürüm sayacıyla** modellenir: bir bitiş değeri, sayaç ona ulaştığında ya da onu geçtiğinde dolmuş sayılır. `effective` imzalı paketin gerçekte kullanabileceği yetkileri verir: paketin istediği ile profilin izin verdiğinin kesişimi. `sign` ise kararı verir ve imzası eşleşmeyen yetkiyi **reddeder**: paket profilde olmayan bir yetki taşıyorsa imza kabul edilmez.

```swift
// Signing.swift — paketin yetkilerini, sertifikayi ve saglama profilini modeller
struct Package {
    let identifier: String
    let entitlements: Set<String>
}

struct Profile {
    let identifier: String
    let allowed: Set<String>
    let expiresAt: Int
}

struct Certificate {
    let expiresAt: Int
}

func effective(_ package: Package, profile: Profile) -> [String] {
    package.entitlements.intersection(profile.allowed).sorted()
}

func sign(_ package: Package, profile: Profile, certificate: Certificate, counter: Int) -> String {
    if certificate.expiresAt <= counter { return "red:sertifika" }
    if profile.expiresAt <= counter { return "red:profil" }
    if package.identifier != profile.identifier { return "red:kimlik" }
    if !package.entitlements.isSubset(of: profile.allowed) { return "red:yetki" }
    return "imzali"
}
```

Kimlik denetimi küçük görünür ama gereklidir: açık kimlikli bir profil tek bir paket kimliğine bağlıdır ve aynı yetkileri taşıyan başka bir kimlikle paketlenmiş uygulama o profille imzalanamaz. Modelde bu `red:kimlik` olarak durur. Bu derste hiçbir girdi onu tetiklemez; kimlik eşleşmesi ölçüm dışında kalır ve kusur ona dokunmaz.

Dersin iki paketi aynı paket kimliğini taşır. Doğru paket iki yetki taşır, kusurlu paket yalnızca birini. Profil ikisine de izin verir ve uygulama ikisini ister.

```swift
// Shop.swift — dersin iki paketi, profili ve sertifikasi
let requested: Set<String> = ["anahtar-zinciri", "uzak-bildirim"]
let shopProfile = Profile(identifier: "com.example.shop", allowed: ["anahtar-zinciri", "uzak-bildirim"], expiresAt: 8)
let shopCertificate = Certificate(expiresAt: 5)
let correctPackage = Package(identifier: "com.example.shop", entitlements: ["anahtar-zinciri", "uzak-bildirim"])
let faultyPackage = Package(identifier: "com.example.shop", entitlements: ["anahtar-zinciri"])
```

**DG1.** Bu ölçüm sertifikanın ve profilin süresini sürüm sayacıyla verir. Gerçek bir bitiş tarihinin sayaca nasıl karşılık geldiği ölçülmedi.

#### Dört Denetim

Aynı iki pakete dört denetim uygulanır. İlki paketin taşıdığı yetkileri **kayıtlı listeyle** karşılaştırır. Kayıtlı liste, ekibin bir kez kaydedip sonra doğrulukta ölçüt saydığı yetki listesidir; bu derste o liste kusurlu derlemeden kopyalanmıştır. İkincisi imzalı paketin etkin yetkilerinin uygulamanın istediği yetkileri kapsayıp kapsamadığına bakar. Üçüncüsü yalnız profilin uygulamanın istediği yetkilere izin verip vermediğine, dördüncüsü yalnız sertifikanın sayaç için geçerli olup olmadığına bakar.

```swift
// main.swift — dort denetim, uc ayrim
let counter = 3
let registered = ["anahtar-zinciri"]

let listed = contrast("kayitli-liste-esit", correct: correctPackage.entitlements.sorted(), faulty: faultyPackage.entitlements.sorted()) { $0 == registered }.describe()
let covered = contrast("etkin-yetkiler-kapsiyor", correct: effective(correctPackage, profile: shopProfile), faulty: effective(faultyPackage, profile: shopProfile)) { requested.isSubset(of: $0) }.describe()
let profileOnly = contrast("profil-kapsiyor", correct: shopProfile.allowed.sorted(), faulty: shopProfile.allowed.sorted()) { requested.isSubset(of: $0) }.describe()
let certificateOk = contrast("sertifika-gecerli", correct: shopCertificate.expiresAt, faulty: shopCertificate.expiresAt) { $0 > counter }.describe()

countSeparations([listed, covered, profileOnly, certificateOk])
```

```text
kayitli-liste-esit | dogru=["anahtar-zinciri", "uzak-bildirim"]/kaldi kusurlu=["anahtar-zinciri"]/gecti -> ters
etkin-yetkiler-kapsiyor | dogru=["anahtar-zinciri", "uzak-bildirim"]/gecti kusurlu=["anahtar-zinciri"]/kaldi -> ayiran
profil-kapsiyor | dogru=["anahtar-zinciri", "uzak-bildirim"]/gecti kusurlu=["anahtar-zinciri", "uzak-bildirim"]/gecti -> kor
sertifika-gecerli | dogru=5/gecti kusurlu=5/gecti -> kor
ayiran: 1
kor: 2
gurultulu: 0
ters: 1
```

Dört satır, üç kategori. `etkin-yetkiler-kapsiyor` kusurun değiştirdiği değere bakar: kusurlu pakette uzak bildirim etkin yetkiler arasında yoktur, denetim kalır; doğru pakette vardır, denetim geçer. Bu **ayıran** denetimdir. `profil-kapsiyor` ve `sertifika-gecerli` ise iki sürümde de aynı değeri görür, çünkü kusur profile de sertifikaya da dokunmaz. İki satırda da `kusurlu` tarafı `gecti` der; bu iki **kör** denetim yeşil bir imza hattında sık görülen güveni verir ve kusur hakkında hiçbir şey söylemez.

#### Eşitlik Denetiminin Tersliği

İlk satır ilginç olandır. `kayitli-liste-esit` doğru paketi reddeder ve kusurlu paketi kabul eder: `dogru=…/kaldi`, `kusurlu=…/gecti`, sonuç **ters**. Denetim kusurlu değildir; sorun beklenen değerin kaynağıdır. Kayıtlı liste kusurlu derlemeden kopyalandığı için eksik yetkiyi "doğru" saymıştır. Sonradan yetkiyi ekleyen ekip, denetimin kırmızıya döndüğünü görür ve çoğu zaman listeyi paketle birlikte günceller. Kusur o andan sonra denetimin kendisine yerleşmiş olur.

Bu, eşitlik denetimine özgü bir tuzaktır: denetim beklenen değeri **bir sürümden alıyorsa**, o sürümün kusurunu da alır. Eşitlik denetimi bir beklentiyi korur, ama beklentinin doğru olup olmadığını söylemez. Ayıran denetim bunu başka bir yoldan çözer. `etkin-yetkiler-kapsiyor` beklenen değeri hiçbir derlemeden almaz; uygulamanın istediği yetkileri kendi kaynağından, yani uygulamanın koduyla birlikte tutulan istek listesinden okur.

Kayıtlı listeyle eşitlik denetimi hiçbir işe yaramaz demek de yanlıştır. Yetkilerin sessizce **artıp artmadığını** yakalamak için iyi bir yoldur: kayıt doğru alındıysa fazladan yetki eklenen bir paket kalır. Bu derste ölçülen şey başka bir kusurdur, eksik yetki. Ayrım tek bir kusura göre ölçülür; aynı denetim başka bir kusura karşı başka bir kategoriye düşebilir.

**DG2.** Bu ölçüm profilin yetkilerinin paketin yetkilerini kapsadığında imzanın kabul edildiğini varsayar. Profilde fazladan yetki bulunmasının gerçek imza aracını nasıl etkilediği ölçülmedi.

#### Kör Denetimin Rahatlığı

Sertifika ve profil denetimleri neden bu kadar yaygındır? Çünkü başarısızlıkları gürültülüdür ve ekibi hemen uyarır. Süresi dolan bir sertifika paketi imzalatmaz, profili olmayan bir paket aygıta yüklenmez. Bu yüzden bu denetimler hattın başında durur ve her gün geçerek ekibe alışkanlık kazandırır. Ama yüzlerce kez yeşil olmuş bir denetim, doğru sürümle kusurlu sürüm arasında bir fark gördüğünü göstermez. İki sürüm aynı sertifikayla imzalanır ve aynı profile bağlanır; `kor` bu yüzden kaçınılmazdır.

Çıkarım şudur: kör bir denetimi silmek gerekmez, çünkü başka bir kusuru yakalar. Gerekli olan, onu **kusuru yakalayan denetim sayıp saymadığını bilmektir.** Hattın yeşil çıktısı üç kör denetim ve bir ayıran denetimden oluşuyorsa, kusur ayırandan geçmediği sürece görünmez. Pratikte bu, hat raporuna yalnız geçen denetimlerin sayısını değil, her denetimin hangi kusuru görebildiğini de yazmak demektir; sayı yüksek olabilir ve ayıran denetimlerin payı sıfıra yakın kalabilir.

#### Yeni Bir Denetimi Bozma Denemesiyle Sınamak

İmza hattına yeni bir denetim eklenirken sorulacak ilk soru "bu denetim bugün geçiyor mu" olmamalıdır. Bugün geçmesi yeşil bir hat verir ve bu hiçbir şey kanıtlamaz. Doğru soru şudur: **paketi bilerek bozsam bu denetim kalır mı?** Bu yönteme **bozma denemesi** denir. Burada bozma, kasıtlı olarak bir yetkiyi paketten çıkarmaktır; denetim o paketi reddetmiyorsa kördür ya da tersidir.

Dersin ölçümü bozma denemesinin küçük bir hâlidir: kusurlu paket, doğru paketten tek bir yetkinin çıkarılmasıyla elde edilmiştir ve dört denetim ona uygulanmıştır. Fark şu: bozma denemesi yalnız "yakalandı mı" diye sorar. Bu kurs doğru sürümün yanıtına da bakar, çünkü doğru paketi reddeden bir denetim, yakalama başarısı ne olursa olsun, ekibi her seferinde yanlış bir kırmızıyla meşgul eder. Gürültülü ile ters ancak iki sürümün yanıtı yan yana konunca ayrılır.

| Denetim | Neye bakar | Kusuru görür mü | Ayrım |
|---|---|---|---|
| `kayitli-liste-esit` | paketin yetkileri, kayıtlı liste | yalnız listenin doğru olduğu sürece | ters |
| `etkin-yetkiler-kapsiyor` | etkin yetkiler, uygulamanın istekleri | evet | ayıran |
| `profil-kapsiyor` | profil, uygulamanın istekleri | hayır | kör |
| `sertifika-gecerli` | sertifika, sayaç | hayır | kör |

Tablodaki üçüncü sütun bir sonucu değil, bir **koşulu** yazar. Birinci satırda kusuru görmek için kayıtlı listenin kusurlu derlemeden alınmamış olması gerekir. Bu koşul denetimin kendi içinde yoktur; listenin nereden geldiği denetimin dışında kalır ve yalnız onu yazan kişi bilir.

#### Kayıtlı Listeyi Düzeltmek

Terslik bulununca iki yol vardır. Birincisi listeyi doğru pakete göre yeniden kaydetmektir. Bu, denetimi bir sonraki kusura kadar doğru yapar, ama listeyi yine bir derlemeden almış olur; yeni derlemede bir yetki sessizce kaybolursa terslik geri döner. İkincisi beklenen değeri paketin dışından, uygulamanın istek listesinden türetmektir: `etkin-yetkiler-kapsiyor`un yaptığı budur. İkinci yol kayıt yenileme işini ortadan kaldırır, çünkü beklenen değer her seferinde uygulamanın kendisinden gelir.

İki yol bir arada da kurulabilir. İstek listesinden türeyen denetim eksik yetkiyi yakalar, kayıtlı listeyle eşitlik denetimi ise fazladan yetkiyi. İkisi farklı kusurlara bakar ve ders bu ikisini rakip değil, tamamlayıcı sayar.

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

Ayrım denetimin tek başına özelliği değildir; kusura ve girdiye göre değişir. Aşağıdaki ölçümde tek bir denetim, `sign` kararının `imzali` olması, üç girdiye uygulanır. Sayaç 3 iken sertifika da profil de geçerlidir ve iki paket de imzalanır. Sayaç 9 iken sertifika dolmuştur ve iki paket de reddedilir. Üçüncü girdide profil dar tutulmuştur ve yalnız `anahtar-zinciri` yetkisine izin verir: imzası eşleşmeyen yetki modelde de reddedilir, yani uzak bildirim taşıyan doğru paket reddedilir.

```swift
// main.swift — ayni imza denetimi uc girdide uc ayrim verir
let narrowProfile = Profile(identifier: "com.example.shop", allowed: ["anahtar-zinciri"], expiresAt: 8)

let early = contrast("sayac-3",
    correct: sign(correctPackage, profile: shopProfile, certificate: shopCertificate, counter: 3),
    faulty: sign(faultyPackage, profile: shopProfile, certificate: shopCertificate, counter: 3)) { $0 == "imzali" }.describe()
let late = contrast("sayac-9",
    correct: sign(correctPackage, profile: shopProfile, certificate: shopCertificate, counter: 9),
    faulty: sign(faultyPackage, profile: shopProfile, certificate: shopCertificate, counter: 9)) { $0 == "imzali" }.describe()
let narrow = contrast("dar-profil",
    correct: sign(correctPackage, profile: narrowProfile, certificate: shopCertificate, counter: 3),
    faulty: sign(faultyPackage, profile: narrowProfile, certificate: shopCertificate, counter: 3)) { $0 == "imzali" }.describe()

countSeparations([early, late, narrow])
```

```text
sayac-3 | dogru=imzali/gecti kusurlu=imzali/gecti -> kor
sayac-9 | dogru=red:sertifika/kaldi kusurlu=red:sertifika/kaldi -> gurultulu
dar-profil | dogru=red:yetki/kaldi kusurlu=imzali/gecti -> ters
ayiran: 0
kor: 1
gurultulu: 1
ters: 1
```

Aynı denetim, üç girdi, üç kategori. `sayac-3`te kusurlu paket de imzalanır; imza hattı yeşildir ve kusur geçer, **kör**. `sayac-9`da süre dolmuştur, iki paket de `red:sertifika` alır ve denetim iki sürümde de kalır, **gürültülü**: kalma nedeni kusur değil sertifikadır. `dar-profil`de ise doğru paket `red:yetki` ile reddedilir, çünkü uzak bildirim profilde yoktur; kusurlu paket tam profilin izin verdiği tek yetkiyi taşıdığı için `imzali` alır, **ters**. Gerçek hayatta da bir profil yetkiyi içermiyorsa doğru paket imzalanamaz; eksik yetkiyle çalışan kusurlu paket bu engele hiç takılmaz.

Bu ölçüm bir sınırı da gösterir. Üç girdide de `imzali` denetimi ayıran çıkmadı: bu denetim tek başına bu kusuru ayıramaz, çünkü imza kararı yalnız paketin profile **uyup uymadığını** söyler, uygulamanın **neye ihtiyaç duyduğunu** söylemez. Uygulamanın ihtiyacı `requested` kümesindedir ve onu okuyan tek denetim `etkin-yetkiler-kapsiyor`dur.

**DG3.** Bu ölçüm imza kararının sırasını modelin sırasıyla verir: önce sertifika, sonra profil, kimlik ve yetki. Gerçek imza aracının hangi eşleşmeyi önce kontrol ettiği ölçülmedi.

#### Özet

- Sertifika kimin imzaladığını, sağlama profili hangi paket kimliğinin hangi yetkilerle çalışacağını, yetkilendirme dosyası paketin ne istediğini söyler; imzalama bu üçünün tutarlılığına bakar.
- Paketin yetkilerini kayıtlı listeyle karşılaştıran denetim, liste kusurlu derlemeden alındıysa doğru paketi reddeder ve kusurlu paketi geçirir: ters.
- Etkin yetkilerin uygulamanın istediklerini kapsayıp kapsamadığına bakan denetim, kusurlu paketi ayırır: ayıran.
- Yalnız sertifikaya ya da yalnız profile bakan denetimler iki sürümde de geçer: kör.
- Aynı imza denetimi sayaca ve profile göre kör, gürültülü ya da ters olabilir; ayrım denetimin değil, denetimle girdinin ortak özelliğidir.
- Bu dersin ayrımı: **ters**.

#### Sonraki Adım

İmzalanabilen bir paket hâlâ yanlış hesap yapabilir; imza kararı hesabı doğrulamaz. Sıradaki derste, Sürekli Tümleştirme, bu denetimlerin bir hat içinde art arda koştuğu ve hattın ilk kalan adımda durduğu ölçülecek.

### Sürekli Tümleştirme

Source: https://academia.sh/tr/kurslar/ios-uretim/surekli-tumlestirme

Bir otomatik hattın temiz makinede eksik bir derleme ayarı yüzünden iki sürümde de kaldığının, ayar düzelince test adımının kusuru ayırdığının ve koşmayan adımların kategoriye girmediğinin ölçümü.

Bir değişiklik her yapıldığında aynı adımların aynı sırayla, kimsenin makinesine bağlı olmadan koşması fikrine **sürekli tümleştirme** (continuous integration) denir. Adımlar bir hat olarak yazılır: önce derle, sonra test et, sonra paketle. Hat bir değişiklikte kalırsa değişiklik ekibe ulaşmadan durur. Bu, bir önceki dersin imza denetimlerini de içine alan bir çatıdır; imza adımı hattın sonunda yer alabilir ve bu derste hat modelinin dışında kalır.

Kursun ölçü adı **ayrım**dır: aynı denetimi doğru sürüme ve kusurlu sürüme uygulayınca iki yanıt farklı mı. Dört kategori vardır: doğru sürümün geçip kusurlu sürümün kaldığı **ayıran**, ikisinin de geçtiği **kör**, ikisinin de kaldığı **gürültülü** ve doğru sürümün kalıp kusurlu sürümün geçtiği **ters**. Çıktıda bunlar `ayiran`, `kor`, `gurultulu` ve `ters` olarak görünür. Bu ders çekirdek dosyayla ölçer.

```swift
// 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 dersin kusuru sepet toplamındadır. Üç kalemlik bir sepetin toplamı hesaplanırken son kalem sessizce atlanır: doğru sürüm kalemlerin hepsini toplar, kusurlu sürüm sonuncuyu dışarıda bırakır. Hesap hata vermez, çökmez, yalnızca eksik bir sayı döndürür. Bu tür bir kusuru bulan şey çoğunlukla bir testtir ve test ancak çalışırsa bulur. Soru, hattın testi gerçekten çalıştırıp çalıştırmadığıdır.

#### Hattın Modeli

Model bir hattı adım adım koşturur. Her adım üç sonuçtan birini alır: `gecti`, `kaldi` ya da `kosmadi`. Hat adımları sırayla dener; ilk `kaldi` alan adımda **durur** ve kalan adımların hepsi `kosmadi` olarak işaretlenir. Bu, gerçek hatlardaki davranışın karşılığıdır: derleme kalmışsa test koşturmanın anlamı yoktur ve hat zaman harcamaz.

Üç adım vardır. `derle` adımı, hedefin gerekli ayarına bakar; ayar yoksa derleme kalır. `test` adımı, sepet toplamını beklenen değerle karşılaştırır. `paketle` adımı, toplamın sıfırdan büyük olduğuna bakar ve burada yalnızca hattın son halkası olarak durur. Ayarlar `Settings` tipindedir; tek alanı hedef ayarının bulunup bulunmadığını söyler.

```swift
// Pipeline.swift — adimlari sirayla kosturan ve ilk kalan adimda duran hat modeli
struct Settings {
    let hasTargetSetting: Bool
}

let pipelineSteps = ["derle", "test", "paketle"]

func runStep(_ name: String, total: Int, expected: Int, settings: Settings) -> Bool {
    switch name {
    case "derle": return settings.hasTargetSetting
    case "test": return total == expected
    case "paketle": return total > 0
    default: return false
    }
}

func runPipeline(total: Int, expected: Int, settings: Settings, steps: [String] = pipelineSteps) -> [String] {
    var outcomes: [String] = []
    var stopped = false
    for name in steps {
        if stopped {
            outcomes.append("\(name):kosmadi")
        } else if runStep(name, total: total, expected: expected, settings: settings) {
            outcomes.append("\(name):gecti")
        } else {
            outcomes.append("\(name):kaldi")
            stopped = true
        }
    }
    return outcomes
}

func outcome(of step: String, in outcomes: [String]) -> String {
    for entry in outcomes where entry.hasPrefix(step + ":") {
        return String(entry.dropFirst(step.count + 1))
    }
    return "yok"
}

func trace(_ outcomes: [String]) -> String {
    outcomes.joined(separator: " ")
}

func stepContrast(_ label: String, step: String, correct: [String], faulty: [String]) -> String {
    let onCorrect = outcome(of: step, in: correct)
    let onFaulty = outcome(of: step, in: faulty)
    if onCorrect == "kosmadi" || onFaulty == "kosmadi" {
        print("\(label) | dogru=\(onCorrect) kusurlu=\(onFaulty) -> kategori-disi")
        return "kategori-disi"
    }
    return contrast(label, correct: onCorrect, faulty: onFaulty) { $0 == "gecti" }.describe()
}

func countOutside(_ results: [String]) {
    print("kategori-disi: \(results.filter { $0 == "kategori-disi" }.count)")
}
```

`stepContrast` tek bir adımın sonucunu iki sürüm için karşılaştırır, ama adım iki sürümden birinde **koşmamışsa** sınıflandırmaz. Neden böyle yapıldığı ayrı bir bölümde açıklanıyor; şimdilik yalnızca çıktıda `kategori-disi` yazdığını bilmek yeter. Bu dersin kuralıdır: ayrımın dört kategorisi çekirdekte durur, `kategori-disi` ise yalnız bu derste adım koşmadığında yazılır.

Sepet ve beklenen değer ayrı bir dosyada durur. Doğru işlev kalemlerin hepsini toplar, kusurlu işlev sonuncuyu atlar. Beklenen toplam doğru sürümün değeridir ve test adımı onu bu değerden alır.

```swift
// Basket.swift — dersin sepeti ve iki toplam islevi
let basketPrices = [40, 25, 15]
let expectedTotal = 80

func basketTotal(_ prices: [Int]) -> Int {
    prices.reduce(0, +)
}

func faultyBasketTotal(_ prices: [Int]) -> Int {
    prices.dropLast().reduce(0, +)
}
```

**DG4.** Bu ölçüm hattı üç adımla ve sıralı koşan tek bir iş olarak modeller. Gerçek hatlardaki paralel adımların ve bağımlılık çizgesinin davranışı ölçülmedi.

#### Temiz Makinede Hat

Hat ilk kez temiz bir makinede koşuyor. Temiz makine, geliştiricinin makinesinde birikmiş ayarları ve yerel önbelleği taşımayan ortamdır. Geliştiricinin makinesinde derleme ayarı bir kez yapılmış ve unutulmuştur; hat tanımına hiç yazılmamıştır. Temiz makinede o ayar yoktur.

**DG5.** Bu ölçüm temiz ortamın yerel önbelleği ve geliştirici ayarlarını taşımadığını varsayar. Gerçek bir hat ortamının hangi ayarları kendiliğinden getirdiği ölçülmedi.

Hat iki sürüme uygulanır ve üç denetim koşar: hattın bütünüyle yeşil olması, derleme adımının geçmesi ve test adımının geçmesi.

```swift
// main.swift — temiz makinede uc denetim
let cleanMachine = Settings(hasTargetSetting: false)
let correctRun = runPipeline(total: basketTotal(basketPrices), expected: expectedTotal, settings: cleanMachine)
let faultyRun = runPipeline(total: faultyBasketTotal(basketPrices), expected: expectedTotal, settings: cleanMachine)

let allGreen = contrast("hat-yesil", correct: trace(correctRun), faulty: trace(faultyRun)) { !$0.contains("kaldi") && !$0.contains("kosmadi") }.describe()
let buildStep = stepContrast("derleme-adimi-gecti", step: "derle", correct: correctRun, faulty: faultyRun)
let testStep = stepContrast("test-adimi-gecti", step: "test", correct: correctRun, faulty: faultyRun)

countSeparations([allGreen, buildStep, testStep])
countOutside([allGreen, buildStep, testStep])
```

```text
hat-yesil | dogru=derle:kaldi test:kosmadi paketle:kosmadi/kaldi kusurlu=derle:kaldi test:kosmadi paketle:kosmadi/kaldi -> gurultulu
derleme-adimi-gecti | dogru=kaldi/kaldi kusurlu=kaldi/kaldi -> gurultulu
test-adimi-gecti | dogru=kosmadi kusurlu=kosmadi -> kategori-disi
ayiran: 0
kor: 0
gurultulu: 2
ters: 0
kategori-disi: 1
```

İki sürümde de hat aynı yerde kalır: `derle:kaldi`. Hattın bütününe bakan `hat-yesil` denetimi iki sürümde de kalır, yani **gürültülü**. Derleme adımının kendisine bakan denetim de aynı: iki sürümde `kaldi`. Hat kırmızıdır ve kusurun bunda payı yoktur; sepet toplamı hiç hesaplanmadan eksik bir ayar hattı durdurmuştur.

Bunun pratik sonucu zararlıdır. Hat kırmızı olunca ekip önce kırmızıyı onarır, çünkü hat kırmızıyken hiçbir değişiklik ilerleyemez. Kırmızının nedeni ayardır ve onarım ayarı eklemektir. Ama ayar eklenene kadar geçen sürede, hattın kusuru görüp görmediğini kimse bilmez; hat kusura hiç ulaşmamıştır. Kırmızı bir hat "kusur yakalandı" diye okunmaya çok yatkındır, ölçüm ise tersini söyler: kusurun yakalanıp yakalanmadığı bu çalıştırmadan çıkmaz.

Üçüncü satır, `test-adimi-gecti`, sınıflandırılmadı. Test adımı iki sürümde de koşmadı, çünkü hat ondan önce durdu. Çıktıda `dogru=kosmadi kusurlu=kosmadi` yazar ve sonuç `kategori-disi` olur. Bu satır ne ayıran, ne kör, ne gürültülü, ne de tersidir; çünkü denetim hiçbir sürümde çalışmamıştır ve çalışmayan bir denetimin iki sürüme verdiği yanıt yoktur.

#### Ayar Düzelince

Ayar hat tanımına eklenir, temiz makine artık derlemeyi geçer. Aynı üç denetim, bir de paketleme adımına bakan dördüncü denetim, aynı iki sürüme uygulanır.

```swift
// main.swift — ayar duzeldikten sonra dort denetim
let fixedMachine = Settings(hasTargetSetting: true)
let correctRun = runPipeline(total: basketTotal(basketPrices), expected: expectedTotal, settings: fixedMachine)
let faultyRun = runPipeline(total: faultyBasketTotal(basketPrices), expected: expectedTotal, settings: fixedMachine)

let allGreen = contrast("hat-yesil", correct: trace(correctRun), faulty: trace(faultyRun)) { !$0.contains("kaldi") && !$0.contains("kosmadi") }.describe()
let buildStep = stepContrast("derleme-adimi-gecti", step: "derle", correct: correctRun, faulty: faultyRun)
let testStep = stepContrast("test-adimi-gecti", step: "test", correct: correctRun, faulty: faultyRun)
let packageStep = stepContrast("paketleme-adimi-gecti", step: "paketle", correct: correctRun, faulty: faultyRun)

countSeparations([allGreen, buildStep, testStep, packageStep])
countOutside([allGreen, buildStep, testStep, packageStep])
```

```text
hat-yesil | dogru=derle:gecti test:gecti paketle:gecti/gecti kusurlu=derle:gecti test:kaldi paketle:kosmadi/kaldi -> ayiran
derleme-adimi-gecti | dogru=gecti/gecti kusurlu=gecti/gecti -> kor
test-adimi-gecti | dogru=gecti/gecti kusurlu=kaldi/kaldi -> ayiran
paketleme-adimi-gecti | dogru=gecti kusurlu=kosmadi -> kategori-disi
ayiran: 2
kor: 1
gurultulu: 0
ters: 0
kategori-disi: 1
```

Ayar düzeldiğinde tablo değişir. `test-adimi-gecti` doğru sürümde geçer, kusurlu sürümde kalır: **ayıran**. Hattın bütününe bakan `hat-yesil` de ayırandır, çünkü test kalınca hat kırmızıdır. `derleme-adimi-gecti` iki sürümde de geçer, **kör**: kusur derlemeye dokunmaz, derleme her iki sürümde de temiz biter. Dördüncü satır yine `kategori-disi`.

İlk ölçümle ikincisi arasındaki fark yalnız bir ayardır. Kod aynı, sepet aynı, test aynı; ama `hat-yesil` denetimi gürültülüden ayırana geçti. Bu, ayrımın denetimin ya da kodun değil, **ortamın** da bir işlevi olduğunu gösterir. Aynı hat, ayar eksik olduğu sürece, kusur hakkında hiçbir şey söylemez.

#### Koşmayan Adım Neden Kategori Değildir

Dördüncü satıra dönelim: `paketleme-adimi-gecti`. Doğru sürümde paketleme geçti, kusurlu sürümde koşmadı. Çekirdek bu çifti, `gecti` ve `kosmadi`, doğrudan sınıflandırsaydı denetimi **ayıran** sayardı: doğru sürüm geçti, kusurlu sürüm "geçemedi". Ama paketleme adımı kusuru hiç görmemiştir. Kusurlu sürümde sepet toplamı eksiktir ve `paketle` adımının `total > 0` denetimi eksik toplamla da geçerdi; adım yalnız hat testte durduğu için koşmamıştır.

Bu fark önemlidir, çünkü aksi hâlde hattın sonundaki her adım ayıran görünürdü. Paketleme, imzalama, yükleme, her biri hattın ilk kalan adımından sonra yer aldığı için, kusur hat boyunca yakalandığında ayıran sayılırdı. Oysa ayırmak, bir denetimin kusurlu sürüme **kendi başına** farklı yanıt vermesidir. Koşmayan adımın yanıtı yoktur; o yanıtı hattın ilk kalan adımı belirler.

Dersin yaptığı ayrım şudur. İki sürümde de koştuysa sonuç dört kategoriden birine girer. Birinde koşmadıysa `kategori-disi` yazılır ve ayrı sayılır. Çıktının son satırı bu sayıyı verir; böylece "kaç denetim sınıflandırıldı" ile "kaç denetim hiç koşmadı" birbirine karışmaz. Yeşil bir hattın kırk adımının otuzu koşmadıysa, o hat otuz denetim kadar sessizdir.

#### Hat Tanımı

Hat tanımı bir yapılandırma dosyasıdır ve genellikle depoda kod gibi durur. Aşağıdaki gösterim genel bir biçimdir; bir hizmetin gerçek anahtar adları değildir ve bu ders hat tanımını ölçmez. Ölçülen şey hat modelidir.

```yaml
steps:
  - name: derle
    run: xcodebuild build -scheme Shop
  - name: test
    run: xcodebuild test -scheme Shop
  - name: paketle
    run: xcodebuild archive -scheme Shop
```

Tanım üç satırlık bir liste gibi görünür ve modelin `pipelineSteps` dizisiyle birebir örtüşür. Önemli olan, listenin **sırası** ve her adımın başarısızlıkta hattı durdurup durdurmadığıdır. Çoğu hat tanımı bunu varsayılan olarak yapar; bazıları bir adımın kalmasına rağmen devam edecek biçimde ayarlanabilir. Devam eden bir hatta `kosmadi` yoktur ve her adımın iki sürüme ne dediği görülür. Bu bir ödündür: hat daha uzun sürer ve daha çok çıktı üretir, ama `kategori-disi` satırları sınıflandırılabilir satırlara dönüşür.

Hat tanımında bir ayar eksikse, eksiklik ancak ilk temiz makinede görünür. Bu yüzden hattın **ilk** çalıştırmasında, bilerek kusurlu bir sürümle de çalıştırması iyi bir alışkanlıktır: hat gerçekten test adımına ulaşabiliyor mu, ulaşıyorsa kusurlu sürümde kalıyor mu. Bu tek deneme hat yeşilken sonuçsuz kalan bir denetim sorununu hat kurulurken yakalar.

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

Ayrım adım sırasına da bağlıdır. Aşağıdaki ölçümde aynı temiz makine, yani eksik ayar, iki farklı sırayla koşar. Birinci sırada derleme önce gelir, ikincisinde test. Sıra değişince test adımına ulaşılır; ama eksik ayar derleme adımını yine kırar. Beşinci denetim ayar düzelince derleme adımına bakar.

```swift
// main.swift — ayni makine, farkli adim sirasi, ayni denetim
let cleanMachine = Settings(hasTargetSetting: false)
let fixedMachine = Settings(hasTargetSetting: true)
let testFirst = ["test", "derle", "paketle"]

let correctTotalNow = basketTotal(basketPrices)
let faultyTotalNow = faultyBasketTotal(basketPrices)

let buildFirstClean = (
    runPipeline(total: correctTotalNow, expected: expectedTotal, settings: cleanMachine),
    runPipeline(total: faultyTotalNow, expected: expectedTotal, settings: cleanMachine))
let testFirstClean = (
    runPipeline(total: correctTotalNow, expected: expectedTotal, settings: cleanMachine, steps: testFirst),
    runPipeline(total: faultyTotalNow, expected: expectedTotal, settings: cleanMachine, steps: testFirst))
let buildFirstFixed = (
    runPipeline(total: correctTotalNow, expected: expectedTotal, settings: fixedMachine),
    runPipeline(total: faultyTotalNow, expected: expectedTotal, settings: fixedMachine))

let first = stepContrast("derle-once-test", step: "test", correct: buildFirstClean.0, faulty: buildFirstClean.1)
let second = stepContrast("test-once-test", step: "test", correct: testFirstClean.0, faulty: testFirstClean.1)
let third = stepContrast("ayar-duzgun-test", step: "test", correct: buildFirstFixed.0, faulty: buildFirstFixed.1)
let fourth = contrast("test-once-hat", correct: trace(testFirstClean.0), faulty: trace(testFirstClean.1)) { !$0.contains("kaldi") }.describe()
let fifth = stepContrast("ayar-duzgun-derle", step: "derle", correct: buildFirstFixed.0, faulty: buildFirstFixed.1)

countSeparations([first, second, third, fourth, fifth])
countOutside([first, second, third, fourth, fifth])
```

```text
derle-once-test | dogru=kosmadi kusurlu=kosmadi -> kategori-disi
test-once-test | dogru=gecti/gecti kusurlu=kaldi/kaldi -> ayiran
ayar-duzgun-test | dogru=gecti/gecti kusurlu=kaldi/kaldi -> ayiran
test-once-hat | dogru=test:gecti derle:kaldi paketle:kosmadi/kaldi kusurlu=test:kaldi derle:kosmadi paketle:kosmadi/kaldi -> gurultulu
ayar-duzgun-derle | dogru=gecti/gecti kusurlu=gecti/gecti -> kor
ayiran: 2
kor: 1
gurultulu: 1
ters: 0
kategori-disi: 1
```

Aynı test adımı denetimi, aynı kusur, üç ayrı yanıt: `derle-once-test` sınıflandırılmaz, `test-once-test` ve `ayar-duzgun-test` ayırır. Eksik ayar durduğu sürece test adımının görülüp görülmediği tamamen adım sırasına bağlıdır. Test önce koşan hat, ayar eksik olsa bile kusuru yakalar. Ama hattın bütününe bakan `test-once-hat` yine de **gürültülü** kalır: derleme sonraki adımda kırılır ve hat iki sürümde de kırmızıdır. Testin kusuru yakaladığını görmek için hat sonucuna değil, adımın kendi sonucuna bakmak gerekir.

Bu iki gözlem birbirine yaslanır. Sıra, ayrımı belirler; ama sırayı ayrım için seçmek kötü bir fikirdir, çünkü hat derleme olmadan test yapamaz. Gerçek bir uygulamada test, derlenmiş çıktıya ihtiyaç duyar ve bu ölçümdeki sıra değişimi yalnız modelde mümkündür. Dersin çıkarımı, hat tanımını yazarken adımın "kusuru görebilecek konumda mı" sorusuyla birlikte "kusura **ulaşılabilir mi**" sorusunun da sorulması gerektiğidir.

**DG6.** Bu ölçüm sıra değişimini modelde serbest sayar; test adımı derlemeye bağlı olmadan koşar. Gerçek bir hatta test adımının derleme çıktısına olan bağımlılığı ölçülmedi.

#### Özet

- Sürekli tümleştirme, değişiklik başına aynı adımları temiz bir ortamda sırayla koşturur ve ilk kalan adımda durur.
- Temiz makinede eksik bir derleme ayarı hattı iki sürümde de kırar. Hattın bütününe ve derleme adımına bakan denetimler gürültülü olur.
- Ayar düzelince test adımı doğru sürümde geçer, kusurlu sürümde kalır: ayıran. Derleme adımı iki sürümde de geçer: kör.
- Hat durduktan sonra koşmayan adımlar sınıflandırılmaz; bu derste ayrı sayılır ve `kategori-disi` yazılır.
- Ayrım yalnız denetime değil, ortama ve adımların sırasına da bağlıdır; kırmızı bir hat kusurun yakalandığı anlamına gelmez.
- Bu dersin ayrımı: **gürültülü**.

#### Sonraki Adım

Test adımı kusuru yakalayan hat, bir sürümü mağazaya yükleyecek adımlara da ulaşır. Sıradaki derste, Yayın Otomasyonu, sürüm ve derleme numaralarını artıran yükleme betiğinin hangi denetimlerle ayrıldığı ölçülecek.

### Yayın Otomasyonu

Source: https://academia.sh/tr/kurslar/ios-uretim/yayin-otomasyonu

Derleme numarasını artırmadan yükleyen bir betiğin, numaranın mağazadaki son numaradan büyük olup olmadığına bakan denetimle ayrıldığının, biçim denetiminin ise kör kaldığının ölçümü.

Bir sürüm hazır olduktan sonra elle yapılan her adım bir hata fırsatıdır: numarayı artırmak, arşivi çıkarmak, imzalamak, yüklemek. **Yayın otomasyonu** bu adımları tek bir betiğe toplar ve her seferinde aynı sırayla koşturur. Betik hatalı yazılmışsa hata da her seferinde tekrarlanır; bu yüzden betiğin kendisi de denetlenir.

Kursun ölçü adı **ayrım**dır: aynı denetimi doğru sürüme ve kusurlu sürüme uygulayınca iki yanıt farklı mı. Dört kategori vardır: doğru sürümün geçip kusurlu sürümün kaldığı **ayıran**, ikisinin de geçtiği **kör**, ikisinin de kaldığı **gürültülü** ve doğru sürümün kalıp kusurlu sürümün geçtiği **ters**. Çıktıda bunlar `ayiran`, `kor`, `gurultulu` ve `ters` olarak görünür. Bu ders çekirdek dosyayla ölçer.

```swift
// 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 dersin kusuru küçük bir unutuştur: yükleme betiği **derleme numarasını artırmaz**. Sürüm numarası aynı kalır, ki bu normaldir, ama derleme numarası da aynı kalır. Betik çalışır, arşiv çıkar, imza geçer ve yükleme komutu koşar. Hiçbir adım kırmızı olmaz. Sorun ancak mağaza aynı numarayı ikinci kez görünce ortaya çıkar ve o noktada yükleme çoktan yapılmıştır.

#### Sürüm Numarası ve Derleme Numarası

Bir uygulamanın iki numarası vardır. **Sürüm numarası** (`2.4.0` gibi) kullanıcıya görünür ve ürünün hangi hâlini taşıdığını söyler. **Derleme numarası** ise aynı sürümün art arda yapılan yüklemelerini birbirinden ayırır: sürüm `2.4.0` iken üç kez yükleme yapılmışsa üç ayrı derleme numarası vardır. Bu iki numaranın rolü farklı olduğu için hata da farklı yerde görünür. Sürüm numarasını yanlış yazan biri bunu hemen fark eder, çünkü ekranda durur. Derleme numarasını artırmayı unutan kimse hiçbir şey görmez.

Aşağıdaki betik bir yükleme adımının iskeletidir. Araç adları genel tutuldu: ilk komut numarayı artıran küçük bir yardımcı, üçüncüsü yükleyicidir. Betik ölçülmez, ders yalnız ne yaptığını gösterir.

```zsh
./bump-build-number.sh
xcodebuild archive -scheme Shop -archivePath Shop.xcarchive
./upload-to-store.sh Shop.xcarchive
```

Kusurlu betikte ilk satır yoktur. Diğer iki satır bire bir aynıdır. Betiği okuyan göz kusuru ancak kaçıran satırı arayarak bulur ve kaçıran satırı aramak için önce kaçan bir şey olduğunu bilmek gerekir. Denetimin işi tam olarak budur.

#### Numarayı Kim Artırır

Derleme numarasını artırma işi üç yere konabilir ve her yerin kendi hatası vardır. Elle artırılırsa unutulur; bu, kusurlu betiğin özel bir hâlidir. Betik artırırsa betiğin her çalıştırmada artırdığından emin olmak gerekir: betik başarısız bir yüklemeden sonra yeniden koşarsa numarayı ikinci kez artırır ve numaralar atlar. Numara hattın sayacından türetilirse, her hat çalıştırması numarayı otomatik olarak yükseltir, ama bu kez numara ile sürüm geçmişi arasındaki bağ gevşer: iki yükleme aynı değişiklikten çıkmış olsa bile farklı numaralar taşır.

Hiçbir seçenek hatasız değildir ve seçim bu dersin konusu değildir. Dersin konusu, seçilen yolun **artırdığının** denetlenip denetlenmediğidir. Elle artıran ekip denetimi bir kontrol listesi gibi tutar; betik artıran ekip denetimi betiğin çıkışına koyar. İki durumda da denetim aynı soruyu sorar: yükleme numarası bir öncekinden büyük mü.

#### Modelin Kurulması

Model bir yüklemeyi iki alanla tutar: sürüm numarası ve derleme numarası. `prepareUpload` betiğin yaptığı işi taklit eder: mağazadaki son numarayı alır, doğru betikte bir artırır, kusurlu betikte olduğu gibi bırakır. `hasVersionShape` sürüm numarasının `x.y.z` biçiminde, yani nokta ile ayrılmış üç rakam grubu olduğunu denetler. `Reading` bir yüklemeyle birlikte **hangi son numarayla karşılaştırıldığını** taşır; bu, dersin son bölümünde gerekecek.

```swift
// Release.swift — yukleme betiginin sürüm ve derleme numarasi modeli
struct Upload: CustomStringConvertible {
    let version: String
    let buildNumber: Int
    var description: String { "\(version)(\(buildNumber))" }
}

struct Reading: CustomStringConvertible {
    let upload: Upload
    let recorded: Int
    var description: String { "\(upload)/kayit=\(recorded)" }
}

func hasVersionShape(_ version: String) -> Bool {
    let parts = version.split(separator: ".", omittingEmptySubsequences: false)
    return parts.count == 3 && parts.allSatisfy { !$0.isEmpty && $0.allSatisfy { $0.isNumber } }
}

func prepareUpload(version: String, lastUploaded: Int, bumpsNumber: Bool) -> Upload {
    Upload(version: version, buildNumber: bumpsNumber ? lastUploaded + 1 : lastUploaded)
}

let releaseVersion = "2.4.0"
let storeLast = 12
```

Doğru betik sürüm `2.4.0` ile derleme numarası `13` üretir. Kusurlu betik aynı sürümle `12` üretir, yani mağazadaki son numarayı tekrar eder.

**DG7.** Bu ölçüm, mağazanın aynı sürüm için derleme numarasının bir öncekinden büyük olmasını beklediğini dersin varsayımı olarak alır. Mağazanın gerçekte hangi numarayı kabul ettiği ya da reddettiği ölçülmedi.

#### Üç Denetim

Betiğin ürettiği yükleme üç denetimle sınanır. Birincisi sürüm numarasının `x.y.z` biçiminde olduğuna bakar: yükleme betiklerinde en sık yazılan denetimdir, çünkü yazması kolaydır. İkincisi derleme numarasının mağazadaki son numaradan büyük olduğuna bakar. Üçüncüsü derleme numarasının pozitif olduğuna bakar.

```swift
// main.swift — uc denetim, iki kategori
let withBump = prepareUpload(version: releaseVersion, lastUploaded: storeLast, bumpsNumber: true)
let withoutBump = prepareUpload(version: releaseVersion, lastUploaded: storeLast, bumpsNumber: false)

let shape = contrast("bicim-x-y-z", correct: withBump, faulty: withoutBump) { hasVersionShape($0.version) }.describe()
let greater = contrast("numara-son-numaradan-buyuk", correct: withBump, faulty: withoutBump) { $0.buildNumber > storeLast }.describe()
let positive = contrast("numara-pozitif", correct: withBump, faulty: withoutBump) { $0.buildNumber > 0 }.describe()

countSeparations([shape, greater, positive])
```

```text
bicim-x-y-z | dogru=2.4.0(13)/gecti kusurlu=2.4.0(12)/gecti -> kor
numara-son-numaradan-buyuk | dogru=2.4.0(13)/gecti kusurlu=2.4.0(12)/kaldi -> ayiran
numara-pozitif | dogru=2.4.0(13)/gecti kusurlu=2.4.0(12)/gecti -> kor
ayiran: 1
kor: 2
gurultulu: 0
ters: 0
```

Üç satır, iki kategori. `bicim-x-y-z` ve `numara-pozitif` iki yüklemede de aynı yanıtı verir: sürüm numarası iki yüklemede de `2.4.0`dır ve derleme numarası ikisinde de pozitiftir. Kusur bu iki değere dokunmaz, çünkü kusur bir **karşılaştırmadır**: numara yanlış değildir, mağazadaki son numarayla aynıdır. Yalnız `numara-son-numaradan-buyuk` bu karşılaştırmayı yapar. Doğru yüklemede `13 > 12` geçer, kusurlu yüklemede `12 > 12` kalır: **ayıran**.

#### Biçim Denetimi Neden Yaygın ve Neden Kör

Biçim denetimi yaygındır, çünkü yazması kolaydır ve çok kusur yakalar: sürüm numarasında harf, eksik grup, fazla nokta. Yakaladığı kusurlar gerçektir. Ama denetimin baktığı şey **tek bir yüklemenin kendi içindeki tutarlılıktır**; yüklemeyi başka bir yüklemeyle karşılaştırmaz. Artmayan numara ise ancak bir önceki yüklemeyle birlikte okununca görünür. Tek başına bakıldığında `2.4.0(12)` geçerli bir yüklemedir, `2.4.0(13)` de öyle.

Bu, kör denetimlerin ortak özelliğidir: denetim kusurun değiştirdiği değere bakmaz. Kusur burada derleme numarasının **önceki değerle ilişkisini** değiştirir, `x.y.z` biçimi ise yalnız sürüm numarasına bakar. Biçim denetiminin yeşil olması "numaralar doğru" cümlesini taşımaz; yalnız "sürüm numarası biçimli" cümlesini taşır. Betiğin ürettiği rapora bu iki cümleyi ayırmadan "numaralar geçerli" yazmak, ilkini ikincinin yerine geçirmektir.

`numara-pozitif` denetimi daha da zayıftır. Derleme numarasının sıfırdan büyük olmasını ister; kusurlu numara da `12`dir. Zayıf bir denetim kötü bir denetim demek değildir: sıfır ya da negatif bir numara üreten başka bir kusurda bu denetim ayıran olurdu. Ayrım, denetimle kusurun çiftine aittir; kusur değişince tablo da değişir.

#### Büyüklük Denetiminin Dayandığı Veri

`numara-son-numaradan-buyuk` ayırandır, ama bu ölçümde `storeLast` değeri doğrudan mağazadan gelmiş gibi alındı. Gerçek bir betikte son numaranın nereden okunduğu ayrı bir sorudur ve denetim o kaynağın doğruluğu kadar güvenilirdir. Üç yaygın kaynak vardır. Biri mağazanın kendisidir: betik yüklemeden önce son numarayı sorar. Biri yerel bir dosyadır: ekip son yüklenen numarayı bir dosyada tutar ve her yüklemede günceller. Biri de sürüm denetim geçmişidir: numara etiketlerden okunur. Her biri farklı biçimde eskir.

Yerel dosya en kolay olanıdır ve en çok sorun çıkaranıdır. Dosya, numarayı artıran adımda yazılır. Kusurlu betikte o adım yoktur; yani dosya da yazılmaz. Dosyada bir önceki yüklemeden kalma, mağazadakinden de **eski** bir numara durur. Denetim bu eski numarayla karşılaştırırsa, kusurlu yükleme eski numaradan büyük görünür ve geçer. Doğru yükleme ise dosyayı kendisi güncellemiştir; yeni numara kendi kendisine eşittir ve `>` kalır. Bu, ayıran denetimin tersine dönmesidir.

**DG8.** Bu ölçüm yerel kaydın yalnız numarayı artıran adımda güncellendiğini varsayar. Gerçek bir betiğin kaydı ne zaman ve nasıl yazdığı ölçülmedi.

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

Aynı denetim, derleme numarası son numaradan büyük, üç farklı son numara kaynağıyla koşar. Birinci kaynak mağazadır: iki yüklemede de son numara `12`dir. İkinci kaynak yükleme sonrasında yazılan bir dosyadır: iki betik de yüklediği numarayı dosyaya yazar, dolayısıyla dosya her yüklemede kendi numarasını taşır. Üçüncü kaynak yalnız numarayı artıran adımda güncellenen dosyadır: doğru betik `13` yazar, kusurlu betik yazmaz ve dosya `11`de kalır.

```swift
// main.swift — ayni buyukluk denetimi, uc son numara kaynagi
let withBump = prepareUpload(version: releaseVersion, lastUploaded: storeLast, bumpsNumber: true)
let withoutBump = prepareUpload(version: releaseVersion, lastUploaded: storeLast, bumpsNumber: false)

let fromStore = contrast("son-numara-magaza",
    correct: Reading(upload: withBump, recorded: storeLast),
    faulty: Reading(upload: withoutBump, recorded: storeLast)) { $0.upload.buildNumber > $0.recorded }.describe()
let afterUpload = contrast("son-numara-yukleme-sonrasi-dosya",
    correct: Reading(upload: withBump, recorded: withBump.buildNumber),
    faulty: Reading(upload: withoutBump, recorded: withoutBump.buildNumber)) { $0.upload.buildNumber > $0.recorded }.describe()
let staleFile = contrast("son-numara-artiran-adimda-dosya",
    correct: Reading(upload: withBump, recorded: withBump.buildNumber),
    faulty: Reading(upload: withoutBump, recorded: 11)) { $0.upload.buildNumber > $0.recorded }.describe()
let shapeAgain = contrast("bicim-x-y-z", correct: withBump, faulty: withoutBump) { hasVersionShape($0.version) }.describe()

countSeparations([fromStore, afterUpload, staleFile, shapeAgain])
```

```text
son-numara-magaza | dogru=2.4.0(13)/kayit=12/gecti kusurlu=2.4.0(12)/kayit=12/kaldi -> ayiran
son-numara-yukleme-sonrasi-dosya | dogru=2.4.0(13)/kayit=13/kaldi kusurlu=2.4.0(12)/kayit=12/kaldi -> gurultulu
son-numara-artiran-adimda-dosya | dogru=2.4.0(13)/kayit=13/kaldi kusurlu=2.4.0(12)/kayit=11/gecti -> ters
bicim-x-y-z | dogru=2.4.0(13)/gecti kusurlu=2.4.0(12)/gecti -> kor
ayiran: 1
kor: 1
gurultulu: 1
ters: 1
```

Aynı karşılaştırma, `buildNumber > recorded`, üç kaynakla üç ayrı kategoriye düştü ve dördüncü satırdaki biçim denetimiyle dört kategori de göründü. İlk satırda kaynak gerçeği söyler: **ayıran**. İkinci satırda dosya yüklemeden sonra yazılmıştır; her yükleme kendi numarasına eşit olduğundan iki sürüm de kalır: **gürültülü**. Kırmızı denetim betiğin yüklemeyi bozduğu izlenimini verir, oysa bozan karşılaştırmanın kendisidir. Üçüncü satırda doğru yükleme kalır, kusurlu yükleme geçer: **ters**.

Bu üçüncü satır ekibin neden bazen güvendiği bir denetimi kapattığını açıklar. Denetim doğru yüklemede kırmızıdır, ekip bunu yanlış alarm sayar ve eşik gevşetilir: `>` yerine `>=` yazılır. Böylece kusurlu yükleme de, doğru yükleme de geçer ve denetim körleşir. Yanlış alarm veriyormuş gibi görünen bir denetimi gevşetmek yerine **kaynağını** incelemek gerekir; `>` bu derste doğru yazılmıştı ve kusur kaynaktaydı.

Bu ölçümün sınırı açık söylenmeli. Yükleme ve mağazanın reddi ölçülmedi; betik yalnızca kaynak olarak gösterildi ve model yükleme sonucunu değil, yüklemeye giden iki sayıyı karşılaştırdı. Mağazanın ikinci aynı numarayı gerçekten reddedip reddetmediği, hangi iletiyle reddettiği ve ne kadar sürede reddettiği bu dersin dışında kalır.

**DG9.** Bu ölçümde dosyanın `11` değeri, kusurlu betiğin yazmadığı bir önceki kayıttır ve mağazadaki son numaradan eskidir. Eski kaydın gerçek bir betikte ne kadar eski olabileceği ölçülmedi.

#### Betik Çıktısına Ne Yazılır

Betiğin sonunda üretilen özet, ekibin yükleme hakkında okuduğu tek yerdir. Özete "sürüm 2.4.0, derleme 12, yüklendi" yazan bir betik, kusurlu yüklemeyi de doğru yükleme kadar temiz gösterir. Özetin ayıran bir bilgi taşıması için, numaranın **önceki numarayla** birlikte yazılması yeterlidir: "derleme 12, önceki 12". Bu iki sayıyı yan yana gören ekip, artmayan numarayı betik hiçbir denetim koşmasa da fark eder.

Aynı ilke başka yerlerde de geçerlidir. Bir sayının tek başına anlamı azdır; hangi sayıyla karşılaştırıldığı yazılırsa denetim okunur hâle gelir. Yüklemenin kaydı da bu yüzden yalnız son durumu değil, bir önceki durumu da taşımalıdır. Önceki durumu taşımayan bir kayıt, bu dersin son ölçümündeki gibi, eski ya da yazılmamış olduğunda sessizce yanlış karşılaştırma üretir.

Son olarak yeni bir denetim yazarken sorulacak soruyu hatırlayalım: kusurlu betiğin ürettiği yüklemeye bu denetim ne der? Bu soruyu yanıtlamak için betiği bilerek bozmak yeterlidir; ilk satırı silmek, ilk bozma denemesidir. Denetim ilk denemede kalmıyorsa kör ya da tersidir ve nedeni, dersin gösterdiği gibi, çoğunlukla denetimin baktığı veride yatar.

#### Özet

- Sürüm numarası kullanıcıya görünür, derleme numarası aynı sürümün art arda yüklemelerini ayırır; artmayan derleme numarası hiçbir adımı kırmızıya çevirmez.
- Yalnız `x.y.z` biçimine ya da numaranın pozitifliğine bakan denetimler iki yüklemede de geçer: kör.
- Derleme numarasının mağazadaki son numaradan büyük olup olmadığına bakan denetim kusurlu yüklemeyi ayırır: ayıran.
- Aynı büyüklük denetimi, son numara yükleme sonrası yazılan bir dosyadan okunursa gürültülü, yalnız numarayı artıran adımda güncellenen eski bir dosyadan okunursa ters olur.
- Kırmızı bir büyüklük denetimini gevşetmeden önce son numaranın kaynağı incelenir.
- Bu dersin ayrımı: **ayıran**.

#### Sonraki Adım

Numarası doğru artan bir yükleme hâlâ herkese açılmak zorunda değildir. Sıradaki derste, Beta Dağıtımı, yeni özelliğin yalnız sınırlı bir kitleye açılması ve bir bayrağın kitle dışına da açık kalması ölçülecek.

### Beta Dağıtımı

Source: https://academia.sh/tr/kurslar/ios-uretim/beta-dagitimi

Bir özellik bayrağının beta kitlesi dışındaki kullanıcılara da açık olduğunu beta kullanıcısında açık diyen denetimin göremediğinin, beta dışı kullanıcıda kapalı diyen denetimin ise gördüğünün ölçümü.

Bir sürüm herkese açılmadan önce küçük bir kitleye verilebilir. Bu kitle ekibin kendisi, davet edilen dış test kullanıcıları ya da ikisi birden olur. Sürüm, bir **beta dağıtım servisi** üzerinden davet edilen kullanıcıların aygıtlarına ulaşır ve onlar sürümü gerçek kullanım içinde dener. Amaç, sorunu herkese ulaşmadan bulmaktır.

Sürümün bir kitleye ulaşması ile özelliğin o kitleye açık olması aynı şey değildir. Sürüm herkese aynıdır; yeni özellik ise bir **özellik bayrağıyla** (feature flag) kapalı gelebilir ve yalnız belirli kullanıcılar için açılır. Beta kitlesinin dışına açılan bir bayrak, beta dağıtımının var olma nedenini ortadan kaldırır.

Kursun ölçü adı **ayrım**dır: aynı denetimi doğru sürüme ve kusurlu sürüme uygulayınca iki yanıt farklı mı. Dört kategori vardır: doğru sürümün geçip kusurlu sürümün kaldığı **ayıran**, ikisinin de geçtiği **kör**, ikisinin de kaldığı **gürültülü** ve doğru sürümün kalıp kusurlu sürümün geçtiği **ters**. Çıktıda bunlar `ayiran`, `kor`, `gurultulu` ve `ters` olarak görünür. Bu ders çekirdek dosyayla ölçer.

```swift
// 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 dersin kusuru bayrağın yapılandırmasındadır. Doğru sürümde yeni özellik yalnız bir izin listesindeki kullanıcılar için açıktır. Kusurlu sürümde yapılandırma "herkese açık" olarak kalmıştır ve izin listesi hiç okunmaz. Beta kullanıcıları için hiçbir şey değişmez: özellik onlar için açıktır ve çalışır. Fark yalnız beta dışındaki kullanıcılarda görünür.

#### Bayrağın Modeli

Bayrak iki değerden oluşur: herkese açık olup olmadığı ve izin listesi. `isOn` bir kullanıcı için bayrağın açık olup olmadığını söyler: bayrak herkese açıksa açık, değilse kullanıcı izin listesindeyse açık. `exposure` bir kullanıcı listesinin her biri için durumu tek bir dizgiye yazar, böylece iki sürümün yanıtı yan yana okunur.

Yapılandırma genellikle şuna benzer bir dosyada durur. Anahtar adları gerçek bir hizmetinki değildir; ders yalnız biçimi gösterir.

```yaml
flags:
  new-checkout:
    enabled_for: beta
    allowlist: [u01, u02, u03]
```

Kusurlu sürümde `enabled_for` değeri `everyone` olarak kalmıştır; izin listesi yerindedir ama kimse ona bakmaz. Yapılandırmayı okuyan göz bunu fark eder, çünkü tek bir değerdir. Okumayan, yalnız davranışa bakan denetim için ise bu fark görünmez olabilir.

```swift
// Beta.swift — ozellik bayraginin izin listesi ve kullanici durumlari
struct Rollout {
    let everyone: Bool
    let allowlist: Set<String>
}

func isOn(_ user: String, rollout: Rollout) -> Bool {
    rollout.everyone || rollout.allowlist.contains(user)
}

func exposure(of users: [String], rollout: Rollout) -> String {
    users.map { "\($0):\(isOn($0, rollout: rollout) ? "acik" : "kapali")" }.joined(separator: " ")
}

let allowlist: Set<String> = ["u01", "u02", "u03"]
let betaTesters = ["u01", "u02", "u03"]
let newTester = ["u04"]
let outsiders = ["u10", "u11"]
let enabledForBeta = Rollout(everyone: false, allowlist: allowlist)
let enabledForAll = Rollout(everyone: true, allowlist: allowlist)
```

Dersin kullanıcıları kısa kimliklerle yazılır: üç beta kullanıcısı, bir yeni davet edilen beta kullanıcısı ve iki beta dışı kullanıcı. Yeni davet edilen kullanıcı beta servisinde vardır ama izin listesine henüz eklenmemiştir; neden önemli olduğu sınırlayıcı ölçümde görünecek.

**DG10.** Bu ölçüm kullanıcıları küçük bir kimlik listesiyle temsil eder. Listenin boyutu kitlenin büyüklüğü hakkında hiçbir şey söylemez ve gerçek bir beta kitlesinin nasıl seçildiği ölçülmedi.

#### Üç Denetim

Bayrağa üç denetim uygulanır. İlki en doğal olanıdır: beta kullanıcılarında özellik açık mı. İkincisi beta dışı kullanıcılarda özelliğin kapalı olduğuna bakar. Üçüncüsü davranışa değil yapılandırmaya bakar: bayrak herkese açık değil mi.

```swift
// main.swift — uc denetim, iki kategori
let onBeta = contrast("beta-kullanicisinda-acik",
    correct: exposure(of: betaTesters, rollout: enabledForBeta),
    faulty: exposure(of: betaTesters, rollout: enabledForAll)) { !$0.contains("kapali") }.describe()
let onOutsiders = contrast("beta-disi-kullanicida-kapali",
    correct: exposure(of: outsiders, rollout: enabledForBeta),
    faulty: exposure(of: outsiders, rollout: enabledForAll)) { !$0.contains("acik") }.describe()
let configured = contrast("yapilandirma-herkese-acik-degil",
    correct: enabledForBeta.everyone,
    faulty: enabledForAll.everyone) { !$0 }.describe()

countSeparations([onBeta, onOutsiders, configured])
```

```text
beta-kullanicisinda-acik | dogru=u01:acik u02:acik u03:acik/gecti kusurlu=u01:acik u02:acik u03:acik/gecti -> kor
beta-disi-kullanicida-kapali | dogru=u10:kapali u11:kapali/gecti kusurlu=u10:acik u11:acik/kaldi -> ayiran
yapilandirma-herkese-acik-degil | dogru=false/gecti kusurlu=true/kaldi -> ayiran
ayiran: 2
kor: 1
gurultulu: 0
ters: 0
```

Üç denetimin üçü de doğru sürümde geçer. Kusurlu sürümde ise ilki de geçer: beta kullanıcılarında özellik **gerçekten** açıktır, kusurlu sürümde de. Bu denetim özelliğin çalışıp çalışmadığını doğru ölçer ve bayrağın kimlere açık olduğu hakkında hiçbir şey söylemez. İki sürümde de aynı yanıtı verir, **kör**.

İkinci denetim kusurun değiştirdiği kitleye bakar. Beta dışı kullanıcıda doğru sürüm kapalı, kusurlu sürüm açık gösterir ve denetim yalnızca kusurlu sürümde kalır: **ayıran**. Üçüncü denetim de ayırandır ama başka bir nedenle: kusur doğrudan yapılandırma değerinde durur ve denetim o değere bakar. İki ayıran denetimin farkı, ilkinin davranışı, ikincisinin dosyayı okumasıdır. Davranışı ölçen denetim yapılandırmanın başka bir yoldan değiştiği kusura da dayanıklıdır; yapılandırmayı okuyan denetim ise yapılandırmanın uygulamada gerçekten okunup okunmadığını göstermez.

#### Davet, Sürüm ve Bayrak

Beta dağıtımında kimin neyi gördüğünü üç ayrı katman belirler ve her katmanın kendi listesi vardır. Birincisi davet listesidir: hangi kullanıcıların sürümü indirebildiğini söyler. İkincisi sürümün kendisidir: indirilen paketin içinde hangi kodun bulunduğunu belirler. Üçüncüsü bayrağın izin listesidir: paketin içindeki özelliğin kime açıldığını söyler. Üç liste ayrı yerde durur ve ayrı kişiler tarafından güncellenir.

Bu ayrım, dersin kusurunun neden sessiz kaldığını açıklar. Davet listesi doğru olabilir, sürüm doğru paketlenmiş olabilir ve yine de bayrak herkese açık kalmış olabilir. Her katman kendi içinde tutarlıdır; tutarsızlık katmanlar arasındadır. Bir katmanı denetleyen sınama diğerlerinin kusurunu göremez. Yeni davetli kullanıcı örneği bunun küçük bir hâlidir: davet listesinde vardır, izin listesinde yoktur ve iki liste birbirine bakmaz.

#### Neden "Açık" Denetimi Yazılır

Beta kullanıcısında açık denetimi yaygındır, çünkü beta dağıtımının amacı özelliği denemektir ve ekip doğal olarak "özellik çalışıyor mu" diye sorar. Bu soru geçerlidir ve hattın bir yerinde durması gerekir. Ama bu soru **özellik çalışıyor mu** sorusudur, **özellik yalnız doğru kitlede mi çalışıyor** sorusu değildir. Bu iki soru farklı kusurları yakalar. Birinci soru kapalı gelen bayrağı bulur; ikinci soru fazla açılan bayrağı.

Fazla açılan bayrak daha az bilinen bir risktir, çünkü kimse şikâyet etmez. Beta kullanıcıları özelliği açık görür ve memnun kalır, beta dışı kullanıcılar da özelliği açık görür ve şikâyet etmez, çünkü özellik çalışıyordur. Hata ancak özellik beklenmedik bir şey yapınca, ya da yayın sorumlusu özelliğin kimlere göründüğüne baktığında ortaya çıkar. Kör denetim sessiz bir tehlikedir, çünkü bu sessizliği yeşil bir çıktı olarak raporlar.

#### Kör Denetimi Tamamlamak

Kör denetim silinmez, tamamlanır. Burada tamamlayıcı denetim, beta dışı kullanıcıda kapalı olandır. İkisi birlikte bayrağın iki yüzünü örter: açık olması gerekenlerde açık, kapalı olması gerekenlerde kapalı. Yalnız birini yazmak, hangi yüzün sınandığını bilmek koşuluyla kabul edilebilir; bilmeden yazılan birinci denetim, ikinciyi yazmanın gerekli olmadığı izlenimini verir.

Bozma denemesi burada da geçerlidir. Yapılandırmada `enabled_for` değerini bilerek `everyone` yapıp denetimleri koşmak, hangi denetimin kırmızıya döndüğünü gösterir. Dersin ilk ölçümü bu denemenin sonucudur: üç denetimden biri hiç kızarmadı.

#### Beta Geri Bildirimi Neyi Söylemez

Beta döneminin sonunda ekip genellikle geri bildirime bakar: kaç hata bildirimi gelmiş, kullanıcılar ne yazmış. Bildirimlerin azlığı sürümün sağlam olduğu izlenimini verir. Ama geri bildirim yalnız **görülen** davranışı yansıtır ve görülmeyen davranış için sessizdir. Bayrağın beta dışı kullanıcılara açık kalması bu sessizliğin tipik bir örneğidir: özellik çalışıyorsa kimse yazmaz.

Bu nedenle beta geri bildirimi bir denetim yerine geçmez. Geri bildirim "kullanıcılar bir şey fark etti mi" sorusunu yanıtlar; denetim "bayrak doğru kitleye mi açık" sorusunu yanıtlar. İkisi aynı beta döneminde birlikte yürür ve biri diğerinin eksiğini kapatmaz. Ekibin beta sonunda yazdığı özet iki ayrı satır taşırsa, geri bildirim ve bayrak durumu, okuyan kişi hangisinin neyi kanıtladığını bilir.

Kör bir denetimin bedeli de buradan görülür. Beta kullanıcısında açık diyen denetim her beta döneminde yeşildir ve bu yeşil ekibe beta döneminin sorunsuz geçtiği güvenini verir. Yeşilin altında bayrağın herkese açık olduğu bir hata yatıyorsa, güven yanlış yerdendir. Çözüm yeşili silmek değil, yanına kitle dışı denetimini koymaktır.

#### Kitle Genişleyince Denetim

Beta kitlesi büyür: başka bir ekip eklenir, dış test kullanıcıları davet edilir. Her genişlemede izin listesi değişir ve "beta dışı" tanımı da onunla birlikte değişir. Beta dışı kullanıcılar için elle yazılmış bir liste, genişlemeden sonra bir beta kullanıcısını içerebilir. Böyle bir denetim doğru sürümde kalır, çünkü artık beta kullanıcısı olan kullanıcı için özellik açıktır. Denetim yanlış alarm verir ve ekip onu kapatır; kitle genişledi diye bir denetimin kapanması ise bayrağın kitle dışına açılma riskini örtüsüz bırakır.

Çözüm, beta dışı kullanıcıları listeden elle seçmek yerine davet listesinden **türetmektir**: davet listesinde olmayan her kullanıcı beta dışıdır. Tanım bir yerde durduğu için genişleme denetimi bozmaz. Denetimin okuduğu veri kaynağını bu şekilde birleştirmek, ayrımı kullanıcı seçimine bağlı olmaktan çıkarır.

#### Bayrak Geri Alınırken

Beta sırasında bir sorun çıkarsa bayrak kapatılır; yeni bir sürüm yüklemek gerekmez. Bu, bayrakların asıl değerlerinden biridir ve geri alma da bir denetim konusudur. Bayrak kapatıldıktan sonra beta kullanıcılarında özelliğin **kapalı** olduğu denetlenirse, o denetim kapatma işleminin gerçekten uygulandığını gösterir. Burada da aynı soru geçerlidir: denetim, kapatılmamış bir bayrağı yakalayacak şekilde mi yazılmış? Beta kullanıcısında açık diyen denetimin tersini yazmak yetmez; hangi sürümde ne yanıt verdiğine bakmak gerekir.

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

Aynı iki denetim, farklı kullanıcı listeleriyle, farklı kategorilere düşer. Birinci liste üç beta kullanıcısıdır. İkinci liste, yeni davet edilen ve izin listesine henüz eklenmemiş tek bir beta kullanıcısıdır. Üçüncü ve dördüncü satırlar beta dışı kapalı denetimini beta kullanıcılarının listesine ve bir beta dışı kullanıcıya uygular.

```swift
// main.swift — ayni denetimler, farkli kullanici listeleri
let open = contrast("acik-uc-beta-kullanicisi",
    correct: exposure(of: betaTesters, rollout: enabledForBeta),
    faulty: exposure(of: betaTesters, rollout: enabledForAll)) { !$0.contains("kapali") }.describe()
let invited = contrast("acik-yeni-davetli",
    correct: exposure(of: newTester, rollout: enabledForBeta),
    faulty: exposure(of: newTester, rollout: enabledForAll)) { !$0.contains("kapali") }.describe()
let closedOnBeta = contrast("kapali-beta-kullanicilari-uzerinde",
    correct: exposure(of: betaTesters, rollout: enabledForBeta),
    faulty: exposure(of: betaTesters, rollout: enabledForAll)) { !$0.contains("acik") }.describe()
let closedOnOutsider = contrast("kapali-bir-beta-disi",
    correct: exposure(of: ["u10"], rollout: enabledForBeta),
    faulty: exposure(of: ["u10"], rollout: enabledForAll)) { !$0.contains("acik") }.describe()

countSeparations([open, invited, closedOnBeta, closedOnOutsider])
```

```text
acik-uc-beta-kullanicisi | dogru=u01:acik u02:acik u03:acik/gecti kusurlu=u01:acik u02:acik u03:acik/gecti -> kor
acik-yeni-davetli | dogru=u04:kapali/kaldi kusurlu=u04:acik/gecti -> ters
kapali-beta-kullanicilari-uzerinde | dogru=u01:acik u02:acik u03:acik/kaldi kusurlu=u01:acik u02:acik u03:acik/kaldi -> gurultulu
kapali-bir-beta-disi | dogru=u10:kapali/gecti kusurlu=u10:acik/kaldi -> ayiran
ayiran: 1
kor: 1
gurultulu: 1
ters: 1
```

Dört satırda dört kategori. `acik-uc-beta-kullanicisi` ilk ölçümle aynıdır: **kör**. `acik-yeni-davetli` **ters**tir: yeni davetli kullanıcı izin listesinde olmadığı için doğru sürümde özellik ona kapalıdır ve denetim kalır; kusurlu sürümde bayrak herkese açık olduğundan özellik ona da açıktır ve denetim geçer. Yani izin listesini güncellemeyi unutan ekip, doğru sürümde bir hata alır ve kusurlu sürümde hiç hata görmez. Kırmızı olan doğru sürümdür.

Üçüncü satırda denetim beta kullanıcıları üzerinde "kapalı" arar ve iki sürümde de kalır, **gürültülü**: beta kullanıcılarında özellik her iki sürümde de açıktır. Bu satır kötü bir denetimin tasarımını gösterir: denetimin beklentisi ile baktığı kullanıcılar uyuşmaz. Dördüncü satır, tek bir beta dışı kullanıcıyla, yine **ayıran**dır. Yani ayıranı bulmak için büyük bir kullanıcı listesi gerekmez; doğru kullanıcının listede bulunması yeter.

Bu ölçüm kullanıcı seçiminin önemini gösterir, ama bir sınırı da vardır. Listenin her biri küçüktür ve gerçek bir kitlede izin listesinin eksikleri, hatalı yazılmış kimlikleri ve eski hesapları da olur. Kitle büyüklüğü ve oranlar ölçülmedi.

**DG11.** Bu ölçüm izin listesinin beta davetlerinden ayrı bir yerde tutulduğunu varsayar. Davet ile listenin gerçek bir hatta nasıl eşitlendiği ölçülmedi.

**DG12.** Bu ölçüm beta servisinin sürümü kullanıcıya ulaştırma zamanını modellemez. Davetin ne zaman iletildiği ya da sürümün aygıta ne zaman indiği ölçülmedi.

#### Özet

- Beta dağıtımı sürümü sınırlı bir kitleye ulaştırır; özelliğin kimlere açık olduğunu ise özellik bayrağı belirler.
- Beta kullanıcısında özelliğin açık olduğunu denetleyen sınama, bayrak kitle dışına da açıkken iki sürümde de geçer: kör.
- Beta dışı kullanıcıda kapalı olduğunu denetleyen sınama kusuru ayırır: ayıran.
- Aynı "açık" denetimi, izin listesine henüz eklenmemiş yeni davetli kullanıcıda ters olur.
- Denetimin ayrımı baktığı kullanıcı listesine bağlıdır; kör denetim silinmez, kapalı olması gerekenleri denetleyen bir eşle tamamlanır.
- Bu dersin ayrımı: **kör**.

#### Sonraki Adım

Beta kitlesinde sınanan sürüm sonunda mağazanın incelemesine gider. Sıradaki derste, Mağaza İnceleme Süreci, bir hazırlık listesinin hangi maddelerinin kusuru ayırdığı ve hangilerinin her iki sürümde de kaldığı ölçülecek.

### Mağaza İnceleme Süreci

Source: https://academia.sh/tr/kurslar/ios-uretim/magaza-inceleme-sureci

Bir hazırlık listesinde izin açıklamasının dolu olup olmadığına bakan maddenin kusuru ayırdığının, listede kalan eski bir maddenin ise iki sürümde de kırmızı kalarak listeyi gürültülü yaptığının ölçümü.

Bir sürüm mağazaya gönderilmeden önce ekipler çoğunlukla bir hazırlık listesinden geçer: ekran görüntüleri yerinde mi, gizlilik bağlantısı çalışıyor mu, izin açıklamaları yazılmış mı. Liste, geçmiş gönderimlerde yaşanan sorunlardan çıkarılır ve her yeni sorunla uzar. Mağazanın inceleme kararı bu listenin dışında verilir; liste ekibin kendi korunma biçimidir.

Kursun ölçü adı **ayrım**dır: aynı denetimi doğru sürüme ve kusurlu sürüme uygulayınca iki yanıt farklı mı. Dört kategori vardır: doğru sürümün geçip kusurlu sürümün kaldığı **ayıran**, ikisinin de geçtiği **kör**, ikisinin de kaldığı **gürültülü** ve doğru sürümün kalıp kusurlu sürümün geçtiği **ters**. Çıktıda bunlar `ayiran`, `kor`, `gurultulu` ve `ters` olarak görünür. Bu ders çekirdek dosyayla ölçer.

```swift
// 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 dersin kusuru üstverideki bir alandadır: konum izni için kullanıcıya gösterilecek **açıklama metni** boştur. Uygulama konumu isteyebilir, ama izin ekranı kullanıcıya neden istendiğini söylemez. Derleme geçer, imza geçer, test geçer; bu alan hiçbir derleme adımının kontrol ettiği bir şey değildir. Mağaza incelemesinde böyle bir eksiğin gönderimi geri çevirmesi mümkündür; ders bu olasılığı yalnız hazırlık listesinin bir maddesi olarak kodlar.

#### Hazırlık Listesinin Modeli

Model bir gönderimi üç alanla tutar: konum izni açıklaması, ekran görüntüsü boyutları ve gizlilik bağlantısının varlığı. Hazırlık listesi üç maddeden oluşur. `izin-aciklamasi-dolu` açıklamanın boş olmadığına bakar ve yalnız boşluklardan oluşan bir metni de boş sayar. `gizlilik-baglantisi-var` bağlantının tanımlı olduğuna bakar. `eski-boyut-gorseli-var` ise ekran görüntüsü kümesinde `eski` adlı bir boyutun bulunmasını ister; bu madde ekibin eski bir gönderiminden kalmadır.

`readiness` her maddeyi gönderime uygular ve `madde:gecti` ya da `madde:kaldi` yazar. `Submission` kendini tek satırda özetler; böylece çıktıda hangi alanın ne taşıdığı görülür.

```swift
// Review.swift — gonderimin alanlari ve ekibin hazirlik listesi
struct Submission: CustomStringConvertible {
    let locationPurpose: String
    let screenshotSizes: [String]
    let privacyLink: Bool

    var purposeState: String {
        if locationPurpose.isEmpty { return "bos" }
        if locationPurpose.allSatisfy({ $0.isWhitespace }) { return "bosluk" }
        return "dolu"
    }

    var description: String {
        "izin=\(purposeState) boyut=\(screenshotSizes.joined(separator: ",")) gizlilik=\(privacyLink ? "var" : "yok")"
    }
}

let readinessItems = ["gizlilik-baglantisi-var", "eski-boyut-gorseli-var", "izin-aciklamasi-dolu"]

func itemPasses(_ item: String, _ submission: Submission) -> Bool {
    switch item {
    case "izin-aciklamasi-dolu": return submission.purposeState == "dolu"
    case "izin-aciklamasi-bos-degil": return !submission.locationPurpose.isEmpty
    case "gizlilik-baglantisi-var": return submission.privacyLink
    case "eski-boyut-gorseli-var": return submission.screenshotSizes.contains("eski")
    default: return false
    }
}

func readiness(_ submission: Submission, items: [String]) -> [String] {
    items.map { "\($0):\(itemPasses($0, submission) ? "gecti" : "kaldi")" }
}

func listPasses(_ submission: Submission, items: [String]) -> Bool {
    readiness(submission, items: items).allSatisfy { $0.hasSuffix(":gecti") }
}

let correctSubmission = Submission(locationPurpose: "Yakindaki magazalari gostermek icin konumunuz kullanilir", screenshotSizes: ["buyuk", "orta"], privacyLink: true)
let emptySubmission = Submission(locationPurpose: "", screenshotSizes: ["buyuk", "orta"], privacyLink: true)
let spaceSubmission = Submission(locationPurpose: "   ", screenshotSizes: ["buyuk", "orta"], privacyLink: true)
```

İki gönderim yalnız açıklama alanında ayrılır. Ekran görüntüleri `buyuk` ve `orta` boyutlarındadır; `eski` boyut ekibin görsel düzeninden kaldırılmıştır ve her iki gönderimde de yoktur. Madde ise listede kalmıştır.

**DG13.** Bu ölçümün hazırlık listesi ekibin kendi listesidir. Maddeler mağazanın yayımladığı bir kural değildir ve mağazanın bir gönderimi hangi gerekçeyle geri çevirdiği ölçülmedi.

#### Dört Denetim

Aynı iki gönderime dört denetim uygulanır: açıklama maddesi, eski boyut maddesi, gizlilik maddesi ve listenin bütünü.

```swift
// main.swift — uc madde ve listenin butunu
let purpose = contrast("izin-aciklamasi-dolu", correct: correctSubmission, faulty: emptySubmission) { itemPasses("izin-aciklamasi-dolu", $0) }.describe()
let oldSize = contrast("eski-boyut-gorseli-var", correct: correctSubmission, faulty: emptySubmission) { itemPasses("eski-boyut-gorseli-var", $0) }.describe()
let privacy = contrast("gizlilik-baglantisi-var", correct: correctSubmission, faulty: emptySubmission) { itemPasses("gizlilik-baglantisi-var", $0) }.describe()
let wholeList = contrast("liste-tamami-gecti", correct: correctSubmission, faulty: emptySubmission) { listPasses($0, items: readinessItems) }.describe()

countSeparations([purpose, oldSize, privacy, wholeList])
```

```text
izin-aciklamasi-dolu | dogru=izin=dolu boyut=buyuk,orta gizlilik=var/gecti kusurlu=izin=bos boyut=buyuk,orta gizlilik=var/kaldi -> ayiran
eski-boyut-gorseli-var | dogru=izin=dolu boyut=buyuk,orta gizlilik=var/kaldi kusurlu=izin=bos boyut=buyuk,orta gizlilik=var/kaldi -> gurultulu
gizlilik-baglantisi-var | dogru=izin=dolu boyut=buyuk,orta gizlilik=var/gecti kusurlu=izin=bos boyut=buyuk,orta gizlilik=var/gecti -> kor
liste-tamami-gecti | dogru=izin=dolu boyut=buyuk,orta gizlilik=var/kaldi kusurlu=izin=bos boyut=buyuk,orta gizlilik=var/kaldi -> gurultulu
ayiran: 1
kor: 1
gurultulu: 2
ters: 0
```

Dört satır, listenin üç ayrı davranışını gösterir. İzin açıklaması maddesi doğru gönderimi geçirir, boş açıklamalı gönderimi kaldırır: **ayıran**. Gizlilik bağlantısı maddesi iki gönderimde de geçer; bağlantı iki sürümde de var olduğu için kusuru görmez ve bu yüzden **kör**dür. Kör olmak bir kusur değildir; madde başka bir sorun için yazılmıştır ve bu kusurda söyleyecek sözü yoktur.

Asıl ders eski boyut maddesindedir. Madde iki gönderimde de kalır, çünkü ekran görüntüsü kümesinde `eski` boyut yoktur. Doğru gönderim bile listeyi geçemez. Kusurlu gönderim de kalır, ama kalma nedeni kusurla ilgisizdir. İki yanıt aynı olduğu için madde **gürültülü** sayılır: kırmızı görünür, ayrım taşımaz.

#### Gürültünün Bedeli

Listenin tamamı satırında bu bedel görünür. `liste-tamami-gecti` doğru gönderimde de kalır; bunun tek nedeni eski boyut maddesidir. Listeyi okuyan kişi iki durumla karşılaşır. Birincisi, kırmızıyı görüp nedenine bakar, eski bir maddeye takıldığını anlar ve devam eder. İkincisi, kırmızıyı artık bildiği için hiç bakmaz. Ekipler çoğu zaman ikinci yola düşer, çünkü hiçbir şeyi ayırmayan bir kırmızı bir süre sonra görülmez olur.

Bu görmezden gelme alışkanlığı, listede gerçek bir kusur kırmızıya döndüğünde de sürer. İzin açıklaması maddesi kusuru doğru biçimde yakalıyor olabilir; ama liste zaten kırmızı olduğundan yakalanan kusur kalabalığın içinde kaybolur. Maddenin tek başına ayıran olması, listenin işe yaradığı anlamına gelmez. Ayrımı taşıyan şey listenin bütününün davranışıdır ve o, gürültülü maddeyle birlikte gürültülüdür.

**DG14.** Bu ölçüm, maddenin neden eski kaldığını bilmez. Ekran görüntüsü düzeninin değiştiği ve `eski` boyutun artık üretilmediği dersin varsayımıdır. Gerçek bir ekibin listesinde böyle bir maddenin hâlâ gerekli olduğu bir durum da olabilir.

#### Eski Madde Nasıl Temizlenir

Gürültülü maddeyle iki şey yapılabilir: silmek ya da güncellemek. Silme, maddenin kapsadığı kaygı artık yoksa doğrudur. Ekran görüntüsü düzeni değiştiyse ve eski boyut hiçbir gönderimde istenmiyorsa madde listeye ait değildir. Güncelleme ise kaygının sürdüğü ama ifade biçiminin eskidiği durumda yapılır: madde bugün istenen boyutları sayar.

Karar verirken sorulacak soru ölçümün sorusudur: bu madde, kusurlu bir gönderimle doğru bir gönderimi bir araya koyduğumda iki yanıtı ayırıyor mu? Ayırmıyorsa ve her gönderimde aynı yanıtı veriyorsa, ya yanıt hep geçtidir ve madde kördür ya da yanıt hep kaldıdır ve madde gürültülüdür. İkisinin de kendine göre bir yeri olabilir. Kör bir madde, yalnızca bir kez yaşanmış bir kusuru kollayan bekçi gibi durabilir; gürültülü madde ise bu işi bile yapmaz.

Silmeden önce de bir ölçüm yapılır. Maddeyi listeden çıkarıp aynı iki gönderimi yeniden denetlemek, listenin bütününün artık doğru gönderimi geçirip kusurluyu kaldırdığını gösterir. Bu, maddenin gerçekten gürültüden ibaret olduğunun kanıtıdır. Kanıt yoksa silme, kuru bir tahmindir.

#### Kırmızı Madde ile Gönderim Kararı

Listenin kırmızı çıkması ile gönderimin durdurulması arasında bir karar vardır ve o kararı liste vermez. Ekip, kırmızı maddeyi okuyup gönderimi durdurur, ya da maddenin eski olduğuna karar verip gönderir. Birinci yol güvenlidir ama yavaştır; ikinci yol hızlıdır ama listenin kendi sözünü çiğner. Gürültülü madde tam bu ikilemi her gönderimde yeniden yaratır.

Ayıran bir maddede böyle bir ikilem yoktur. Kırmızı, gönderimde bir şeyin yanlış olduğu anlamına gelir ve yeşil, o şeyin yerinde olduğu anlamına gelir. Karar da buna göre kolaylaşır. Listeyi ölçmenin pratik getirisi budur: kırmızıya bakıp tartışmak yerine kırmızıya bakıp düzeltmek.

Bir de kimin okuduğu sorusu vardır. Hazırlık listesini çoğu zaman gönderimi yapan kişi okur; maddelerin yazıldığı zamanı o hatırlamaz. Maddenin neden listede olduğunu bilmeyen biri, eski boyut maddesini ciddiye alır ve boşuna bir görsel hazırlamaya girişebilir. Gürültülü madde bu yüzden yalnız zaman değil, emek de harcatır.

#### Boş ile Yalnız Boşluk

İzin açıklaması maddesi iki biçimde yazılabilir. `izin-aciklamasi-dolu` metnin yalnız boşluklardan oluşmadığına da bakar. `izin-aciklamasi-bos-degil` ise yalnızca metnin sıfır uzunlukta olmadığına bakar. İkisi aynı amaçla yazılmış görünür; ama üç boşluktan oluşan bir açıklama karşısında farklı davranırlar. Kullanıcı ekranında üç boşluk gösteren bir açıklama, boş bir açıklamadan farksızdır; ancak ikinci madde bunu göremez.

Bir sonraki ölçüm aynı iki denetimi üç boşluklu gönderime uygular. Yanına eski boyut maddesini de koyar; böylece gürültülü maddenin girdi değişince de aynı kaldığı görülür.

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

Aynı maddeler, boş açıklama yerine yalnız boşluktan oluşan açıklama taşıyan gönderime uygulanır. Ekranda ikisi de aynı boşluğu gösterir.

```swift
// main.swift — ayni maddeler, bos ve bosluk aciklamasi
let emptyByLength = contrast("bos-degil-bos-aciklama", correct: correctSubmission, faulty: emptySubmission) { itemPasses("izin-aciklamasi-bos-degil", $0) }.describe()
let blankByLength = contrast("bos-degil-bosluk-aciklama", correct: correctSubmission, faulty: spaceSubmission) { itemPasses("izin-aciklamasi-bos-degil", $0) }.describe()
let blankByContent = contrast("dolu-bosluk-aciklama", correct: correctSubmission, faulty: spaceSubmission) { itemPasses("izin-aciklamasi-dolu", $0) }.describe()
let oldSizeOnBlank = contrast("eski-boyut-bosluk-aciklama", correct: correctSubmission, faulty: spaceSubmission) { itemPasses("eski-boyut-gorseli-var", $0) }.describe()

countSeparations([emptyByLength, blankByLength, blankByContent, oldSizeOnBlank])
```

```text
bos-degil-bos-aciklama | dogru=izin=dolu boyut=buyuk,orta gizlilik=var/gecti kusurlu=izin=bos boyut=buyuk,orta gizlilik=var/kaldi -> ayiran
bos-degil-bosluk-aciklama | dogru=izin=dolu boyut=buyuk,orta gizlilik=var/gecti kusurlu=izin=bosluk boyut=buyuk,orta gizlilik=var/gecti -> kor
dolu-bosluk-aciklama | dogru=izin=dolu boyut=buyuk,orta gizlilik=var/gecti kusurlu=izin=bosluk boyut=buyuk,orta gizlilik=var/kaldi -> ayiran
eski-boyut-bosluk-aciklama | dogru=izin=dolu boyut=buyuk,orta gizlilik=var/kaldi kusurlu=izin=bosluk boyut=buyuk,orta gizlilik=var/kaldi -> gurultulu
ayiran: 2
kor: 1
gurultulu: 1
ters: 0
```

Uzunluğa bakan madde boş açıklamada ayıran, boşluk açıklamasında kördür. Aynı kusurun iki görünümü aynı maddeye farklı yanıt verdirir. İçeriğe bakan madde ise ikisinde de ayırır. Eski boyut maddesi girdi ne olursa olsun kırmızı kalır ve bu ölçümde de gürültülüdür.

Sınırlar açıktır. Bu ders mağazanın inceleme kararını ya da süresini ölçmedi; ölçülmesi gereken şey de bu değildir, çünkü o karar ekibin elindeki bir girdi olmaktan çok bir çıktıdır. Ders mağazanın hangi gerekçelerle gönderimleri geri çevirdiğini de ölçmedi. Ret nedenleri sayısızdır: izin açıklamaları, gizlilik metinleri, ekran görüntülerinin içeriği, uygulamanın bildirdiğinden farklı davranması ve daha pek çoğu bunlardandır. Bu sayısızlığın içinden bir listeye girenler, ekibin geçmişte yaşadıklarıdır.

**DG15.** Hazırlık listesindeki her madde mağazanın kuralı değil, ekibin listesidir. Listenin geçmesi incelemenin geçeceği anlamına gelmez; listenin kalması da gönderimin geri çevrileceğini söylemez. Ders yalnız şunu ölçer: listedeki bir madde, ekibin bildiği bir kusuru, o kusursuz bir gönderimden ayırıyor mu.

#### Listeyi Dürüst Tutmak

Hazırlık listesi iki yönde çürür. Birincisi, kusur kaynağı ortadan kalktığında maddenin listede kalmasıdır; eski boyut maddesi budur. İkincisi, yeni bir kusurun listeye girmeden geçmesidir. Bir gönderim geri çevrildiğinde yapılacak iş, geri çevirme nedenini bir maddeye çevirmek ve o maddeyi bu dersin ölçümüyle sınamaktır: maddeyi kusurlu gönderime uygula, doğru gönderime uygula, iki yanıtın farklı olduğunu gör.

Bu sınama maddeyi eklemeden önce yapılır, çünkü eklenen her madde bir bakım yükü getirir. Ayıran olmayan bir madde yük getirir ama ayrım getirmez. Kör madde bir sonraki kusuru yakalamaz; gürültülü madde ise listeyi okuyan kişiyi kırmızıya alıştırır. Ters bir madde en kötüsüdür: doğru gönderimi kaldırıp kusurluyu geçirir ve ekibi yanlış yöne iter.

Listenin bakımı bir kişiye ya da bir adıma bağlanırsa işler. Her gönderimden sonra liste tek bir soruyla gözden geçirilir: bu gönderimde hangi madde yalnız kırmızı olup kalmıştı? O madde ya güncellenir ya silinir. Bu alışkanlık, listeyi geçmişin değil bugünün kusurlarını kollayan bir araca çevirir.

#### Özet

- Mağaza incelemesi öncesindeki hazırlık listesi, ekibin kendi kusur geçmişinden çıkar; mağazanın yayımladığı bir kural değildir.
- İzin açıklamasının dolu olup olmadığına bakan madde, boş açıklamalı gönderimi doğru gönderimden ayırdığı için ayırandır.
- Güncelliğini yitirmiş bir madde iki sürümde de kırmızı kalır ve listenin bütününü gürültülü yapar.
- Aynı kusurun boş ve yalnız boşluktan oluşan iki görünümü, yalnızca uzunluğa bakan maddeye farklı yanıt verdirir; içeriğe bakan madde ikisini de ayırır.
- Maddeyi listeye almadan ya da listeden çıkarmadan önce, kusurlu ve doğru gönderim üzerinde ölçmek gerekir.
- Bu dersin ayrımı: **gürültülü**.

#### Sonraki Adım

Hazırlık listesi gönderimin içeriğini denetler; mağazada kullanıcının gördüğü sayfa ise ayrı bir iştir. Sonraki derste **Mağaza Listelemesi** ele alınacak: ekran görüntüleri ile alt başlığın, kayıtlı bir referansla karşılaştırıldığında kusuru ayırıp ayırmadığı ve eşitlik denetiminin ters çalışabildiği bir durum ölçülecek.

### Mağaza Listelemesi

Source: https://academia.sh/tr/kurslar/ios-uretim/magaza-listelemesi

Kayıtlı ekran görüntüsüyle eşitliğe bakan denetimin, referans kusurlu derlemeden alındığında doğru sürümü kaldırıp kusurlu sürümü geçirdiğinin ve alt başlık sınırı denetiminin kusuru göremediğinin ölçümü.

Mağazadaki uygulama sayfası, kullanıcının uygulamayı indirmeden önce gördüğü tek şeydir: birkaç ekran görüntüsü, bir ad, bir alt başlık ve bir açıklama. Bu parçalar uygulamanın kodundan ayrı tutulur, çoğunlukla geliştirici panelinden elle yüklenir ve bu yüzden kodun denetimlerinden geçmez. Sayfa ile uygulama arasında bir uyumsuzluk çıktığında bunu ilk fark eden, çoğu zaman bir kullanıcıdır.

Kursun ölçü adı **ayrım**dır. Doğru sürüme ve kusurlu sürüme aynı denetimi uygularız; denetim iki sürümü ayırıyorsa **ayıran**, ikisini de geçiriyorsa **kör**, ikisini de kaldırıyorsa **gürültülü**, doğruyu kaldırıp kusurluyu geçiriyorsa **ters**tir. Çıktıda bu dört ad `ayiran`, `kor`, `gurultulu` ve `ters` olarak yazılır. Bu ders de aynı çekirdek dosyayla ölçer.

```swift
// 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 dersin kusuru sayfanın ekran görüntüsündedir: ekran görüntüsü yeni arayüzü değil eskisini gösterir. Uygulama yenilenmiş, sayfa yenilenmemiştir. Kullanıcı eski bir arayüz görür, indirir ve bambaşka bir uygulamayla karşılaşır. Alt başlık ise iki sürümde de aynıdır ve dersin koyduğu sınırın içindedir.

#### Listelemenin Modeli

Model, bir derlemeden iki şey çıkarır: ekran görüntüsünün hangi arayüzü gösterdiği ve alt başlığın metni. `listing(of:)` bu ikisini bir `Listing` değerinde toplar; `Listing` kendini tek satırda özetler, böylece çıktıda ekranın ne gösterdiği ve alt başlığın kaç karakter olduğu görülür.

Üç denetim vardır. `matchesReference` ekran görüntüsünü kayıtlı bir referansla karşılaştırır; ekiplerin görüntü karşılaştırmasıyla yaptığı eşitlik denetiminin sadeleştirilmiş biçimidir. `subtitleFits` alt başlığın karakter sayısının sınırı aşmadığına bakar. `showsNewInterface` ise ekran görüntüsünün yeni arayüzü gösterdiğini doğrudan sınar.

```swift
// Listing.swift — derlemeden listeleme, uc denetim
struct Build {
    let interface: String
    let subtitle: String
}

struct Listing: CustomStringConvertible {
    let screenshot: String
    let subtitle: String

    var description: String {
        "ekran=\(screenshot) alt-baslik=\(subtitle.count)"
    }
}

func listing(of build: Build) -> Listing {
    Listing(screenshot: "ekran-\(build.interface)", subtitle: build.subtitle)
}

func matchesReference(_ listing: Listing, reference: String) -> Bool {
    listing.screenshot == reference
}

func subtitleFits(_ listing: Listing, limit: Int) -> Bool {
    listing.subtitle.count <= limit
}

func showsNewInterface(_ listing: Listing) -> Bool {
    listing.screenshot == "ekran-yeni"
}

let subtitleLimit = 30
let correctBuild = Build(interface: "yeni", subtitle: "Siparisleri tek yerde izleyin")
let faultyBuild = Build(interface: "eski", subtitle: "Siparisleri tek yerde izleyin")
```

**DG16.** Alt başlık sınırı otuz karakterdir. Bu sayı dersin kuralıdır; mağazanın gerçek sınırı ölçülmedi ve ölçülen hiçbir şey ona dayanmaz. Ders yalnız şunu sorar: sınırı aşmadığına bakan bir denetim, bu kusuru görüyor mu.

#### Üç Denetim

Referans, sayfaya ilk kez ekran görüntüsü yüklendiğinde kaydedilir. Kaydı alan kişi o gün elindeki derlemeden görüntü almıştır. Bu ölçümde o derleme kusurludur: ekran görüntüsü `ekran-eski` olarak kaydedilmiştir. Eşitlik denetimi bu kayda göre çalışır.

```swift
// main.swift — kayitli referansla esitlik, alt baslik sinirlari, dogrudan sinama
let correctListing = listing(of: correctBuild)
let faultyListing = listing(of: faultyBuild)
let recordedFromFaulty = faultyListing.screenshot

let equality = contrast("ekran-referansa-esit", correct: correctListing, faulty: faultyListing) { matchesReference($0, reference: recordedFromFaulty) }.describe()
let subtitle = contrast("alt-baslik-sinir-icinde", correct: correctListing, faulty: faultyListing) { subtitleFits($0, limit: subtitleLimit) }.describe()
let direct = contrast("ekran-yeni-arayuzu-gosteriyor", correct: correctListing, faulty: faultyListing) { showsNewInterface($0) }.describe()

countSeparations([equality, subtitle, direct])
```

```text
ekran-referansa-esit | dogru=ekran=ekran-yeni alt-baslik=29/kaldi kusurlu=ekran=ekran-eski alt-baslik=29/gecti -> ters
alt-baslik-sinir-icinde | dogru=ekran=ekran-yeni alt-baslik=29/gecti kusurlu=ekran=ekran-eski alt-baslik=29/gecti -> kor
ekran-yeni-arayuzu-gosteriyor | dogru=ekran=ekran-yeni alt-baslik=29/gecti kusurlu=ekran=ekran-eski alt-baslik=29/kaldi -> ayiran
ayiran: 1
kor: 1
gurultulu: 0
ters: 1
```

Eşitlik denetimi **ters** çıktı. Doğru sürüm kayıtlı görüntüden farklıdır ve denetim kalır. Kusurlu sürüm kayıtla birebir aynıdır ve denetim geçer. Denetim kendi içinde doğru çalışıyor: iki şeyin eşit olup olmadığına bakıyor. Yanlış olan, neyle eşit olması gerektiğidir. Referans kusurlu bir derlemeden kaydedildiği için denetim, kusurun korunduğunu doğruluyor.

Bu, ters olmanın en sinsi biçimidir. Gürültülü bir madde kırmızıya döner ve dikkat çeker; kör bir madde sessizce geçer. Ters madde ise kusurlu sürümde yeşildir ve ekip bu yeşili bir uyarıdan çok bir onay olarak okur: sayfayı denetledik, kayıtla birebir aynı. Doğru sürüm geldiğinde ise denetim kırmızıya döner ve ekip yeni, doğru görüntüyü hatalı sayıp eski kaydı korumaya çalışabilir.

Alt başlık denetimi **kör** kaldı. Alt başlık iki sürümde de aynıdır ve sınırın içindedir; denetim doğru bir şey ölçer, ama bu kusurun alt başlıkla ilgisi yoktur. Doğrudan sınama ise ayıran çıktı: ekran görüntüsünün yeni arayüzü gösterdiği, referansa değil içeriğe bağlanmıştır.

#### Referans Nereden Gelir

Eşitlik denetimlerinin ortak zayıflığı budur. Denetim, referansın doğru olduğunu varsayar ve referansı doğrulayan bir şey yoktur. Referans alınırken derleme kusurluysa, eşitlik denetimi kusuru kilitler. Kayıt doğru derlemeden alınmışsa aynı denetim ayıran olur; ölçümün ikinci bölümünde bu görülecek.

Dört durum ayırt edilir. Referans doğru derlemeden alınmışsa denetim ayırır. Referans kusurlu derlemeden alınmışsa denetim ters çalışır. Referans hiç alınmamışsa denetim her iki sürümde de kalır ve gürültülüdür. Referans sürümler arasında güncellenmiyorsa, doğru bir yeni arayüz bile eski kayda karşı kalır; bu da bir tür ters davranıştır.

Ekipler bu zayıflığı genelde fark etmez, çünkü referansı alan kişi ile denetimi yazan kişi farklıdır. Birincisi görüntüyü yükleyip geçer, ikincisi görüntünün karşılaştırılacağı bir dosya bulur ve eşitliği yazar. Referansın nereden geldiğini kimse yazmaz.

**DG17.** Bu ders mağaza sayfasının arama görünürlüğünü, indirme oranını ya da kullanıcının sayfada ne okuduğunu ölçmedi. Ekran görüntüsünün ve alt başlığın kullanıcıyı ne kadar etkilediği bu dersin konusu değildir; ders yalnız sayfa ile uygulamanın uyuşup uyuşmadığına bakan denetimlerin ayrımını ölçer.

#### Ekran Görüntüsü Güncelleme Zamanı

Referans sorunu bir kez çözüldüğünde ikinci bir soru ortaya çıkar: arayüz her değiştiğinde referans da değişmelidir. Bu iki değişiklik çoğu zaman ayrı kişilerin elinde ve ayrı günlerde gerçekleşir. Arayüzü değiştiren kişi uygulamanın kodunu gönderir; sayfayı güncelleyecek kişi ise bunu duymadan eski görüntülerle yoluna devam eder.

Eşitlik denetimi bu ayrılığı yakalayabilir, ama yalnızca referans da güncellenmişse. Referans güncellenmeden yeni arayüz gönderildiğinde denetim yeni arayüzü kaldırır; ekip bu ters davranışı arayüzün bozulduğu biçiminde değil, referansı yenileme hatırlatması olarak okumalıdır. Bu okuma ayrı bir disiplin ister: kırmızı denetimin sebebi iki yerden biri olabilir, ya görüntü yanlıştır ya da referans eskidir. Hangisi olduğunu söylemek için denetimin yanında doğrudan bir sınama bulunmalıdır.

Doğrudan sınama bu yüzden iki yönlü bir kanıt sağlar. Eşitlik denetimi kırmızı, doğrudan sınama yeşilse sorun referanstadır. Eşitlik denetimi yeşil, doğrudan sınama kırmızıysa sorun, eşitlik denetiminin dayandığı kayıttadır. İkisi birlikte kırmızıysa sorun görüntünün kendisindedir. Tek bir denetim bu üç durumu ayıramaz.

#### Alt Başlığın Yeri

Alt başlık denetimi bu derste kör çıktı ve kör çıkması doğaldı. Yine de silinmesi gerekmez. Alt başlık sınırı gerçek bir kusura bakar: sınırı aşan bir alt başlık mağazanın karakter kuralını çiğner. Bu kusur bu derste yoktu, bu yüzden denetim ayıramadı.

Kör bir denetimi tutup tutmamak, onun kollamak için yazıldığı kusurun hâlâ olası olup olmadığına bağlıdır. Alt başlığı elle yazan bir ekipte olasıdır; başka bir dilden otomatik çevrilen bir alt başlıkta daha da olasıdır, çünkü çeviri çoğu zaman özgün metinden uzun çıkar. Kör denetim ancak kolladığı kusur artık hiç oluşamıyorsa gürültüye dönüşür.

Bu ayrım, kör ile gürültülünün farkını da açıklar. Kör denetim bugün işe yaramıyor, yarın yarayabilir. Gürültülü denetim ise bugün de yarın da aynı yanıtı verir ve hiçbir kusurla bağı kalmamıştır. Alt başlık denetimi bu ölçümde kördür; eski boyut gibi gürültülü değildir.

#### Geliştirici Paneli

Listeleme bilgileri uygulamanın deposunda değil, mağazanın geliştirici panelinde durur. Panelde her dil için bir ad, bir alt başlık, bir açıklama ve ekran görüntüsü kümeleri vardır. Ekipler çoğu zaman bu bilgilerin bir kopyasını depoda yapılandırma dosyası olarak tutar; böylece değişiklik bir inceleme geçer ve panele tek adımda taşınır.

```yaml
# listing.yaml — panele tasinan listeleme yapilandirmasi (KAYNAK, olculmez)
language: tr
subtitle: Siparisleri tek yerde izleyin
screenshots:
  - file: screens/home.png
  - file: screens/orders.png
reference:
  recorded_from: ""
```

Bu yapılandırma ölçülmedi; bir yaml örneğidir ve dersin çalıştırdığı hiçbir şey ona dayanmaz. Önemli olan `recorded_from` alanıdır. Referansın hangi derlemeden alındığını yazmak, eşitlik denetiminin zayıflığını görünür kılar. Alan boş bırakıldığında, referansın kaynağı kayıt altında değildir ve kimse onu denetleyemez.

**DG18.** Geliştirici paneli bu derste bir düzyazı ve bir yapılandırma örneğiyle anlatıldı. Panelin gerçek alanları, sınırları ve yükleme adımları ölçülmedi; paneldeki her şeyin sayfayı nasıl etkilediği de ölçülmedi.

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

Bu kez referans doğru derlemeden alınır. Aynı eşitlik denetimi ve aynı iki sürümle ayrım geri gelir. Yanına referansın hiç kaydedilmediği durum ve iki sürümde de aynı kalan alt başlık denetimi konur.

```swift
// main.swift — referansin kaynagi degisince ayni denetimler
let correctListing = listing(of: correctBuild)
let faultyListing = listing(of: faultyBuild)
let recordedFromCorrect = correctListing.screenshot

let fromCorrect = contrast("referans-dogru-derlemeden", correct: correctListing, faulty: faultyListing) { matchesReference($0, reference: recordedFromCorrect) }.describe()
let missing = contrast("referans-hic-kaydedilmedi", correct: correctListing, faulty: faultyListing) { matchesReference($0, reference: "") }.describe()
let withinLimit = contrast("alt-baslik-otuzda-icinde", correct: correctListing, faulty: faultyListing) { subtitleFits($0, limit: subtitleLimit) }.describe()
let seesNew = contrast("ekran-yeni-arayuz-dogrudan", correct: correctListing, faulty: faultyListing) { showsNewInterface($0) }.describe()

countSeparations([fromCorrect, missing, withinLimit, seesNew])
```

```text
referans-dogru-derlemeden | dogru=ekran=ekran-yeni alt-baslik=29/gecti kusurlu=ekran=ekran-eski alt-baslik=29/kaldi -> ayiran
referans-hic-kaydedilmedi | dogru=ekran=ekran-yeni alt-baslik=29/kaldi kusurlu=ekran=ekran-eski alt-baslik=29/kaldi -> gurultulu
alt-baslik-otuzda-icinde | dogru=ekran=ekran-yeni alt-baslik=29/gecti kusurlu=ekran=ekran-eski alt-baslik=29/gecti -> kor
ekran-yeni-arayuz-dogrudan | dogru=ekran=ekran-yeni alt-baslik=29/gecti kusurlu=ekran=ekran-eski alt-baslik=29/kaldi -> ayiran
ayiran: 2
kor: 1
gurultulu: 1
ters: 0
```

Referans doğru derlemeden alınınca eşitlik denetimi ayıran olur; denetimin kodu hiç değişmedi, yalnız kaydın kaynağı değişti. Referans yokken denetim iki sürümde de kalır ve gürültülüdür. Alt başlık denetimi yine kördür; sayfanın alt başlığı bu kusurun taşıyıcısı değildir. Doğrudan sınama referansa ihtiyaç duymadığı için iki ölçümde de ayırandır.

Sınırlar açıktır. Bu ders sayfanın arama sonuçlarında ne kadar göründüğünü ölçmedi. Alt başlığın karakter sınırı dersin kuralıdır, mağazanın kuralı değil. Geliştirici paneli ise yalnız bir yapılandırma örneği ve düzyazıyla anlatıldı. Ölçülen şey, listelemeyi denetleyen kodun kusuru ayırıp ayırmadığıdır; kullanıcının sayfada ne gördüğü ya da ne anladığı bu ölçümün dışındadır.

#### Referansı Doğrulamak

Referanslı bir eşitlik denetimi ancak referans doğrulanmışsa ayıran olur. Üç alışkanlık bunu sağlar. Birincisi, referansı kaydederken kaydın hangi derlemeden alındığını yazmaktır; yapılandırma örneğindeki `recorded_from` alanı bunun içindir. İkincisi, kaydı alan kişinin ekranı kendi gözüyle uygulamayla karşılaştırmasıdır; insan onayı yalnız o kaydı kapsar ve referans her yenilendiğinde yinelenir. Üçüncüsü, eşitlik denetiminin yanına referans gerektirmeyen bir doğrudan sınama koymaktır. Doğrudan sınama ile eşitlik denetimi birbirinden ayrışırsa, ikisinden biri yanlış referansa dayanıyor demektir.

Bu üçüncü alışkanlık dersin en az maliyetli çıkarımıdır. `showsNewInterface` ile `matchesReference` aynı iki sürüme uygulandığında farklı yanıt verdiler: biri ayırdı, diğeri tersine çalıştı. İki denetim arasındaki bu ayrılık, ölçüm olmadan da bir uyarıdır.

#### Kapanış Düşüncesi

Dersin başında listelemenin kodun denetimlerinden geçmediği söylendi. Ölçüm bunun bir bedeli olduğunu gösterdi: denetimi yazan kişi, denetimin neye karşı çalıştığını kendisi seçer ve bu seçim yanlış olabilir. Kod denetimlerinde doğru sürüm test girdisinde yazılıdır. Listelemede doğru sürüm bir görüntüdür ve görüntü bir kere kaydedildikten sonra kimse ona geri dönüp bakmaz.

#### Özet

- Mağaza sayfası uygulamanın kodundan ayrı durur; bu yüzden kodun denetimleri sayfayı kapsamaz.
- Kayıtlı referansla eşitlik denetimi, referans kusurlu derlemeden alındığında doğru sürümü kaldırıp kusurlu sürümü geçirir; yani ters çalışır.
- Alt başlığın sınırı aşmadığına bakan denetim iki sürümde de aynı yanıtı verir; bu kusurda kördür.
- Referans doğru derlemeden alınınca aynı eşitlik denetimi ayıran olur; referans hiç yoksa gürültülü olur.
- Referansın kaynağını yazmak ve eşitlik denetiminin yanına doğrudan bir sınama koymak ayrımı geri getirir.
- Bu dersin ayrımı: **ters**.

#### Sonraki Adım

Sayfa ve gönderim hazır olduğunda geriye bir soru kalır: yayına çıkan sürüm kullanıcıda çöktüğünde bunu nasıl öğrenirsiniz? Sonraki derste **Çökme Raporlama ve Analitik** ele alınacak; yalnız bir dil yapılandırmasında çöken kusurun toplam sayaçla ve yapılandırma başına sayaçla ayrılıp ayrılmadığı ölçülecek.

### Çökme Raporlama ve Analitik

Source: https://academia.sh/tr/kurslar/ios-uretim/cokme-raporlama-ve-analitik

Yalnız bir dil yapılandırmasında çöken bir kusura toplam çökmesiz oturum denetiminin kör kaldığının, yapılandırma başına denetimin ise ayırdığının ve kursun on dokuz dersinin kapanış tablosunun ölçümü.

Yayına çıkan bir sürüm kullanıcıda çöktüğünde bunu öğrenmenin yolu, çökme raporudur. Rapor, çöken oturumun izini ekibe geri taşır. Analitik ise çökmeyen oturumlarda kullanıcının ne yaptığını sayar. İkisi de bir sayaçtır ve sayaçlar, bir kusurun nerede yaşadığını bilmeden okunursa yanıltır.

Kursun ölçü sorusu hep aynıdır: aynı denetim doğru sürüme ve kusurlu sürüme uygulanınca iki yanıt farklı mı. Doğru sürümün geçip kusurlunun kaldığı denetim **ayıran**, ikisinin de geçtiği **kör**, ikisinin de kaldığı **gürültülü**, doğrunun kalıp kusurlunun geçtiği **ters**tir. Çıktıda `ayiran`, `kor`, `gurultulu` ve `ters` olarak yazılırlar. Çekirdek önceki derslerdeki gibi birebir gelir.

```swift
// 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 dersin kusuru yalnız bir dil yapılandırmasındadır: tarih biçimi çözülemeyen kullanıcılarda uygulama çöker, öteki yapılandırmalarda çalışır. Çökmenin kendisi modelde gerçekleşmez; model her yapılandırma için oturum ve çökme sayısını tutar ve sayıları bir rapor gibi okur.

#### Raporun Modeli

`ConfigTally` bir yapılandırmanın oturum ve çökme sayısıdır. `Telemetry` üç yapılandırmayı ve tek bir analitik olay adını taşır; kendini `oturum`, `cokme` ve `olay` olarak özetler. `overallPasses` bütün oturumlar üzerinden çökmesiz oranın eşikte kalıp kalmadığına, `configPasses` tek bir yapılandırma üzerinden aynı şeye bakar. `dashboardCount` ise panonun belirli bir olay adıyla saydığı kaydı döndürür.

```swift
// Crash.swift — yapilandirma basina oturum ve cokme sayilari, tek bir olay adi
struct ConfigTally {
    let config: String
    let sessions: Int
    let crashes: Int
}

struct Telemetry: CustomStringConvertible {
    let tallies: [ConfigTally]
    let eventName: String

    var sessions: Int { tallies.reduce(0) { $0 + $1.sessions } }
    var crashes: Int { tallies.reduce(0) { $0 + $1.crashes } }

    var description: String {
        "oturum=\(sessions) cokme=\(crashes) olay=\(eventName)"
    }
}

func crashFree(sessions: Int, crashes: Int, threshold: Int) -> Bool {
    (sessions - crashes) * 100 >= sessions * threshold
}

func overallPasses(_ telemetry: Telemetry, threshold: Int) -> Bool {
    crashFree(sessions: telemetry.sessions, crashes: telemetry.crashes, threshold: threshold)
}

func configPasses(_ telemetry: Telemetry, config: String, threshold: Int) -> Bool {
    telemetry.tallies.filter { $0.config == config }.allSatisfy {
        crashFree(sessions: $0.sessions, crashes: $0.crashes, threshold: threshold)
    }
}

func dashboardCount(_ telemetry: Telemetry, name: String) -> Int {
    telemetry.eventName == name ? telemetry.sessions : 0
}

let crashFreeThreshold = 90
let correctTelemetry = Telemetry(
    tallies: [ConfigTally(config: "en", sessions: 40, crashes: 0),
              ConfigTally(config: "tr", sessions: 30, crashes: 0),
              ConfigTally(config: "ja", sessions: 10, crashes: 0)],
    eventName: "siparis-tamamlandi")
let faultyTelemetry = Telemetry(
    tallies: [ConfigTally(config: "en", sessions: 40, crashes: 0),
              ConfigTally(config: "tr", sessions: 30, crashes: 0),
              ConfigTally(config: "ja", sessions: 10, crashes: 4)],
    eventName: "siparis-tamamlandi")
```

**DG21.** Oturum ve çökme sayıları modelin kendi sayılarıdır; gerçek bir uygulamanın çökme oranını göstermez. Yüzde doksan eşiği de dersin kuralıdır, platformun ya da bir servisin eşiği değildir.

#### Üç Denetim

Birinci denetim bütün oturumlara bakar. İkincisi yalnız `ja` yapılandırmasının sayacına bakar. Üçüncüsü, olayın adı geçen sürümde `siparis-verildi` iken `siparis-tamamlandi` olarak değiştirildiği için, panonun hâlâ eski adla saydığı kaydı arar. Olay iki sürümde de yeni adla gönderilir.

```swift
// main.swift — toplam sayac, yapilandirma sayaci, eski olay adi
let overall = contrast("toplam-cokmesiz-oturum-esikte", correct: correctTelemetry, faulty: faultyTelemetry) { overallPasses($0, threshold: crashFreeThreshold) }.describe()
let japanese = contrast("ja-yapilandirmasi-cokmesiz-esikte", correct: correctTelemetry, faulty: faultyTelemetry) { configPasses($0, config: "ja", threshold: crashFreeThreshold) }.describe()
let oldName = contrast("panoda-eski-olay-adi-var", correct: correctTelemetry, faulty: faultyTelemetry) { dashboardCount($0, name: "siparis-verildi") > 0 }.describe()

countSeparations([overall, japanese, oldName])
```

```text
toplam-cokmesiz-oturum-esikte | dogru=oturum=80 cokme=0 olay=siparis-tamamlandi/gecti kusurlu=oturum=80 cokme=4 olay=siparis-tamamlandi/gecti -> kor
ja-yapilandirmasi-cokmesiz-esikte | dogru=oturum=80 cokme=0 olay=siparis-tamamlandi/gecti kusurlu=oturum=80 cokme=4 olay=siparis-tamamlandi/kaldi -> ayiran
panoda-eski-olay-adi-var | dogru=oturum=80 cokme=0 olay=siparis-tamamlandi/kaldi kusurlu=oturum=80 cokme=4 olay=siparis-tamamlandi/kaldi -> gurultulu
ayiran: 1
kor: 1
gurultulu: 1
ters: 0
```

Toplam denetim **kör** çıktı. Kusurlu sürümde dört oturum çöker, ama seksen oturumun içinde bu dört oturum eşiğin altına düşmeyi gerektirmez. Denetim doğru bir şeye bakar, yalnız kusurun yaşadığı yere bakmaz. Yapılandırma başına denetim aynı veriden kusuru ayırır, çünkü çöken dört oturum küçük bir yapılandırmada yoğunlaşmıştır. Bir ortalamanın içinde küçük bir kullanıcı grubunun çökmesi kaybolur.

Pano denetimi **gürültülü** çıktı. Olay yeniden adlandırıldığı için eski adla sayan pano iki sürümde de sıfır görür ve kalır. Kırmızı pano çökmeyi göstermez; panonun yeni adı bilmediğini gösterir.

#### Raporlama Yapılandırması

Çökme raporlama servisi ve sembolleştirme bu derste ölçülmez. İkisi de KAYNAK olarak kalır: yapılandırma, raporun açık olduğunu ve sembollerin yüklendiğini söyler.

```yaml
# telemetry.yaml — raporlama yapilandirmasi (KAYNAK, olculmez)
crash_reporting:
  enabled: true
  upload_symbols: true
analytics:
  events:
    - siparis-tamamlandi
```

```zsh
# sembol-yukleme adimi — KAYNAK
# 1. arsivden sembol dosyalari cikarilir
# 2. sembol dosyalari raporlama servisine yuklenir
# 3. servis gelen cokme izini bu dosyalarla okunur hale getirir
```

Sembol yüklenmeden gelen bir çökme izi adres satırlarından ibarettir ve hangi işlevde çöküldüğünü söylemez. Bu yüzden sembol yükleme adımı, hattaki hiçbir adımı kırmızıya çevirmeyen ama unutulursa raporu işe yaramaz kılan bir adımdır.

**DG22.** Çökme raporunun ekibe hangi gecikmeyle ve hangi oranda ulaştığı ölçülmedi. Raporlama servisi, sembolleştirme ve panonun gerçek davranışı da ölçülmedi.

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

Toplam denetimin körlüğü eşiğe bağlıdır. Aşağıda aynı iki sürüm eşiğin değişen değerleriyle, bir de çökmeyen `tr` yapılandırmasıyla ve yeni olay adıyla denetlenir.

```swift
// main.swift — esik ve yapilandirma degisince
let strictOverall = contrast("toplam-esik-96", correct: correctTelemetry, faulty: faultyTelemetry) { overallPasses($0, threshold: 96) }.describe()
let turkish = contrast("tr-yapilandirmasi", correct: correctTelemetry, faulty: faultyTelemetry) { configPasses($0, config: "tr", threshold: crashFreeThreshold) }.describe()
let newName = contrast("panoda-yeni-olay-adi-var", correct: correctTelemetry, faulty: faultyTelemetry) { dashboardCount($0, name: "siparis-tamamlandi") > 0 }.describe()
let strictJapanese = contrast("ja-yapilandirmasi-esik-100", correct: correctTelemetry, faulty: faultyTelemetry) { configPasses($0, config: "ja", threshold: 100) }.describe()

countSeparations([strictOverall, turkish, newName, strictJapanese])
```

```text
toplam-esik-96 | dogru=oturum=80 cokme=0 olay=siparis-tamamlandi/gecti kusurlu=oturum=80 cokme=4 olay=siparis-tamamlandi/kaldi -> ayiran
tr-yapilandirmasi | dogru=oturum=80 cokme=0 olay=siparis-tamamlandi/gecti kusurlu=oturum=80 cokme=4 olay=siparis-tamamlandi/gecti -> kor
panoda-yeni-olay-adi-var | dogru=oturum=80 cokme=0 olay=siparis-tamamlandi/gecti kusurlu=oturum=80 cokme=4 olay=siparis-tamamlandi/gecti -> kor
ja-yapilandirmasi-esik-100 | dogru=oturum=80 cokme=0 olay=siparis-tamamlandi/gecti kusurlu=oturum=80 cokme=4 olay=siparis-tamamlandi/kaldi -> ayiran
ayiran: 2
kor: 2
gurultulu: 0
ters: 0
```

Eşik doksandan doksan altıya çıkınca toplam denetim ayıran olur. Çökmeyen `tr` yapılandırmasına bakan denetim yine kördür, çünkü kusur orada yaşamaz. Yeni adla sayan pano iki sürümde de geçer ve kördür: panonun doğru saydığı olay çökmeyi bilmez.

Sınırlar açıktır. Ayrım bir mimarinin, bir test kümesinin ya da bir yayın sürecinin seçim ölçütünün tamamı değildir. Maliyet, ekibin büyüklüğü ve bir yapının zaman içinde sürdürülebilirliği bu kursta ölçülmedi ve bir sayıya dökülmedi; bunlar düzyazıyla tartışılır.

**DG23.** Eşikler dersin kuralıdır. Hangi eşiğin doğru olduğu, ekibin kullanıcıya ve çökmeye ne kadar tahammül ettiğine bağlıdır ve bu ders onu söylemez.

#### Kurs Kapanışı

Kursun on dokuz dersi, her biri kendi kusuruyla, aşağıdaki tabloda yan yana durur. Ayrım sütunu her dersin kendi `## Özet` bölümündeki karardan alındı.

| Ders | Kusur | Ayrım | Sınırlayıcı ölçüm |
|---|---|---|---|
| Mimari Kalıplar | indirim iki kez uygulanır | dördü (2/2/1/1) | yüzde değişince aynı denetim kör, sonra ayıran |
| Koordinatör Kalıbı | ödemeden sonra yanlış ekran | kör | kusur ara adıma kayınca yalnız son ekran kör |
| Tek Yönlü Veri Akışı | indirgeyici yanlış durum üretir | ayıran | kayıtlı örnek kusurludan alınınca ters |
| Bağımlılık Enjeksiyonu | boş yanıtta liste silinir | gürültülü | güdük hep dolu dönünce kör |
| Modülerleştirme | özellik modülü başka özelliğe bağlanır | kör | yeniden derleme bütçesi: biri ayıran, biri kör |
| Birim Testleri | eşikte ücret hâlâ alınır | ayıran | beklenen değer kusurludan kopyalanınca ters |
| Arayüz Testleri | düğme sipariş göndermez | gürültülü | ekran oturmadan bakınca gürültülü |
| Test Planları ve Kapsam | indirim yanlış hesaplanır | kör | tek dile yazılan beklenti gürültülü |
| Profil Çıkarma Araçları | hücre gereğinden çok iş yapar | ayıran | bütçeye göre gürültülü, ayıran, kör |
| Bellek Sızıntısı Tespiti | kapanış nesneyi güçlü tutar | ayıran | sayaç ekranı sayınca kör |
| Başlangıç ve Kaydırma Başarımı | açılışta ve kaydırmada fazla iş | ters | yenilenen temel değer kör |
| Kod Kalitesi Araçları | zorla açma boş girdide çöker | kör | sıkı kural gürültülü |
| İmzalama ve Sağlama Profilleri | paket bildirim yetkisini taşımaz | ters | aynı denetim üç girdide üç ayrım |
| Sürekli Tümleştirme | sepet toplamı son kalemi atlar | gürültülü | adım sırası değişince ayrım değişir |
| Yayın Otomasyonu | derleme numarası artmaz | ayıran | son numara kaynağına göre gürültülü ya da ters |
| Beta Dağıtımı | bayrak kitle dışına açık | kör | yeni davetlide ters |
| Mağaza İnceleme Süreci | izin açıklaması boş | gürültülü | boşluk açıklamasında uzunluk maddesi kör |
| Mağaza Listelemesi | ekran görüntüsü eski arayüzü gösterir | ters | doğru referansla ayıran |
| Çökme Raporlama ve Analitik | yalnız bir dilde çöker | kör | eşik değişince ayıran |

Tablonun ayrım sütunu açılış dersini dördü olarak, öteki on sekiz dersi tek ayrımla yazar. Aşağıdaki ölçüm bu on sekiz kararı sayar; son karar bu dersin kendi toplam denetiminden gelir. Altına çekirdeğin altı denetimi ayrıca konur.

```swift
// main.swift — acilis disindaki on sekiz dersin dagilimi
let ownLesson = contrast("kapanis", correct: correctTelemetry, faulty: faultyTelemetry) { overallPasses($0, threshold: crashFreeThreshold) }.separation
let remaining = ["kor", "ayiran", "gurultulu", "kor",
                 "ayiran", "gurultulu", "kor", "ayiran", "ayiran", "ters", "kor",
                 "ters", "gurultulu", "ayiran", "kor", "gurultulu", "ters",
                 ownLesson]
print("acilis-disi-on-sekiz")
countSeparations(remaining)
print("cekirdek-alti-denetim")
countSeparations(["ayiran", "ayiran", "kor", "kor", "gurultulu", "ters"])
```

```text
acilis-disi-on-sekiz
ayiran: 5
kor: 6
gurultulu: 4
ters: 3
cekirdek-alti-denetim
ayiran: 2
kor: 2
gurultulu: 1
ters: 1
```

Açılış dışındaki on sekiz dersin dağılımı beş ayıran, altı kör, dört gürültülü ve üç terstir. Çekirdeğin 2/2/1/1 dağılımı bundan farklı bir şeydir: çekirdek ilk dersteki altı denetimi sayar, bu dağılım ise on sekiz dersin ayrımını. Tablodan okunacak şey dağılımın kendisi değil, aynı denetim türünün farklı kusurlarda dört ayrımın hepsine düşebildiğidir. Kör ve ters birlikte dokuz derste çıkar; ayıran beşte kalır.

M30 burada kapanır: Swift dili, platform ve araçlar, bildirimsel arayüz, buyurgan arayüz ve köprüleme, veri ve ağ, mimari ve yayın olmak üzere altı kurs tamamlandı.

#### Özet

- Çökme sayacı bütün oturumların toplamında okunursa, tek bir dil yapılandırmasında çöken kusur toplamda kaybolur.
- Çökme sayacını yapılandırma başına ayıran denetim aynı veriden kusuru ayırır.
- Yeniden adlandırılan olayı eski adla sayan pano iki sürümde de sıfır görür ve gürültülü olur.
- Toplam denetimin eşiği sıkılaştırılınca aynı denetim ayıran olur; kör olmak denetimin değil, denetimle eşiğin ortak özelliğidir.
- Kursun on dokuz dersinde ayrım dört kategoriye dağılır; hiçbir denetim türü yalnız tek kategoride kalmaz.
- Bu dersin ayrımı: **kör**.

#### Sonraki Adım

Bu, kursun son dersidir. Yeni bir denetim yazarken sorulacak soru değişmez: denetimi doğru sürüme ve kusurlu sürüme uygulayın, iki yanıtı yan yana okuyun.

