Ders 14 / 15
Eski Kodun Kademeli Dönüşümü
Eski bir ekranı ekran ekran yeni çerçeveye taşırken taşınmayan alanın yeni ekranda eksik kaldığının, modelden kalkan alanın eski ekranda artık kaldığının ve kuralın ekran düzeyinde değil alan başına çalıştığının ölçümü.
Yayın Güncelleme
Bu derste
- Eski kodu tek seferde değil ekran ekran dönüştürmenin neden iki çerçeveyi bir arada tuttuğunu açıklar
- Yeni ekrana taşınmayan alanın eksik ölçüldüğünü gösterir
- Modelden kalkıp eski ekranda kalan alanın artık ölçüldüğünü gösterir
- Aynı modeli iki ekrana karşı denetleyip kuralın ekran düzeyinde tek yanıt vermediğini doğrular
İçindekiler
Önceki derste (Bildirimsel ve Buyurgan Arayüz Bir Arada) iki çerçevenin dikiş yerine baktık: değer sınırdan geçerken atama ya da güncelleme çağrısı yazılmazsa alan bayat kalıyordu. Gerçek bir uygulamada o dikişin yanında ikinci bir soru vardır. Eski kod yüzlerce ekran içerir ve hiçbir takım hepsini bir gecede yeniden yazmaz. Dönüşüm ekran ekran ilerler: her adımda bir ekran bildirimsel çerçeveye taşınır, geri kalanı eski hâlinde çalışmaya devam eder. Uygulama dönüşümün ortasında da yayımlanabilir olmak zorundadır.
Bu, iki çerçevenin uzun süre yan yana yaşaması demektir. Kademeli yolun gerekçesi basittir: her adım küçük olduğu için hata bulunduğunda kaynağı bellidir. Bir ekran taşındığında bozulabilecek şey yalnız o ekrandır. Tek seferlik yeniden yazımda aynı hata uygulamanın herhangi bir yerinden çıkabilir ve ne zaman girdiği bilinmez. Taşıma sırası genellikle yaprak ekranlardan köke doğrudur: başka ekrana açılmayan, az alanlı bir ayrıntı ekranı ilk aday olur, çünkü hem küçüktür hem de hata verirse geri alınması kolaydır. Dikişte ise önceki dersin ikisi de geçerlidir: yeni ekran eski bir gezinme yığıtına barındırma denetleyicisiyle itilir ya da eski bir parça yeni ekrana sarmalamayla girer.
Bu derste ölçeceğimiz şey, bir ekran taşınırken eski ekranın yaptığı işlerden hangisinin yeni ekrana geçtiğidir. Ölçü ekseni değişmedi: uyum. Alan başına dört sonuçtan biri çıkar: uyumlu, bayat, eksik, artik. Çekirdek dosya önceki derslerle aynıdır.
// Sync.swift — ekranda gosterilen her alani modelin o anki degeriyle karsilastirir
final class Screen {
let name: String
private(set) var shown: [String: String] = [:]
init(_ name: String) { self.name = name }
func set(_ key: String, _ value: String) { shown[key] = value }
func clear(_ key: String) { shown[key] = nil }
}
func verdict(model: String?, shown: String?) -> String {
switch (model, shown) {
case (nil, nil): return "olcum-disi"
case (nil, _?): return "artik"
case (_?, nil): return "eksik"
case let (wanted?, displayed?): return wanted == displayed ? "uyumlu" : "bayat"
}
}
func check(_ label: String, model: [String: String], screen: Screen, keys: [String]) -> [String] {
keys.map { key in
let result = verdict(model: model[key], shown: screen.shown[key])
print("\(label) | \(screen.name).\(key): model=\(model[key] ?? "-") ekran=\(screen.shown[key] ?? "-") -> \(result)")
return result
}
}
func countVerdicts(_ verdicts: [String]) {
for name in ["uyumlu", "bayat", "eksik", "artik"] {
print("\(name): \(verdicts.filter { $0 == name }.count)")
}
}
Eski ekranın yaptığı işler çoğu zaman yazılı bir yerde durmaz. Eski ekranı yazan kişi ayrılmıştır, belge yoktur, ekran yıllar içinde küçük eklerle büyümüştür. Yeni ekranı yazan kişi eski ekranı okuyup yeniden kurar ve okurken bir alanı gözden kaçırabilir. Kural bu gözden kaçmayı kimin yaptığına bakmadan, alanın modelde olup ekranda olmadığını söyleyerek yakalar. Bu yüzden dönüşüm adımının bitiş ölçütü “ekran açılıyor” değildir; “modelin her alanı ekranda karşılık buluyor” olmalıdır. İlk ölçüt gözle, ikincisi alan alan karşılaştırarak sağlanır.
Taşınmayan Alan
Elimizde bir görev ekranı var. Eski sürüm buyurgan arayüz çerçevesinde yazılmış ve üç değer gösteriyor: başlık, açık görev sayısı ve teslim etiketi. Her değer bir bağlantı (outlet) üzerinden etikete yazılır; yazan kod ekran görünmeden hemen önce çalışan bir yöntemdir. Ayrıca ekranda bir de eski bir rozet var.
import UIKit
final class LegacyTaskController: UIViewController {
@IBOutlet private var titleLabel: UILabel!
@IBOutlet private var countLabel: UILabel!
@IBOutlet private var dueLabel: UILabel!
@IBOutlet private var badgeLabel: UILabel!
func bind(_ task: TaskModel) {
titleLabel.text = task.title
countLabel.text = String(task.openCount)
dueLabel.text = task.dueLabel
badgeLabel.text = "yeni"
}
}
Dönüşümün ilk adımında ekran bildirimsel çerçevede yeniden yazılır. Yeni görünüm başlığı ve sayıyı alır ve gösterir. Teslim etiketi yeniden yazım sırasında atlanmıştır.
import SwiftUI
struct TaskScreenView: View {
let task: TaskModel
var body: some View {
VStack {
Text(task.title)
Text(String(task.openCount))
}
}
}
İki parçayı karşılaştırınca fark görülebilir, ama çoğu gerçek ekranda alan sayısı on ikidir ve fark gözden kaçar. Model iki ekranı da saf Swift ile kurar: kim neyi ekrana yazıyor. Ortak tipler yardımcı dosyada durur; KAYNAK parçalardaki TaskModel, LegacyTaskController, bind ve body adları burada da aynıdır. Etiket adlarının sonundaki Label atılırsa anahtarlar çıkar: titleLabel için title, dueLabel için dueLabel.
// Migration.swift — modelin degeri ve eski ekranin kurulumu
struct TaskModel {
var title: String
var openCount: Int
var dueLabel: String
var values: [String: String] {
["title": title, "openCount": String(openCount), "dueLabel": dueLabel]
}
}
final class LegacyTaskController {
let screen = Screen("LegacyTaskController")
func bind(_ task: TaskModel) {
screen.set("title", task.title)
screen.set("openCount", String(task.openCount))
screen.set("dueLabel", task.dueLabel)
screen.set("badge", "yeni")
}
}
Model, kodun ekrana ne yazdığını kurar; çerçevenin kendisi değildir. Birinci olay: yeni ekran, dueLabel yazılmadan kurulur ve model üç alanı da ister.
// main.swift — yeni ekran dueLabel alanini tasimaz
struct TaskScreenView {
let task: TaskModel
func body(on screen: Screen) {
screen.set("title", task.title)
screen.set("openCount", String(task.openCount))
}
}
let task = TaskModel(title: "Gorevler", openCount: 5, dueLabel: "yarin")
let migrated = Screen("TaskScreenView")
TaskScreenView(task: task).body(on: migrated)
let dropped = check("yeni-ekran-eksik", model: task.values, screen: migrated,
keys: ["title", "openCount", "dueLabel"])
countVerdicts(dropped)
yeni-ekran-eksik | TaskScreenView.title: model=Gorevler ekran=Gorevler -> uyumlu yeni-ekran-eksik | TaskScreenView.openCount: model=5 ekran=5 -> uyumlu yeni-ekran-eksik | TaskScreenView.dueLabel: model=yarin ekran=- -> eksik uyumlu: 2 bayat: 0 eksik: 1 artik: 0
İki alan taşındı ve uyumlu. dueLabel modelde yarin, yeni ekranda hiç yok: eksik. Bu bayattan farklı bir durumdur. Bayatta ekranda eski bir değer vardı; burada ekran o alanı hiç yazmamıştır. Bir önceki dersteki hatayla kök nedenleri aynı kökten, yazılmamış bir satırdan gelir, ama sonuç ayrı adlandırılır, çünkü ayrı biçimde fark edilir. Bayat bir rakam yanlış okunabilir, eksik bir etiket ise ekranda boşluk olarak görünür.
KP1. Model, yeni ekranın body içinde yazdığı her değeri ekrana yazılmış kabul eder. Bildirimsel çerçevenin bodyyi ne zaman çağırdığı ve değişiklikte neyi yeniden hesapladığı ölçülmez; ölçülen şey, bodynin hangi alanları verdiğidir.
Eksik alanın düzeltilmesi kısadır: yeni görünüme Text(task.dueLabel) eklemek. Ölçümde bunu body içine bir satır yazarak yapıyoruz.
// main.swift — dueLabel yeni ekrana tasindi
struct TaskScreenView {
let task: TaskModel
func body(on screen: Screen) {
screen.set("title", task.title)
screen.set("openCount", String(task.openCount))
screen.set("dueLabel", task.dueLabel)
}
}
let task = TaskModel(title: "Gorevler", openCount: 5, dueLabel: "yarin")
let migrated = Screen("TaskScreenView")
TaskScreenView(task: task).body(on: migrated)
let carried = check("yeni-ekran-tasindi", model: task.values, screen: migrated,
keys: ["title", "openCount", "dueLabel"])
countVerdicts(carried)
yeni-ekran-tasindi | TaskScreenView.title: model=Gorevler ekran=Gorevler -> uyumlu yeni-ekran-tasindi | TaskScreenView.openCount: model=5 ekran=5 -> uyumlu yeni-ekran-tasindi | TaskScreenView.dueLabel: model=yarin ekran=yarin -> uyumlu uyumlu: 3 bayat: 0 eksik: 0 artik: 0
Üç alanın üçü de uyumlu. Dönüşüm bu alan için tamamlandı. Dikkat edilecek bir nokta daha var: bu düzeltme bodyye bir satır ekledi, ama başka bir şey değiştirmedi. Kademeli dönüşümün işe yarar tarafı budur. Hatanın yeri bellidir, düzeltmesi küçüktür ve düzeltmeden sonra ölçüm hatanın gittiğini aynı sayılarla gösterir.
Modelden Kalkan Alan
Dönüşümün ters yüzü, eski ekranın hâlâ yazdığı ama artık hiçbir şeyin karşılığı olmayan alandır. Rozeti hatırlayalım: eski ekranda badgeLabel.text = "yeni" satırı var ve bu satır koşulsuz çalışıyor. Diyelim ki bir ürün kararıyla rozet kaldırıldı. Model artık badge alanı taşımaz, ama eski ekran onu yazmaya devam eder.
// main.swift — model badge alanini tasimaz, eski ekran yazmaya devam eder
let task = TaskModel(title: "Gorevler", openCount: 5, dueLabel: "yarin")
let legacy = LegacyTaskController()
legacy.bind(task)
let leftover = check("eski-ekran-artik", model: task.values, screen: legacy.screen,
keys: ["title", "badge"])
countVerdicts(leftover)
eski-ekran-artik | LegacyTaskController.title: model=Gorevler ekran=Gorevler -> uyumlu eski-ekran-artik | LegacyTaskController.badge: model=- ekran=yeni -> artik uyumlu: 1 bayat: 0 eksik: 0 artik: 1
title uyumlu. badge için model değeri yok (-), ekranda yeni yazıyor: artik. Kural yine nedeni görmez; alanın kaldırıldığını mı, yoksa eski ekranın yanlışlıkla yazdığını mı ayıramaz. Yalnız ekranda modelin bilmediği bir değer olduğunu söyler. Eski kodda bu türden satırlar çoktur: kaldırılmış bir özelliğin kalıntısı, bir hata ayıklama için konmuş sabit bir metin, artık kimsenin okumadığı bir etiket ve yeni ekrana taşınırken en kolay yok olanlar da bunlardır; ama bir alan taşınmadığında da, eksik ile artık aynı soruya iki ayrı cevap verir: modelde olan ekranda yok, ekranda olan modelde yok.
Artığın iki çözümü vardır ve hangisinin doğru olduğunu ölçüm söylemez. Rozet gerçekten kaldırıldıysa eski ekrandaki satır silinir ve alan sonuçtan düşer. Rozet kaldırılmadıysa, model ona yeniden bir değer verir ve sonuç uyumlu olur. İlk çözüm eski ekranı değiştirir, ikincisi modeli. Aynı ayrım eksik için de geçerlidir: ya yeni ekrana alan eklenir ya da model o alanı bırakır. Dönüşümde kimin hangisini seçeceği ürünle ilgili bir karardır; kural yalnız iki tarafın ayrıştığını gösterir ve karar verilene kadar ayrışmanın görünür kalmasını sağlar.
Dönüşüm planına bu yüzden bir envanter adımı konur. Taşınacak ekranın modelden okuduğu her alan listelenir, eski ekranın kendi başına yazdığı sabitler ayrıca işaretlenir. Envanter çıkmadan taşınan bir ekran, ölçümde ilk iki sonucu, eksik ve artığı, birlikte verir. Envanter çıktıktan sonra ise taşınan her ekran için kontrol tek bir soruya iner: bu ekranın alanları, eski ekranın alanlarıyla ve modelin alanlarıyla aynı mı?
Sınırlayıcı Ölçüm
Dönüşümün ortasında aynı model iki ekranı birden besler: eski ekran henüz yerinde, yeni ekran yanında. Soru, “bu ekran modelle uyumlu mu” sorusunun ekran düzeyinde cevaplanıp cevaplanamayacağıdır.
// main.swift — ayni model iki ekrana karsi denetlenir
struct TaskScreenView {
let task: TaskModel
func body(on screen: Screen) {
screen.set("title", task.title)
screen.set("openCount", String(task.openCount))
}
}
let task = TaskModel(title: "Gorevler", openCount: 5, dueLabel: "yarin")
let keys = ["title", "openCount", "dueLabel"]
let legacy = LegacyTaskController()
legacy.bind(task)
let migrated = Screen("TaskScreenView")
TaskScreenView(task: task).body(on: migrated)
let oldScreen = check("iki-ekran-eski", model: task.values, screen: legacy.screen, keys: keys)
let newScreen = check("iki-ekran-yeni", model: task.values, screen: migrated, keys: keys)
countVerdicts(oldScreen + newScreen)
iki-ekran-eski | LegacyTaskController.title: model=Gorevler ekran=Gorevler -> uyumlu iki-ekran-eski | LegacyTaskController.openCount: model=5 ekran=5 -> uyumlu iki-ekran-eski | LegacyTaskController.dueLabel: model=yarin ekran=yarin -> uyumlu iki-ekran-yeni | TaskScreenView.title: model=Gorevler ekran=Gorevler -> uyumlu iki-ekran-yeni | TaskScreenView.openCount: model=5 ekran=5 -> uyumlu iki-ekran-yeni | TaskScreenView.dueLabel: model=yarin ekran=- -> eksik uyumlu: 5 bayat: 0 eksik: 1 artik: 0
Eski ekran üç alanda da uyumlu, yeni ekran iki alanda uyumlu, birinde eksik. Toplamda beş uyumlu, bir eksik. Bu toplamdan “uygulamanın yüzde seksen üçü uyumlu” gibi bir cümle kurmak mümkün olurdu, ama bu cümle ekranın hangisinin tutarlı olduğunu söylemez. Kullanıcı yalnız yeni ekranı görüyorsa onun gözünde ekran bozuktur; yalnız eskisini görüyorsa sorun yoktur. Ekran düzeyinde tek bir yanıt yoktur, kural alan başınadır. Aynı sebeple bir ekranı “uyumlu” ilan etmek, yazılan her alanın modelle eşit olduğunu söyler; kuralın bilmediği alan modelde olsa da ekranda eksik kalırsa yalnız sayıda görünür.
Bu ölçüm modelin çerçeveden ayrıldığı yeri de adlandırır. KP2. Model, eski ekranın bind içinde yazdığı her değeri ekrana yazılmış kabul eder; etiket metninin yerleşimi, yazı tipi ya da kırpılması ölçülmez. Ölçülen şey, bind çağrısının hangi anahtarlara değer yazdığıdır.
Gerçek dönüşümde iki ekran aynı kullanıcıya aynı anda gösterilmez; biri yığıttan çıkar, öteki girer. Modelde ikisi birden vardır, çünkü kural bir ekranı değil bir alanı sorgular ve alan başına sonuç veren bir kural için ikisini yan yana koymak yeterlidir.
Ölçümün sınırı şudur: kural iki ekranı birbiriyle değil, her birini ayrı ayrı modelle karşılaştırır. Eski ekran ile yeni ekran arasında doğrudan bir karşılaştırma yoktur; ikisi de modele göre ölçülür. Modelin kendisi yanlışsa, örneğin bir alan modelde hiç tanımlanmamışsa, iki ekran da aynı eksiği taşıyabilir ve kural ikisini de uyumlu bulur. Kuralın kapsamı yalnız modelin bilinen alanlarıdır.
Dönüşüm bittiğinde son bir adım kalır: eski ekranın silinmesi. Silinen ekranla birlikte onun yazdığı artık alanlar da gider. Yeni ekranın hâlâ eksik bıraktığı bir alan ise bu noktadan sonra karşılaştırılacak bir eski ekran bulamaz ve eksiklik ancak model üzerinden fark edilir. Kademeli dönüşümün sağladığı güvence, eski ekran yerindeyken her alanın iki ekranda da aynı sonucu vermesidir; bu güvence eski ekran silindikten sonra ölçüm çalıştırılmadıkça kaybolur.
Özet
- Eski kod ekran ekran dönüşür; dönüşümün ortasında iki çerçeve aynı uygulamada yaşar ve aynı modeli besler.
- Yeni ekrana taşınmayan
dueLabelalanı modeldeyariniken ekranda hiç yoktur: eksik ölçülür (2 uyumlu, 1 eksik). - Alan taşındığında üç alan da uyumludur (3 uyumlu).
- Modelden kalkan
badgealanını eski ekran yazmaya devam ettiğinde sonuç artıktır (1 uyumlu, 1 artık). - Aynı model iki ekrana karşı denetlendiğinde eski ekran üç alanda, yeni ekran iki alanda uyumludur; toplam 5 uyumlu, 1 eksik. Ekran düzeyinde tek yanıt yoktur, kural alan başınadır.
- Bu dersin uyumu: eksik.
Sonraki Adım
Bir sonraki ders konunun ve kursun kapanışıdır: Çerçeve Seçimi. İki çerçeveyi aynı kuralla ölçmüş olduk; hangisini ne zaman seçeceğiz sorusuna oradan bakacağız.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.