JavaScript でも型チェックと契約による設計で安定した開発をする

  • 145
    いいね
  • 5
    コメント

チーム開発をやっていると特定の処理を呼び出す際にインターフェイスを明示することがとても重要になってきます。言い換えると使い方がきちんと示されていることが最低ラインということです。ドキュメントは実際の処理と乖離しますし、各人がソースコードの処理を追わなければならないというのはチームでやっている意味がありません。

ところが JavaScript にはそういった仕組みが存在しません。どういった処理をするのかを表すための関数名は指定できますが、 JavaScript では関数を任意の名前の変数に代入できるので実はあまり役に立ちません。

といった状況にあった JavaScript ですが、昨今のツールの登場によって事情が変わってきました。 JavaScript でもインターフェイスを明示しながら開発するにはどうすればいいかを要素技術と一緒に書いていきます。

型チェック

あくまでも JavaScript での開発ということで flow を使います。ツールとしてすぐに着脱可能なのがよいですね。

https://flowtype.org/

契約による設計

契約による設計とは Eiffel で導入された概念で、あるコード群を使う側 Client と使われる側 Supplier で満たすべき事前条件 Obligations 、事後条件 Benefits 、不変条件 Invariants を明確に定義し、動作したときにチェックできるようにしましょう、というものです。

https://www.eiffel.com/values/design-by-contract/introduction/

一般的に、各条件のチェックには assert が使われます。 assert に期待されている役割として満たすべき条件のチェックということが浸透しているためです。

実例

コードみたほうがわかりやすいと思うのでサンプルを用意しました。

https://github.com/januswel/type-check-dbc-sample

つぎが使われる側のコード片です。取り扱いやすいよう関数として定義しています。

https://github.com/januswel/type-check-dbc-sample/blob/master/src/lib/awesome.js

型チェック

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 はちょうどこの前ロゴが決まったそうです🎉。イルカさんです🐬。

片方でできないの ?

flow による型表現では内包表記がないため、今回のケースは表現できません。また、複数の引数が指定された際に満たすべき条件、というものも事前条件として指定したい場合があります。これは型だけではチェックしようがないので契約による設計が必要です。

いっぽう、 JavaScript は動的に型を取得することができるので、 assert で型チェックまでこなすことは可能です。が、 typeof で型チェックを書いていかなければならないので手間がかかってしまいます。ちなみに flow は事前に型の整合性がとれているかをチェックするツールのため、オーバーヘッドも生じません。

うれしさ

というわけでここまでやるとなにがうれしいのか、ということをまとめます。

テストケースが減るうれしさ

https://github.com/januswel/type-check-dbc-sample/blob/master/test/lib/awesome.js#L16-L19

このテストは assert での型チェックを想定したつくりとしていますが、事前に flow によるチェックができていれば必要ない部分です。こういったテストは引数が増えるごとにパターンが爆発していきますので、 flow でこの部分を担保することでテスト数を低減させることができます。

使い方を間違えると教えてくれるうれしさ

契約による設計の恩恵として、事前条件に違反する、つまり使い方を間違えると AssertionError が返ってくるのですぐに気づくことができます。さらに、該当コードをみるとどういう条件を満たすべきなのかということも書いてあるため、別途ドキュメントとして残す必要が無いことも利点です。

本番実行時にオーバーヘッドがないうれしさ

flow も unassert も開発時には大活躍してくれますが、本番で動かす際には何もせずオーバーヘッドが生じません。どうしてもこれが前提となるため、 unassert はもう手放せないツールです。

まとめ

今回紹介したものは単なるツールなので必要なくなったときやもっと良いものができたときに、比較的簡単にパージもしくは乗り換えることが可能です。 "Do one thing and one thing well" なものをうまく組み合わせてやりやすい環境を作りましょう。

あしたは @uryyyyyyy さんによる React Native のハナシです。

この投稿は CureApp Advent Calendar 201620日目の記事です。