トップ «前の日記(2005-02-26) 最新 次の日記(2005-02-28)» 編集

日々の破片

Subscribe with livedoor Reader
著作一覧

2005-02-27

_ DIへの途

kdmsnrさんのクリップ経由。

で、思い出したが、純単体テストってやってないのかな? と。

DIコンテナ(であろうがなかろうが。たとえば意味合いは相当ずれるがファサードだって良いわけで)を利用するということはバカが逝くで最近試行錯誤されているように、テストを簡単にするっていう意味がすごく大きい。

でも、テストって一言で言ってしまうと受け入れテストとか負荷テスト(マシンハンマー=MCハマー)とか猫ピアノテストとかが入ってきたり、用語の差があったりして厄介なんだが、あくまでも単体テストなんだが、実はこいつも、ではデータベース(が曲者なのだが)と接続しないで単体テストと言えるのですかとか真顔で聞かれてもちょっと困ってしまうわけで、そこで、TDDという3文字略語で済ませられればいいのだが、動作を検証しながらプログラミングしましょうね、ということだから、もういっそテストとか言うと誤解を受けるから、ダックタイピングというのはアヒルがとにかく突っついてみるってことから命名されたということを援用して(でもダックタイピングについては実は大きく間違って僕が読んでいるかも知れないからまったくもっての見当外れかも知れないけど)ダックプログラミングとか呼んだらどうだろうとか、つまるところは開発のスタイルのことである。

エロ事師たち (新潮文庫)(野坂 昭如)

突然、野坂昭如の息の長い(=「。」を打たない)文体を思い出したが書いていても疲れるなぁ、ハァハァ。

で、『DIへの途』の例はなかなかうまいので内容を真似してDIを利用してプログラミングするということを書いてみる。でも、内容はくそまじめだが書き方はよたよたふらふら夾雑物をじゃかすか入れて書いてみる。だからそのままここ読めじゃ通用しないから必ず自分で内容を咀嚼するよろし。

/**
 * 生鮮食品用の価格を照会するクラス。
 * 午後6時を過ぎたら(以降だよ)無条件に5割引きだ。
 */
public class FishPricePicker extends PricePicker {
    /**
     * 値段を返す。日本だし、別に不動産を扱うわけじゃないからlongで
     * 良いのだ。って言うか生鮮食品ならshortでも十分かも。
     * @param itemCode 商品を示すコード
     * @return 値段。
     */
    public long getPrice(String itemCode) {
        long price = super.getPrice(itemCode);
        Calendar c = Calendar.getInstance();
        if (c.get(Calendar.HOUR_OF_DAY) >= 18) {
            // このプログラムの仕様で18:00って言ってるわけだから
            // (それが証拠にクラスのJavadocにもそう書いてある)
            // こんなのは直定数で良いのだ。
            // 条件節を読んだとき、他の何も参照しなくても条件が
            // わかるように書くほうが読みやすいぞ。
            price /= 2; // 切り捨てご免
         }
         return price;
    }
}

なかなか自己主張が強いクラスだが、これを例とする。

このクラスはいきなり歩けない(とアヒルにこだわってみたり。っていうかダックタイピングのダックって本当にそういう意味なのかなぁ)。何しろスーパークラスには依存しているは(たとえばスーパークラスは当然のようにデータベースを読んで値段を参照するだろうし。するとデータベースに登録されていないitemCodeを与えるときっと例外とかになるだろうからこのクラスの割り算処理とかはテストできないだろうし、っていうかこのプログラムを作っているPCからデータベースにつなげられるのかどうかとかいろんな問題がある)、プログラムしている時刻にも依存しているは(18:00過ぎない――過ぎるって言葉はそれを含まないと思うんだけど、それでは以降の意味の言葉はなんだろう?――と5割引きのテストができないってことだな。またはいちいち時刻をコントロールパネルから変えるのか?)で、どうするのよ? という話だ。

こんな単純なプログラムだからコンパイル通ったらいきなりリリースでしょ、というのは、確かにこれくらいだったらありかも知れないとか思わないでもないけどそれは例だからで、やはり動かしながらプログラミングしなよ、そのほうが差し戻しとか無くてハッピーでしょ、お互いに。

って言うか、いきなりPricePickerがインターフェイスじゃないのが問題なんだが。なんで問題かはとりあえずおいておいて、このまま、動かしながらプログラミングできる形に変えていく(コメントは省略)。

public class FishPricePicker extends PricePicker {
    PricePicker innerPicker;
    public FishPricePicker() {
    }
    public void setPricePicker(PricePicker newInnerPickder) {
        innerPickder = newInnerPickder;
    }
    public long getPrice(String itemCode) {
        // 実行時にバグれば最初の行でNullPointerExceptionで死ぬ。
        // 異常なら正しく死ぬ場合には不変条件の検証のみで良い。
        // (DBを破壊してから死ぬような場合にはやはりちゃんとチェックしたほうが良い)
        assert innerPicker != null; 
        long price = innerPicker.getPrice(itemCode);
        Calendar c = Calendar.getInstance();
        if (c.get(Calendar.HOUR_OF_DAY) >= 18) {
            price /= 2;
         }
         return price;
    }
}

っていうわけでデコレータ(追記:わけわからん)元のPricePickerを利用するように変形する。これでPricePickerが依存していたテーブルとかデータベースとかからまずは解放されたわけだ。っていうのも次のプログラムをどこでも呼べるように本来的にはなる。

public class FishPricePickerTest extends TestCase {
    FishPricePicker fpp;
    protected void setUp() throws Exception {
        fpp = new FishPricePicker();
    }
    private class MockPricePicker extends PricePicker {
        String itemCode;
        long testPrice;
        MockPricePicker(String c, long p) {
            itemCode = c;
            testPrice = p;
        }
        long getPrice(String c) {
            if (itemCode.equals(c)) {
                return testPrice;
            }
            return -1; // アサートされる数
        }
    }
    public void testPick() throws Exception {
        fpp.setPricePicker(new MockPricePicker("ABCDEFG", 12345L));
        long p = fpp.getPrice("ABCDEFG");
        assertEquals(12345L, p);
    }
    public static void main(String[] args) {
        junit.textui.TestRunner.run(new TestSuite(FishPricePickerTest.class));
}

っていうか、ここでPricePickerがクラスではなく、インターフェイスにしておくべき理由がわかる。もしPricePickerがコンストラクタでいきなりJNDIからデータソースをルックアップする作りだったらどうよ? いきなり動かないわけだ。いやもちろん、テスト用のデータベースサーバといつでも接続可能なように開発マシンをセットアップしておいて、データベースサーバ(テスト用なんだけど開発中は上げたり下げたりしないようにして、って既に問題あるだろ)にはいつでも参照すべきテーブルを用意しておけば良いのだが、その場合はその場合で、テストケースの実行の都度コネクションをしなおすのはあほくさいぞ。コネクションプールが使えるわけじゃないし。だから、こういったクラスは実装クラスとその派生とはせずに、インターフェイスを常にきっておくべきなのだ。たとえば

public interface PricePicker {
    long getPrice(String itemCode);
}

これだけのことだ。で、今までスーパークラスとして扱ってたやつはpublic class PricePickerImpl implements PricePickerとすればいいだけの話である。当然、public class FishPricePicker implements PricePickerとかprivate class MockPricePicker implements PricePickerに変わるわけで、これでPricePickerの実装を気にせずどこでも呼び出しが可能となるわけであるのである。というわけでたかだか(Javadocとか含めなければ)3行の1つのファイルを作る手間を惜しんじゃいけない。でも、まあ、ここではこのまま進めることにするけど。

というわけでPricePickerデコレート間接的に利用するようにFishPricePickerを修正したから、これでテストプログラムを使って実行できるようになりました。めでたし。でも、そうじゃない。18:00以降の問題が残っている。本当に5割引きするのか、というか、仮に正しく作られていたらFishPricePickerTestを18:00以降に実行するとアサーションに引っ掛かるじゃん。まずいじゃん。

何がまずいかというと、Calendar.getInstnace()がまずい。実行環境の時刻に依存してるからだ。というわけで、ここを変形する。元のページだと一工夫あるけど、疲れてきたからストレートにやってしまう。

public class FishPricePicker extends PricePicker {
    Calendar calendar;
    PricePicker innerPicker;
    public FishPricePicker() {
    }
    public void setPricePicker(PricePicker newInnerPickder) {
        innerPicker = newInnerPickder;
    }
    public void setCalendar(Calendar newCalendar) {
        calendar = newCalendar;
    }
    public long getPrice(String itemCode) {
        assert calednar != null;
        assert innerPicker != null;
        long price = innerPicker.getPrice(itemCode);
        if (calendar.get(Calendar.HOUR_OF_DAY) >= 18) {
            price /= 2;
         }
         return price;
    }
}

テストプログラムは次のようになる。

public class FishPricePickerTest extends TestCase {
    FishPricePicker fpp;
    protected void setUp() throws Exception {
        fpp = new FishPricePicker();
    }
    private class MockPricePicker extends PricePicker {
        String itemCode;
        long testPrice;
        MockPricePicker(String c, long p) {
            itemCode = c;
            testPrice = p;
        }
        long getPrice(String c) {
            if (itemCode.equals(c)) {
                return testPrice;
            }
            return -1; // アサートされる数
        }
    }
    public void testPick() throws Exception {
        fpp.setPricePicker(new MockPricePicker("ABCDEFG", 12345L));
        fpp.setCalendar(new GregorianCalendar(2005,0,1,10,0)); // 日付は何でも良いはずだ。時刻のみ重要
        long p = fpp.getPrice("ABCDEFG");
        assertEquals(12345L, p);
    }
    public void testPickAfter1800() throws Exception {
        fpp.setPricePicker(new MockPricePicker("ABCDEFG", 12345L));
        fpp.setCalendar(new GregorianCalendar(2005,0,1,18,0));
        long p = fpp.getPrice("ABCDEFG");
        assertEquals(12345L / 2, p);
    }
    public void testNoNoMatch() throws Exception {
        fpp.setPricePicker(new MockPricePicker("ABCDEFG", 12345L));
        fpp.setCalendar(new GregorianCalendar(2005,0,1,10,0));
        long p = fpp.getPrice("ABCDEFGH");
        // マッチしない場合のルールが負値の返送で良いとは思えないけど。
        assertEquals(-1, p);
    }
    public static void main(String[] args) {
        junit.textui.TestRunner.run(new TestSuite(FishPricePickerTest.class));
}

で、DIコンテナが必須かと聞かれるとその答えはNOということになるんですな。たとえば、次のようなファサードを用意するだけでも良いからだ。って言うか、extendsで良いかどうか、それをファサードと呼んで良いか、デコレータではないかとかいろいろあるが、実装はデコレータパターンを使った意味はファサードなクラスなのだ(追記:なんか本当にファサードかなぁという気もしてきたが、内部の事情を見せずに利用させてやるってことを考えるとやっぱりファサードであってるようにも思えるし)。

public class FishPricePickerFacade extends FishPricePicker { 
    public FishPricePickerFacade() {
        setPricePicker(new PricePicker());
        setCalendar(Calendar.getInstance());
    }
}

もちろん、FishPricePickerFacadeをTDDしたいという人がいても良いけど、さすがに、僕はこれには不要だと思うな。でも、そういう場合には、こういうある意味においては余分なクラスを作らないですませる方法がある。つまりDIコンテナを使うという方法だ。

注:当然だがある時点のCalendarのインスタンスを使う格好で書いてあるわけで、これはもちろんまずい。元のページは時刻(じゃなくて月日だけど)を取得するインターフェイスを別に用意することで解決しているので、それについてはそちらを参照するか、正しく動作する方法を考えるべきでしょう。

追記:(っていうか、コメントで横着せずに最初からここへ書けばいいのに何をやってんだか。相当あせったようだな。)nekopさんの指摘に合わせてさすがにまず過ぎるコードを修正しました。どうもご指摘ありがとうございます。

_ っていうかDI

ブラボーブラバーブラベスト。今まで見た中でもっとも素晴らしいDIについての知見(そう言えば矢野徹片手に灰になるまでやりましたな。忘れてたけど)。

本日のツッコミ(全6件) [ツッコミを入れる]
_ dot (2005-02-28 01:03)

ささやき-いのり-えいしょう-ねんじろ!<br>dotは灰になりました。

_ nekop (2005-02-28 02:09)

Calendar.HOURは0から11っす。

_ arton (2005-02-28 02:28)

>nekopさん<br>ぐは、まさにそのとおり。これは直接ストライクアウト無しで修正します。だからTDDが必要なんです(と言い訳するのだが、実際テストすれば引っ掛かるな)。

_ arton (2005-02-28 02:32)

あ、直接修正したらnekopさんのコメントの意味がわかりにくくなってしまった(当然か)。上のリストで、Calendar.HOUR_OF_DAYとなっている個所が最初、全部Calendar.HOURと書いていたのでした。

_ babie (2005-02-28 19:26)

「ガァ」って鳴いたら何でもアヒルとして扱う、同じ振る舞いさえすれば同じものとして扱う、ですよね?<br>>ダックタイピング

_ arton (2005-02-28 20:30)

そっか。すごく納得しました。>babieさん。なんか、アヒルみたいに突っつき回すだかよちよち歩くだか書いてあるのを見て誤解してたみたいです。<br>どうもありがとうございます。


2003|06|07|08|09|10|11|12|
2004|01|02|03|04|05|06|07|08|09|10|11|12|
2005|01|02|03|04|05|06|07|08|09|10|11|12|
2006|01|02|03|04|05|06|07|08|09|10|11|12|
2007|01|02|03|04|05|06|07|08|09|10|11|12|
2008|01|02|03|04|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|07|08|09|10|11|12|
2018|01|02|03|04|05|

ジェズイットを見習え