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.params.saveAnyway;
var noHealthCheck = req.params.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 (noHealthCheck) {
save(proxy);
}
return proxy;
})
.then(function(proxy) {
if (!noHealthCheck) {
var promises = HealthService.checkProxies([proxy]);
Promise.all(promises)
.catch(function(error) {
if (saveAnyway) {
save(proxy);
}
return false;
})
.then(function(result) {
proxy.status = (result && result[0]) ? "valid" : "invalid";
if (proxy.status == "valid" || saveAnyway) {
save(proxy);
} else {
if (!saveAnyway) {
res.status(500).send("invalid proxy");
}
}
});
} else {
save(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 "noHealthCheck" 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: "saveAnyway" verilmiş ise: proxy'yi 'invalid' olarak işaretle proxy'yi kaydet. proxy'nin kaydına dair cevap dön verilmemiş ise: proxy'nin valid olması gerektiğine dair bir hata dön hata aldiysan: proxy 'invalid' muamelesi yap
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: Birden fazla kere cevap yollamaya calisiyoruz (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. -
HealthService’in cevap vermemesi durumundaki istemeden
save()
yapiyoruz. davranisimiz yanlis.
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:
Adım Adım İyileştirme
Benzer hamleleri renklendirecegim. Ideal bir durumda ayni renkli islemlerden sadece birer adet olmali.
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
işlemini açarsak:
save and respond 'un şeması |
Koparmasi kolay olan "save and respond" hamlelerini tek bir yerde topluyorum.
Ayni yol uzerinde birden fazla ziyaret edilen adimlari farketmeye devam ediyorum…
Pembe yolun "saveAnyway?" ve "save and respond"u iki kere ziyaret ettigini dikkat edelim. Tekrara gerek yok.
Pembe yolu degistirerek tekrar eden "proxy.status = valid" baloncugu teke indirilebilir.
Graphviz nedense balonu sagdan sola atti, ama tek yaptigimiz tepedeki balonu silip yolu mavi olacak sekilde degistirmek idi.
Pembe ve sari konumlari degistirmek biraz daha simetri katacak ve kosullari (elmaslari) bir araya toparlayacak.
Akis semalarinda elmas kumeleri birlestirilebilir: |
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 dallanmanin etkilerini daha asagidaki baloncuklarda aklimizda tutmamiza gerek yok. Parmagimizi "start"in uzerine koyup asagiya dogru cizgileri takip ederek davranisi anlayabiliyoruz. Herhangi bir noktada geriye gidip bir seyi hatirlamamiza gerek olmuyor.
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(_ => false)
)
}
})
.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.