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

December 2019 · 6 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.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:

  • 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 "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.

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:

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?

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

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(_ => 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:

  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.