En Güzel JavaScript kodu: Akış Şeması Programlama

December 2019 · 5 minute read

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:

  • req gelen isteğe dair bilgileri içerir.

  • res.send() isteğe cevap yollamak için kullanılıyor.

  • ProxyResource veritabanında tutulan bir model. new denerek oluşturulan nesnelerin save() metodları çağırılarak veritabanına kaydedilebilirler. save() metodu başarılı veya başarısız olabilir ve sonucunu Promise olarak döner.

  • Promise asenkron işlemleri birbirlerine zincirleyebilmek için bir yapı.

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.

save()
  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?

  1. Sadece bir yerde kaydediyor, bir yerde cevap dönüyoruz.

  2. Cevap dönmek yaptığımız son işlem

  3. Şema yukarıdan aşağıya doğru okunabiliyor.

  4. 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 bunun noHealthCheck dendiği için mi yoksa proxy kontrolünde hata çıktığı için mi olduğunu umursamıyoruz; status'un valid olarak atanması isteğiyle ilgileniyoruz sadece.

Tüm bu maddeler de zaten Promise kullanarak bunun inşaa edilebileceğine ipucu veriyor.

yeni kodumuz
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:

  1. res.send ve proxy.save() bir yerde yapılıyor

  2. res.send yaptığımız son işlem

  3. (ve 4) Promise zincirinin her bir halkası (then (veya catch) statement’ları) da kendi başına manalı. isValid ⇒ …​ adımında bu isValid 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:

Luhn doğrulama
  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.

Early-return ile Luhn doğrulama
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.

Maybe kullanmak istememin sebebi hata yönetimi için yegane çözüm olduğu için değil. Maybe bizi Nothing durumunu gözetmeye zorladığı için kullanılmasını teşvik ettiğim bir hazne tipi.

parseInt
parseInt :: String -> Number|NaN
parseIntSafe :: String -> Maybe Number

Javascript eğer parseInt için Maybe dönüyor olsa biz hata durumunu görmezden gelemeyecektik; Maybe'den Number'ı çıkarmak için yapabileceğimiz her şey bir şekilde bu hata durumunun kontrolü olacaktı.

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