流れるようなFizzBuzz
15 Oct 2023Webの世界ではある意味で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_callback
やbuzz_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()
だけでも使うことができる。
そのためこのクラスではダウンロードの処理にだけ集中できる仕組みだ。
このように普段使わないような書き方を試してみるのもなかなか趣があってよい。