ブラウザAPIに依存した複雑なJSの処理から、ロジックを抽出し分離する方法を、事例を通して紹介します。
今回の事例は、繰り返し処理を伴うロジックの分離にgeneratorが有効であった例になります。
はじめに少し Universal ≠ SSR について
Universal JSの興味関心は、SSR(サーバーサイドレンダリング)に限りません。
より汎用なロジックの移植可能性がテーマです。例えば
- markdown文字列の構文解析
- deep copy
- テスト
- http client
- ランダムに文字列を出力
と、プラットフォームに依存しないべきロジックすべてが守備範囲になります。
しかし、Universal JSへの関心が薄い層がライブラリを作ると、
たとえばmarkdown"ファイル"の構文解析ライブラリができあがります。「ファイル」は物質世界の登場人物で、この時点でNodeJSでしか利用できないものになってしまうのです。
ですから、プラットフォーム依存APIと、依存しないロジックを分離することは、プログラムの再利用において非常に重要なのです。
題材
「文字がランダムで、切り替わるサンプル」
http://qiita.com/shunjiro/items/8eb6ad5426aefed67f45
上記の記事に、とてもカッコいいのアニメーションのサイトと、それをjQueryで実現するためのサンプルコードがありました。とても興味深い題材と思い、採用いたしました。
STEP1 ロジックの抽出
まずは、プラットフォーム非依存なロジックがどこなのかを決めるところから始めます。
概念図を上に示しましたが、JSのアプリケーションは、プラットフォーム固有のAPIと、プラットフォームに依存しないplainなJS = Logic の2つを持っています。これを分離し、適切な入出力を設計することで、ロジック部分がテストしやすくなったり、再利用可能になったりします。
分離後、下記のように3行で書けたらそれがベスト。
const input = $('#el').text() // ここは環境依存
const result = calcComplexLogic(input) // ここは複雑で本質的な処理、Universal
$('#el').text(result) // ここは環境依存
題材での、プラットフォームに依存しないロジック部分とは、
与えられた文字列を、最初は全部ランダム、その後前から順に正しい文字に置き換えていくという処理になります。
逆に、ブラウザへの表示や、ブラウザの文字列取得などは、プラットフォームAPIです。
ロジック部分は下記のようなイメージです。
// 半角英小文字をランダムに1文字取得
function randomChar() {
return String.fromCharCode(Math.floor(26 * Math.random()) + 97)
}
// だんだん正しい文字が増えていく処理
for (let i = 0; i <= original.length; i++) {
original.split('').map((c, j) => (j < i) ? c : randomChar()).join('')
}
STEP2 ロジックとプラットフォームのやりとりを決める
基本的にロジックとプラットフォーム依存部は関数ベースでやりとりします。上記のように、引数と戻り値、というコミュニケーションであれば非常に明快です。しかし題材では、計算された結果を毎回描画しています。描画というプラットフォームなAPIを毎回実行しなくてはなりません。
非推奨例1 オブジェクト/関数渡し
そこでまずは下記のようにやってみます。
const input = $('#el').text()
outputResults(input, $('#el')) // ここはUniversal?
function outputResults(input, view) {
setTimeout(() => {
// ... 出力したい文字を計算する処理
view.text(result) // jQuery依存 + 副作用
})
}
描画処理も、ロジック側に持たせてみましたが、どうでしょう。
ロジック側の関数、outputResults
は、jQueryのAPIを知らないといけなくなりました。
もちろん、JSはduck typingですから、 view.text()
というインターフェイスに依存しただけで、
他のプラットフォームでも利用することはできるかもしれません。
しかし、そのインターフェイスに統一できない系があるかもしれないし、本来UniversalにしたいoutputResults
のなかで、副作用を起こすのはあまりよくないでしょう。
非推奨例2 配列
やはりロジック側は値を計算し、プラットフォーム側に渡してあげるべきです。
そこで、予め、すべての結果を計算し、結果を配列で返すようにしました。
const input = $('#el').text()
const results = calcAllResults(input) // 全結果の取得。時間かかるかも?
const timer = setInterval(() => {
if (results.length === 0) return clearInterval(timer)
$('#el').text(results.pop())
}, 50)
今回の仕様ならば問題ないでしょうが、
計算時間がかかるものであれば、全結果を計算するのは躊躇されます。
generator 必要な分だけ計算
そこで、generatorを使うと、綺麗に無駄なく分離できます。
function *randomToOriginal(original) {
for (let i = 0; i <= original.length; i++) {
yield original.split('').map((c, j) => (j < i) ? c : randomChar()).join('')
}
}
function randomChar() {
return String.fromCharCode(Math.floor(26 * Math.random()) + 97)
}
const iter = randomToOriginal($('#el').text())
const timer = setInterval(function() {
const { done, value } = iter.next()
if (done) return clearInterval(timer)
$('#el').text(value)
}, 50)
利用側のコードはほとんど変化していませんが、文字列生成処理は、描画前に実行され、効率がよいです。
STEP3 他のプラットフォームでも利用
このgeneratorをNode.jsでも同じように利用してみました。
const iter = randomToOriginal('Universal JavaScript')
const timer = setInterval(function() {
const { done, value } = iter.next()
if (done) {
process.stdout.write('\n')
return clearInterval(timer)
}
process.stdout.clearLine()
process.stdout.cursorTo(0)
process.stdout.write(value)
}, 50)
今回抽出、分離したロジックは、
「文字列をランダムからだんだん目的のものに変えていく」
というプラットフォームに依存しないものであったため、このようにNodeでも利用できました。
これが
「文字列をランダムからだんだん目的のものに変えてブラウザに表示する」
だと、Nodeでは利用できなくなります。「物質世界」をロジック内に入れないことが大事です。
まとめ
- ロジック抽出とは、プラットフォームに依存しない部分を抜き出すこと
- ロジック側と利用側のやりとりが多くなる場合はgeneratorを検討するとよい
- ロジック抽出がうまくできると、再利用できるプラットフォームが増えてよい
最後に 物質世界とか言ってますが
「関数型プログラミングに目覚めた!」という本があります。ぶっ飛んでいるようにも見えますが、私は「物質世界」「精神世界」あたりのイメージを掴むのに参考になりました。ロジックとプラットフォームAPIを分離する話をするときに、便利な共通言語を作ってくれたと思っているのですが...。