野次馬エンジニア道

野次馬な気持ちでプログラミングをあれこれと綴ります

コードの複雑さ - テスタブルJavaScript

テスト熱が高まっているので自宅で下記の本を発掘。放置していた間に本書でメインに取り上げられているYUIは残念ながらメンテナンスされなくなっているようだ*1

テスタブルJavaScript

テスタブルJavaScript

後半のツールに関する記述も内容が古いかもしれないが、前半の1,2章は著者の見識が伺える内容だった。

1章は、一般的なソフトウェア開発のアプローチを紹介した上でテストの方法論やツールを紹介していく流れ。 前回アジャイル開発の流れでBDDについて書いてなかったのでここにメモ。

P6.より

ビヘイビア駆動開発(BDD: Behavior-Driven Development)はTDDを元にしており、開発者もそうでない人も共通の言語を使ってアプリケーションやモジュールのふるまいを表現できるようにしようというものです。(中略) 例えば、testEmptyCartというテストを作成するのではなく、テスト対象のふるまいを「ショッピングカートが空の場合は、ユーザは購入手続きに進めません」というように記述します。(中略) BDDはアジャイル開発で使われているユーザーストーリーがテストへ直接変換されます。一般的には、ユーザーストーリーは「私は[誰か]として[何かを行っ]て、[何かの処理結果を得る]ようにしたい」というテンプレートに基づいていなければなりません。

ユーザーストーリを直接テストコードに落とせることで「これって前々回のイテレーションで実装したはずなんだけどデグレしてる」といった事態をプロセス的に防ぐ。

複雑さ

2章のテーマは複雑さ。保守が容易なJavaScriptは、疎結合であり短くて分離が可能。この辺りは特にJavaScriptに限った話ではない。有名な話だが、与えられた時間のうちに半分はテストやデバッグに費やされる(http://www.rand.org/content/dam/rand/pubs/papers/2008/P4947.pdf)。

JSLintとforループ

複雑さを減らす一つの方法としては、JSLintなどを導入してコードを常に健全にしておくというもの。本書ではforループの例が紹介されていた。例えば、JSLintでは、`var i = 0i++と書かずに

for( i = 0; i < a.length; i = i + 1) {
    a[i] = i * i;
}

のように書く必要がある。もっともJavaScriptの慣習としては、

a.forEach(function (val, index) {
    a[index] = index * index;
});

の方が意図も伝わりクリーン。JavaScriptは変数を巻き上げるのでこのコールバックのみのスコープを用意できる方が確かに優れている。

循環的複雑度(cyclomatic complexity)

コードの中での実行の経路の数でそのままユニットテストの個数になる。

P25より

Aivosto(http://www.aivosto.com/)が循環的複雑度と誤った変更が行われる確率との関係を調査したところ(http://www.aivosto.com/project/help/pm-complexity.html)、表2-1のような結果が得られました。

循環的複雑度(CC) 誤った変更の可能性(Bad fix probability)
1-10 5%
20-30 20%
>50 30%
approaching 100 60%

(中略)先ほどの論文の中でMcCabeは、循環的複雑度が16以上のコードは信頼性が乏しいとも述べています。

感覚的にこれらの数字を頭にいれながらコードやテストを眺めるだけでメリットはあるのではないかと思う。

再利用

「コードをなるべく書かないように書く」という個人的な経験にも通じる有名な話、

P28より

コードのうち85%はアプリケーション特有のものではなく、プログラムの独自性が発揮されるのはコードの15%にすぎないという試算を発表しました。

後ろの章でdupfindツール(https://github.com/sfrancisx/dupfind)をJenkinsに導入する例が紹介されている。

ファンアウト

関数が直接または間接的に依存しているモジュールやオブジェクトの数を表す指標。で、具体的にどう数値化されるかというと、

P30より

手続きAについてのファンアウトとは、Aから発するローカルなフローの数とAが更新するデータ構造の数を加えた値です。

で、ローカルなフローというのがさらに下記のように分かれている。

  1. AがBを呼び出す場合
  2. AがBからの呼び出しに対して値を返し、Bがこの値を使用する場合
  3. CがAを呼び出し、返された値をBに渡す場合

ファンイン

イメージとしては、コードが再利用されているか否か。

P39より

手続きAについてのファンインとは、Aに対するローカルなフローの数とAが情報を取得する対象のデータ構造の数を加えた値です。

これらの値を使って情報理論的に複雑さは、(ファンイン x ファンアウト)2で表されるとしている。この値とバグには明確な相関関係が見られる。つまり、設計段階でこれらの値を把握することで、複雑さを避けて保守のコストを下げることにつながる。

結合とインジェクション

サブモジュールを定義すればファンアウトの値は小さくなるが依然元のモジュールは必要となるため結合度は下がらない。 密に結合してしまっているコードをインジェクションを用いて書き換える例を。

//P34より
function makeChikenDinner(ingredients) {
    var chicken = new ChickenBreast();
    var oven = new ConventionalOven();
    var mixer = new Mixer();
    var dish = mixer.mix(chiken, ingredients);
    return oven.bake(dish, new FDegrees(350), new Timer('50 min'));
}
var dinner = makeChikenDinner(ingredients);

関数と5個のオブジェクトが密結合してしまっている。ここでインジェクションを用いて結合度を下げる。 まずは、関連付けられそうなものをFacadeパターンを利用して抽象化してしまって

//P35-36より
function Cooker(oven){
    this.oven = oven;
}
Cooker.prototype.bake = function(dish, deg, timer) {
    return this.oven.bake(dish, deg, timer);
}
Cooker.prototype.degree_f = function(deg) {
    return new FDegrees(deg);
}
Cooker.prototype.timer = function(time) {
    return new Timer(time);
}

とする。これでOvenとTimer, FDegreesがスッキリした。

他もインジェクションの形にすると

//P37より
function makeChikenDinner(ingredients, cooker, chicken, mixer) {
    var = dish.mixer.mix(chicken, ingredients);
    return cooker.bake(dish, 
          cooker.degrees_f(350), 
          cooker.timer('50min'));
}
var cooker = new Cooker(new ConventionalOven());
var dinner = makeChikenDinner(ingredients, cooker);

となる。

テストコード

インジェクションを使うことで複雑度が下がるだけでなくモックでテストが可能となる。

//P38より
describe("test making dinner(after injection is applied)", function(){
    it("making dinner", function(){
        this.addMatchers(
            toBeYammy: function(expected) {
                return this.actual.attr.isCooked && this.actual.attr.isMixed;
            }
        );

        var ingredients = ["Parsley","Salt"];
        var chicken = {};
        var mixer = {
            mix : function(chick, ings) {
                expect(ingredients).toBe(ings);
                expect(chicken).toBe(chick);
                return { attr: { isMixed: true } }; 
            }
        };

        var MockCooker = function(){ }
        MockCooker.prototype = {
            bake: function(food, deg, timer) {
                expect(food.attr.isMixed).toBeTruthy();
                food.attr.isCooked = true;
                return food;
            },
            degree_f: function(temp) { expect(temp).toEqual(350); },
            timer: function(tiime) {
                expect(time).toEqual('50 min');
            }
        };

        var cooker = new MockCooker();
        var dinner = makeChickenDinner(ingredients, cooker, chicken, mixer);

        expect(dinner).toBeYammy();
    });
});

JavaScriptのテストに精通する著者のコードだけあって非常に綺麗なテストコード。