チーム開発をやっていると特定の処理を呼び出す際にインターフェイスを明示することがとても重要になってきます。言い換えると使い方がきちんと示されていることが最低ラインということです。ドキュメントは実際の処理と乖離しますし、各人がソースコードの処理を追わなければならないというのはチームでやっている意味がありません。
ところが JavaScript にはそういった仕組みが存在しません。どういった処理をするのかを表すための関数名は指定できますが、 JavaScript では関数を任意の名前の変数に代入できるので実はあまり役に立ちません。
といった状況にあった JavaScript ですが、昨今のツールの登場によって事情が変わってきました。 JavaScript でもインターフェイスを明示しながら開発するにはどうすればいいかを要素技術と一緒に書いていきます。
型チェック
あくまでも JavaScript での開発ということで flow を使います。ツールとしてすぐに着脱可能なのがよいですね。
契約による設計
契約による設計とは Eiffel で導入された概念で、あるコード群を使う側 Client と使われる側 Supplier で満たすべき事前条件 Obligations 、事後条件 Benefits 、不変条件 Invariants を明確に定義し、動作したときにチェックできるようにしましょう、というものです。
一般的に、各条件のチェックには assert
が使われます。 assert
に期待されている役割として満たすべき条件のチェックということが浸透しているためです。
実例
コードみたほうがわかりやすいと思うのでサンプルを用意しました。
つぎが使われる側のコード片です。取り扱いやすいよう関数として定義しています。
型チェック
concat
は引数に指定されたものをすべて文字列化して結合する関数、 sum
は引数に指定された数値すべてを足し合わせる関数です。それぞれインターフェイスは次のようになっています。
function concat(...s: Array<any>): string
function sum(...n: Array<number>): number
これを型なしで記述すると次のようになります。
function concat(...s)
function sum(...n)
名前が違うものの、どちらも引数についてはいくらでも指定可能、ということしかわかりません。 concat
は英語の concatenate の略です。 sum
はそのまま英語の sum の意味で書いていますが、これだけだと「合計する」という意味なのか「要約する」という意味なのかもはっきりしません。「要約する」という意味なら summarize
にすべきという指摘は一度おいておいてください…。
もう一度型つきの定義を見てみます。
function concat(...s: Array<any>): string
function sum(...n: Array<number>): number
concat
はどんなものでも受け付けて文字列として返してくれるもの、とわかります。ここで名前から読み取れる意味も総合して、引数すべて文字列として連結するものとわかりますね。
sum
は数値のみを受け付けて数値を返すものなので先ほどの「合計する」という意味あいだとはっきりします。
契約による設計
zeroPadding
にも型定義をしています。
function zeroPadding(n: number, digit: number = 8): string
ここからは数値を渡すとデフォルトで 8 桁の 0 詰めした文字列を返してくれることがわかります。
さらに本処理の前に、いくつか assert
文があります。
assert(0 <= n)
assert(1 < digit)
これが事前条件のチェックになります。使う側がきちんと事前条件をクリアしているかを使われる側がチェックしているわけです。このチェックのあとに本処理がはじまります。
ここからは 0 詰めしたい数値は 0 以上でなければいけないこと、桁数は 1 桁より大きくなければいけないことがわかりますね。どちらもその条件を満たしていないと処理しようがないことがわかります。
assert
について重要なこととして、 C 言語などでも存在しますが、多くの言語ではデバッグ用にコンパイルしたときのみ動作し、本番用にコンパイルしたときは命令そのものがなくなってしまうという挙動をします。ところが JavaScript ではそういったことはおこらず、デバッグとして動かそうが本番として動かそうが assert
は毎回走ることになります。
assert
を実行時の環境によって作用させたりさせなかったりと切り分けることができるのが、 babel と unassert のあわせ技です。
https://babeljs.io/
https://github.com/unassert-js/unassert
babel-plugin-unassert を使って、 NODE_ENV=production
を指定したときのみ assert
を削除するよう設定できます。
https://github.com/januswel/type-check-dbc-sample/blob/master/package.json#L16
https://github.com/januswel/type-check-dbc-sample/blob/master/.babelrc
実際に↑の repo を手元に clone し、 npm run build && node ./dist/contract-violation.js
したときと NODE_ENV=production npm run build && ./dist/contract-violation.js
したときの実行結果を見比べてみてください。 NODE_ENV=production
を指定しなかったときは AssertionError
が、指定したときはまた違うエラーが出ていることが確認できます。
タイミングいいことに、 unassert はちょうどこの前ロゴが決まったそうです🎉。イルカさんです🐬。
JavaScript で表明プログラミングや契約プログラミングをサポートする unassert ファミリーを github organization に移行し、ロゴも決まりました。よろしくお願いします! / “unassert-…” https://t.co/r7Pl0Cfrbc
— Takuto Wada (@t_wada) 2016年12月15日
片方でできないの ?
flow による型表現では内包表記がないため、今回のケースは表現できません。また、複数の引数が指定された際に満たすべき条件、というものも事前条件として指定したい場合があります。これは型だけではチェックしようがないので契約による設計が必要です。
いっぽう、 JavaScript は動的に型を取得することができるので、 assert
で型チェックまでこなすことは可能です。が、 typeof
で型チェックを書いていかなければならないので手間がかかってしまいます。ちなみに flow は事前に型の整合性がとれているかをチェックするツールのため、オーバーヘッドも生じません。
うれしさ
というわけでここまでやるとなにがうれしいのか、ということをまとめます。
テストケースが減るうれしさ
このテストは assert
での型チェックを想定したつくりとしていますが、事前に flow によるチェックができていれば必要ない部分です。こういったテストは引数が増えるごとにパターンが爆発していきますので、 flow でこの部分を担保することでテスト数を低減させることができます。
使い方を間違えると教えてくれるうれしさ
契約による設計の恩恵として、事前条件に違反する、つまり使い方を間違えると AssertionError
が返ってくるのですぐに気づくことができます。さらに、該当コードをみるとどういう条件を満たすべきなのかということも書いてあるため、別途ドキュメントとして残す必要が無いことも利点です。
本番実行時にオーバーヘッドがないうれしさ
flow も unassert も開発時には大活躍してくれますが、本番で動かす際には何もせずオーバーヘッドが生じません。どうしてもこれが前提となるため、 unassert はもう手放せないツールです。
まとめ
今回紹介したものは単なるツールなので必要なくなったときやもっと良いものができたときに、比較的簡単にパージもしくは乗り換えることが可能です。 "Do one thing and one thing well" なものをうまく組み合わせてやりやすい環境を作りましょう。
あしたは @uryyyyyyy さんによる React Native のハナシです。