Aşağıdaki satırlar, işyerindeki tüm ekibimizin birkaç senelik eforunun en lezzetli meyvelerinden.
route.post("/api/proxy", function (req, res) {
var saveAnyway = req.body.saveAnyway;
var noHealthCheck = req.body.noHealthCheck;
function save(proxy) {
proxy.save(function (err, saved) {
if (err) {
res.status(400).send(err.stack);
}
res.send(saved);
});
}
Promise.resolve(new ProxyResource(req.body))
.then(function(proxy) {
proxy.status = "valid";
if (saveAnyway) {
save(proxy);
}
return proxy;
})
.then(function(proxy) {
if (!noHealthCheck) {
var promises = HealthService.checkProxies([proxy]);
Promise.all(promises)
.catch(function(error) {
save();
})
.then(function(result) {
proxy.status = (result && result[0]) ? "valid" : "invalid";
if (proxy.status == "valid") {
save(proxy);
} else {
if (!saveAnyway) {
res.status(500).send("invalid proxy");
}
}
});
}
});
});
Bir expressJS
Router
handler’ı bu: Sisteme bir proxy
eklemek istediğinizde çağırdığımız API.
NodeJS ile back-end yazımına aşina olmayanlar için notlar:
|
Her request için sadece ama sadece 1 adet cevap gönderilmeli. |
Yapılmak istenen şu:
İsteğin gövdesinde gönderilen bilgilerle yeni bir proxy oluştur Eğer "saveAnyway" veya "noHealthCheck" parametlerine bak: sadece "saveAnyway" verilmiş ise: proxy'yi 'valid' olarak işaretle proxy'yi kaydet. proxy'nin kaydına dair cevap dön "noHealthCheck" verilMEmiş ise: proxy'nin dogruluğunu kontrol et: proxy calışıyor ise: proxy'yi 'valid' olarak işaretle proxy'yi kaydet. proxy'nin kaydına dair cevap dön değil ise: proxy'nin valid olması gerektiğine dair bir hata dön
Hadi bakalım, istenen davranış ile kodlanmış davranışın arasındaki 7 farkı bulun.
Öncelikle, bu kodun (eğer zaten bariz değil ise) neden sorunlu olduğuna bakalım:
Hatalar ve Sorunlar
En öncelikli hata (testleri patlaması üzerine bu kodu keşfetmemin sebebi de bu) save()
Promise içerisinden çağırılıyor ama Promise dönmüyor. save()
'in birden fazla defa çağırılabileceğini düşünürsek bu hataya gebe bir durum.
function save(proxy) {
return proxy.save() (1)
.catch(err => {
res.status(400).send(err.stack);
return proxy
})
}
1 | Bu metodu çağıran her yeri de döndüğümüz Promise’i kullanacak şekilde değiştireceğiz. Mesela save() denilip geçilen bir yerde artık return save() demeli. |
Kaçıranlar için altını çizeyim, API’mize gelen her bir istek için sadece ama sadece bir cevap dönülmeli. Kodda bu kuralın sağlandığını iddia etmek zor.
Bunu daha görsel bir biçimde görmek adına kodun akış şemasını çizeceğim:
Veritabanına kaydı yapıp cevap dönen balonu sarıyla işaretledim. Bu işlemi sadece bir defa yapma hakkımız var. Ama akışlara bakarak pek çok halde (mesela saveAnyway
verilmiş ise ve proxy’nin çalıştığı denenirken hata olursa) bunun birden fazla yapıldığı görülebiliyor.
save and respond 'un şeması |
Adım Adım İyileştirme
save and respond
bir defa yapılsın istiyor isek, o balondan çıkan tüm okları silebiliriz.
Sırada saveAnyway
durumunu birden fazla kontrol ediyor olmamız var.
Zaten saveAnyway
parametresinin değerine bakarak girdiğimiz yolda ayni değere ikinci bir defa bakmamıza gerek yok. Yani:
proxy.status = valid
atamasının da 2 defa yapılmasına gerek yok.
noHealthCheck
parametresi olmadığı vakit ne yapacağımız tanımlı değil. Parametreyi görmezden gelebiliriz (o kutuyu silerek); ama API’nin yapmak istediği saveAnyway
parametresi verilmiş ise invalid
olan proxy’leri de kaydetmek.
Ki bu şemayı şu hale sokuyor:
save and respond
işlemini açarsak:
Peki bu şemayı ilk şemamızla kıyaslarsak dikkat çeken şeyler neler?
-
Sadece bir yerde kaydediyor, bir yerde cevap dönüyoruz.
-
Cevap dönmek yaptığımız son işlem
-
Şema yukarıdan aşağıya doğru okunabiliyor.
-
Akışta bir dallanma olmuş ve bu dallar tekrar birleşmişse eğer, bunun nasıl olduğu değil sonucu bizi ilgilendiriyor. Örneğin
status = valid
derken bununnoHealthCheck
dendiği için mi yoksa proxy kontrolünde hata çıktığı için mi olduğunu umursamıyoruz;status
'unvalid
olarak atanması isteğiyle ilgileniyoruz sadece.
Tüm bu maddeler de zaten Promise kullanarak bunun inşaa edilebileceğine ipucu veriyor.
route.post("/api/proxy", function (req, res) {
let noHealthCheck = req.params.noHealthCheck
let saveAnyway = req.params.saveAnyway
let proxy = new Proxy(req.body)
Promise.resolve()
.then(() => {
if (noHealthCheck) {
return true;
} else {
return (
HealthService.checkProxy(proxy)
.catch(_ => true)
)
}
})
.then(isValid => {
if (!isValid && !saveAnyway) {
throw new Error("proxy is invalid")
}
proxy.status = isValid ? "valid" : "invalid"
return proxy.save()
})
.then(proxy => res.send(proxy))
.catch(err => res.status(400).send(err))
});
Az evvel vurguladığım 4 maddeyi burada da bulabiliriz:
-
res.send
veproxy.save()
bir yerde yapılıyor -
res.send
yaptığımız son işlem -
(ve 4) Promise zincirinin her bir halkası (
then
(veyacatch
) statement’ları) da kendi başına manalı.isValid ⇒ …
adımında buisValid
değerinin nereden geldiğini umursamadan onu kullanabiliriz. Bu sayede kodu yukarıdan aşağıya okuyabiliyoruz.
Maybe Akışı
Promise olmadan da bu gibi akışlar inşaa edilebilir. Güzel bir örnek Maybe’ye değindiğim önceki bir yazımda vardı; Luhn doğrulama algoritmasını yazmıştım. Onun şemasını hayal etsek:
new Just(str)
.map(x => x.split(""))
.map(l => l.filter(x => x != " "))
.map(l => l.map(x => parseInt(x)))
.chain(safe(l => !l.includes(NaN)))
.chain(safe(l => l.length > 1))
.map(l => l.reverse())
.map(l => l.map((x, i) => i % 2 == 1
? (x * 2)
: x))
.map(l => l.map(x => x >= 10
? (x - 9)
: x))
.map(sumList)
.map(x => x % 10 == 0)
.withDefault(false)
Bu örnekte daha da tek bir yatağa sınırlı bir akış görülebiliyor. Tüm akış tek bir noktada sonlanıyor ve tüm hatalar false
baloncuğunda toplanıyor.
Hataları yönetmek için Promise’lerden veya Maybe’den faydalanamayacak olsak, bu akışı erken-kaçış (early return) ile temsil edebilirdik.
const luhn(str) => {
let digits = str.split("")
.filter(x => x != " ")
.map(x => parseInt(c))
if (digits.includes(NaN)) {
return false
}
if (digits.length <= 0) {
return false
}
return sumList(digits.reverse()
.map((x, i) => i % 2 == 1
? (x * 2)
: x)
.map(x => x >= 10
? (x - 9)
: x)
) % 10 == 0
}
Aynı akış hissini bu kod da verebiliyor.
parseInt
Javascript eğer |
Çoğu problem de aslında basit bir akış şemasına denk gelebiliyor. Gereğinden karmaşık; okuması veya anlaması zor bir kodla karşı karşıyaysanız bunun bir de akış şeması olarak nasıl olabileceğini düşünün. Belki de çözümü sadeleştirmek için gereken şey, -beyaz yakalı bir ailenin gönderildiği özel okulda programlama öğretilen 8 yaşındaki çocuğu gibi- akış şeması üzerinden basit çözümler üretmektir.