流れるようなFizzBuzz

Webの世界ではある意味でjQueryという単語は恐れられている。 その登場以降は雨後の筍のように増え続け、長らくWebエンジニアたちを苦しめてきた。 今ではだいぶその姿を見る機会もすくなりつつあるのだが、きっと完全になくなることはないだろう。 jQueryそのものを批判するつもりはなく、jQueryは便利であるが故にずっと採用され続けてきた。

今回はそんなjQueryのようなインターフェースを備えたもの。 Fluent Interfaceについて考えたい。 日本語では流暢なインターフェースと訳されるようだ。

NOTE: この投稿で使ったコードはこのページで確認することができる。

jQuery("#foo")
  .addClass("bar")
  .find("li")
  .click(function () {
    jQuery(this).addClass("baz").text("You clicked!");
  });

このプログラムの役割は直感的に理解しやすい。 #fooというIDを持つDOM要素にbarクラスを追加し、ツリーに含まれるliタグに対してクリックするとliタグの内容が変化する。

特徴はjQueryのメソッドを実行すると大抵の返り値はself、つまりjQueryを返すので変数に初期化しなくても同じDOM要素に対するメソッドを呼び出すことができる。

もしこのプログラムを手続き型のように1行ごとに記載したらこうなるだろう:

const bar = document.getElementById("bar");
bar.className = "bar";
bar.querySelectorAll("li").forEach((li) => {
  li.addEventListener("click", () => {
    li.className = "baz";
    li.innerText = "You clicked!";
  });
});

setter系のメソッドは返り値がここではセットした文字列を返すのであらかじめ変数に初期化しておく必要がある。

確かに理解しやすくはあるが、処理の流れが変わる箇所もあるし、必ずしもFluentInterfaceの書き方が優れているとも言い切れない。 ではなぜこのような書き方をするのだろうか。 なにかメリットはあるのだろうか。 もっとイメージしやすいものがある。

fetch('https://jsonplaceholder.typicode.com/todos/1')
// => Promise { <state>: "pending" }
      .then(response => response.json())
      .then(json => console.log(json))
// => { userId: 1, ...

そう、Promiseだ。 Promiseについてよく理解していない頃はどうしてこのようなコードを書くのだろうと疑問に思わなかっただろうか。 fetch()に対してthen()を呼び出さない場合は<state>: "pending"のPromiseクラスを返す。 response.json()は記述されているとおりレスポンスを受け取ってから初めて実行されるコールバック関数である。 つまり、then()は実際にWebサーバーのレスポンスに対して行う処理をあらかじめ記載しておくためのものだ。

今回はRubyを使ってFluentInterfaceを実装してみる。 とはいえ、例によって今回もChatGPTにコードを書いてもらった。

class FluentInterface
  def initialize
    @fizz_callback = nil
    @buzz_callback = nil
    @something_else_callback = nil
  end

  def on_fizz(&block)
    @fizz_callback = block
    self
  end

  def on_buzz(&block)
    @buzz_callback = block
    self
  end

  def on_something_else(&block)
    @something_else_callback = block
    self
  end

  def call(number)
    if number % 3 == 0 && @fizz_callback
      @fizz_callback.call
    elsif number % 5 == 0 && @buzz_callback
      @buzz_callback.call
    elsif @something_else_callback
      @something_else_callback.call(number)
    end
    self
  end
end

fi = FluentInterface.new
  .on_fizz { puts "Fizz" }
  .on_buzz { puts "Buzz" }
  .on_something_else { |n| puts n }

100.times do |n|
  fi.call(n)
end

実行結果までははらないでおくが、このようなコードだ。 細かく見るとFizzBuzzの間に改行が含まれていたり、0でもFizzが出力されているがそこは気にしないでおく。 このコードのキモとなる部分はあらかじめfizz_callbackbuzz_callbackを定義していて、実際にコードが評価されるのはcallが呼び出されたときだ。

もう少し具体的な例に落とし込むと、たまたま今日私が書いていたプログラムの一部を掲載しておく:

VideoDownloader.new
  .on_progress { |progress| puts progress }
  .on_complete { puts "done!" }
  .on_error { |code| puts "program exited with #{code}" }
  .download(video_url)

このクラスはあるコマンドのラッパーであるのだけれども、ちょうど前回の円形のプログレスなどと組み合わせて使うことを想定している。 ただ使うだけならわざわざFluent Interfaceに書き換える必要はない。 ダウンロードの処理に対して密結合を避けたかったので処理をわけることにした。 このプログラムでは最低限クラスを初期化してdownloadを実行すればいいだけなので、VideoDownloader.new.download()だけでも使うことができる。 そのためこのクラスではダウンロードの処理にだけ集中できる仕組みだ。

このように普段使わないような書き方を試してみるのもなかなか趣があってよい。