Skip to content.

Sections
Personal tools

Cotton Bolls: February 2004 バックナンバー

Document Actions

« January 2004 | トップページ | March 2004 »

2004.02.29

トートロジーのアンチパターン

テストファーストを行うと,テストコードの中に実装コードが紛れ込むことがある.

先日,工場の機械類を監視するシステムの開発をしていた.監視システムは機械の状況をポーリングし,故障を発見すると画面上にダイアログを表示する.機械は複数あり,一度に故障が何箇所も発生してしまう場合がある.このとき,故障の数だけダイアログを表示し,位置をずらして古いダイアログを隠してしまわないようにしなければならない.また,24時間稼動なので,ユーザがいないと画面上にダイアログがどんどんたまっていく.それを防ぐため,ダイアログが表示される数の上限を決め,古いダイアログは自動的に閉じて消費メモリを増やさない工夫が必要だ.

そこで,ダイアログの配置と上限を管理するクラスをTroubleDialogFactoryにする.ダイアログ全体の管理はそれを生成するTroubleDialogFactoryに任せ,故障をフックするコードがTroubleDialogFactoryにダイアログの生成と表示を依頼する.そうすれば,現在表示されているダイアログの状況から次に表示するダイアログの位置と上限が管理できそうだ.

まずはテストコードだ.TroubleDialogFactoryTestクラスを書こう.最初は,故障1件でダイアログが画面中央に表示されればよい.画面中央の位置を知るには,スクリーンサイズを知る必要がある.調べてみると,java.awt.Toolkitクラスを使えばできそうだ.

import junit.extensions.converter.*;
import java.awt.*;

public class TroubleDialogFactoryTest extends ConverterTestCase {
  public TroubleDialogFactoryTest(String name) {
    super(name);
  }
  public void test() throws Exception {
    TroubleDialogFactory factory = new TroubleDialogFactory(new Frame());
    TroubleRecord record = new TroubleRecord();
    Dialog dialog = factory.create(record);

    Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
    Point expected =  new Point(screenSize.width / 2 - dialog.getWidth() / 2,
                                screenSize.height / 2 - dialog.getHeight() / 2);
    converter = new BeanValueConverter(converter, Dialog.class, "location");
    converter = new ClassFieldValueConverter(converter, "{0},{1}",
                                             Point.class, "x", "y");
    converter.assertEquals(expected, dialog);
  }
}

上のコードでは,まずダイアログの親ウィンドウとなるFrameオブジェクトでTroubleDialogFacotryを生成する.次に故障を表すTroubleRecordオブジェクトを作り,Factoryのcreateメソッドでダイアログを生成する.このダイアログの位置をテストすればよいわけだ.

ダイアログ位置の期待値を求めるため,Toolkit.getScreenSizeメソッドでスクリーンサイズを取得する.スクリーンサイズとダイアログの大きさから,画面中央にダイアログがある場合の左上の座標(Dialog.getLocationの返り値)を求め,expected変数に代入する.最後のconverterは,DialogをgetLocationの返り値に,Pointをx, yのフィールド値に変換するコードだ.

テストコードができたので,適当にTroubleDialogFactory.createメソッドを書いて失敗させる:

There was 1 failure:
1) test(TroubleDialogFactoryTest)
   junit.framework.AssertionFailedError:expected:<640,512> but was:<0,0>

何も設定しなければダイアログの位置は0, 0になっているようだ.今度はテストが通るように実装してみよう:

import java.awt.*;

public class TroubleDialogFactory {
  private Frame parent;
  public TroubleDialogFactory(Frame parent) {
    this.parent = parent;
  }
  public Dialog create(TroubleRecord record) {
    Dialog dialog = new Dialog(parent, "故障ダイアログ");
    Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
    Point location = new Point(screenSize.width / 2 - dialog.getWidth() / 2,
                               screenSize.height / 2 - dialog.getHeight() / 2);
    dialog.setLocation(location);
    return dialog;
  }
}

なんと,ダイアログの位置を求める部分がテストコードと同じになってしまった!――これがトートロジーのアンチパターンと呼んでいるものだ.テストコードと実装コードが同じになっては,そもそもテストする意味がない.同語反復で何もテストしていないのだ(このパターンは,Kent Beckのいう"Fake it"とは異なる.上のコードは実装として正しくみえるからだ).

トートロジーのアンチパターンを解決するには,「テストコードには特殊解,実装コードには一般解」ということを思い出せばよい.つまり,特殊解が設定できるクラス設計にしてしまうわけだ:

public void test() throws Exception {
  TroubleDialogFactory factory = new TroubleDialogFactory(new Frame());
  factory.setScreenSize(new Dimension(600, 400));
  factory.setDefaultDialogSize(new Dimension(200, 100));

  TroubleRecord record = new TroubleRecord();
  Dialog dialog = factory.create(record);
  converter = new BeanValueConverter(converter, Dialog.class, "location");
  converter = new ClassFieldValueConverter(converter, "{0},{1}",
                                           Point.class, "x", "y");
  converter.assertEquals("200,150", dialog);
}

上のコードでは,TroubleDialogFactoryにスクリーンサイズとダイアログサイズを設定できるようにした.こうすれば,特殊解がはっきりわかるテストコードになり,トートロジーは消える.さらにその特典として,以後のテストケース(複数の故障が出た場合のテスト,ダイアログ数が上限値を超える場合のテスト)が格段に書きやすくなる.これらはテストのためだけに導入されたメソッドだが,テストを書きやすくし,コードの信頼性を上げるには必要なメソッドなのだ.

ここで,実際にスクリーンサイズやダイアログサイズを設定しない場合のテストはどうするのか,という疑問を持つ人もいるかもしれない.しかし,それは受け入れテスト(といっても自動化する必要はない)で確認すればいいだけの話だ.そもそもJUnitのコードですべてをテストできないし,実際に動かさないと絶対に信頼はできない.

トートロジーのアンチパターンは,「実装の詳細をテストする」というアンチパターンと共に現れることも多い.JUnitを使い始めたころ,SQL文を直接テストするテストコードを書いていたことがある.検索条件からくるSQL文のいろんなパターンに対応しようとすると,どうしてもテストコードが一般的になり,テストコードと実装コードが同じになってしまう.これは,SQL文という実装の詳細をテストしようとしたことからくる誤りだ.このあたりの話は,XP をはじめよう:トートロジーになってしまったテストにも少し書いた.このときは,特殊解を設定しやすいクラス設計で解決したわけではなく,テスト対象をブラックボックス化し,外側からテストを行うことでトートロジーをなくした.

XPにはYAGNIという過度の機能拡張を戒めるルールがある.しかし,一方でテストコードから元の仕様では必要のない機能拡張が求められることも多い.今回のようなメソッド単位の小さなものから,モデルとビューを分離するというアーキテクチャレベルのものまで,様々なものがあるようだ.この辺のバランスがXPのコードの品質を上げるポイントなのかもしれない.

| | コメント (0) | トラックバック (0)

2004.02.22

開発環境としてのMake

ワークフローを無視したシステムは使いにくい.いくらデータモデルがしっかりして見栄えがよくても,ユーザが行う作業に合ってないのは良くないシステムだといえる.これは業務システムだけでなく,プログラマの開発環境にも当てはまることだ.

例えばXPlannerというツールがある.XPのストーリー管理を行うツールだ.ストーリー管理はXPの重要項目の一つなので,毎回新しいプロジェクトが始まるたびにXPlannerの導入を検討する.だが,いつも少し使うだけでやめてしまう.確かに見栄えはいいのだが,ストーリーの編集,見積もりと実工数の入力,どれも自分のスタイルに合っていない.XPlannerが想定しているワークフローと自分が普段行っているワークフローが違うのだ.だから,ストーリーの管理にはいつも自前のツールを使っている.

IDEについても同じことがいえる.もっとも,プログラマが行う作業(ビルド,実行,テスト等)はそんなに変わらないため,それほどXPlannerのような使いにくさはない.しかし,開発作業にはプロジェクト特有のものも含まれている.そういったものをどんどんカスタマイズして開発環境を改善していきたい.だが,IDEの仕組みによってはカスタマイズの余地がなかったり,あったとしてもプラグインの作成などかなりの労力が必要だったりする.

一方,Makeは自由にカスタマイズ可能な開発環境が構築できるツールだ.必要に応じてMakefileにターゲットを追加していくだけでどんどん使いやすくなる.コマンドラインで動くツールがあれば,それらを組み合わせてよりいっそう便利な環境に育てていける.つまり,自分のワークフローに合うように作業を自動化し,開発スピードを上げていくことができるのだ.

例えば,あるファイルサーバに置いてある仕様書(Excelファイル)を見ながら開発する必要があったとしよう.このとき,別のSEがいて,もっぱらこのExcelファイルを編集して仕様書を作成していたとする.ファイルをそのまま開いてしまうと仕様書を編集しているSEが保存できなくなるので,このファイルを一旦ローカルにコピーして開くか,そのファイルを読み取り専用で開かなければならない.こういう場合,MakefileにExcelを読み取り専用で開くコマンドを書く.ターゲット名はview-specにしておこう:

view-xls = ruby -e"\
  require 'win32ole';\
  excel=WIN32OLE.new('Excel.Application');\
  excel.visible=true;\
  excel.workbooks.open('$(1)',{'ReadOnly'=>true})"

spec-xls = \\server\share\仕様書.xls

view-spec:
	$(call view-xls,$(spec-xls))

view-xlsは,RubyのWin32OLEを使ったExcelファイルを読み取り専用で開くファンクションの定義だ.$(call view-xls,[Excelファイル名])でファイルをオープンできる.こうしておけば,

$ make view-spec

とするだけで仕様書を見ることが可能だ.わざわざ共有フォルダにあるファイルをコピーし,マウスをダブルクリックして開く必要はない.

さて,IDEを使っている人はMakeのターゲットをIDEのメニューコマンドと考えればわかりやすいと思う.ビルド,リビルド,テストなど,プロジェクトメニューあるコマンドは,

$ make build

$ make rebuild

$ make test

というターゲットが対応する.また,IDEでは編集中のファイルに対するコマンドもある.こういうコマンドに対応するのがサフィックスルールだ.例えば,Foo.javaを実行したい場合,Makefileに

%.run:build
	java $*

と書いておけば,

$ make Foo.run

とするだけで実行してくれる(もっとも,Fooすら入力が面倒なのでEmacs-lispでmake Foo.runと展開するコマンドを作ることも多いが).

このように,Makefileは自分に合ったメニューコマンドをどんどん定義していくようなものだ.IDEに比べて不便なのは,複数ディレクトリにわたるプロジェクトの扱いが面倒なことと,メニュー一覧を出すことができないことだろう.しかし,コマンドラインで実行できるツールがあればなんでもできるのはすごく便利だ.

Javaの開発だけならEclipseの方がいいだろうが,Makeを使いこなせればどんな言語でも対応できてしまうし,ビルド前にコードジェネレータをはさんだり自由自在だ.Antはソフトウェアのデプロイやパッケージングには適しているかもしれないが,開発環境としての視点が抜けている.だから日々の開発にAntは使えない.僕にとって,Makeはこれからもずっと手放せられないツールになるだろう.

| | コメント (0) | トラックバック (0)

2004.02.15

オブジェクト生成のUniform Access

人を表すクラス:Personがプロパティ:age(年齢)を持つとしよう.このとき,ageにアクセスするメソッドの実装は,毎回誕生日から計算する方法と,年齢を保持するフィールド:ageの値をそのまま返す方法などが考えられる.どちらを選ぶかはPersonクラスの実装者に委ねられており,ageプロパティの実装によってアクセス方法が変わるようなことがあれば,Personクラスのモジュール性は著しく悪くなる.つまり,オブジェクトのプロパティは,実装によらず同じようにアクセスできること――これがBertrand MeyerのUniform Access Principleだ.

Uniform Accessの観点から,Javaはあまりいい言語とは言えない.プロパティごとにgetter, setterメソッドを作る必要があるからだ.一方Curlでは,その辺りはきちんとサポートされている.先ほどのPersonクラスは,フィールドを使って次のように実装できる:

{define-class Person
  field public-get private-set age:int
  ...
}

上の例では,int型のageフィールドを宣言し,アクセス修飾子としてpublic-get, private-setを使っている.こうするとageは読み取り専用フィールドとなる.get, setごとにアクセス指定できるのが面白いところだ.このとき,Person.ageプロパティのアクセス方法は次のようになる.

{value
  let person = {Person}
  person.age
}

変数:personにPersonクラスのインスタンスがセットされ,person.ageが評価されてブラウザに年齢が表示される.

次に年齢を誕生日からの計算で求めたくなったとしよう.このときgetter宣言をすればUniform Accessを壊さないageプロパティの実装が可能だ:

{define-class Person
  {getter public {age}:int
      || ここで年齢を計算
  }
}

このように,Curlではgetter宣言をすることでUniform Accessを壊さず実装できる.なお,同じようにsetter宣言があることを補足しておこう.

さて,ここまではVisual BasicやDelphiなどにも実装されており,特筆すべきことはない.Curlが優れているのは,Uniform Accessをオブジェクト生成にまで拡張していることにある――つまり,ファクトリーと呼ばれる機能だ.

一般に,クラスの利用者がインスタンスを取得するには次の2つの方法があるだろう:

  • コンストラクタを直接利用する
  • ファクトリーメソッドを利用する

この機能は,上のどちらの場合でもアクセス方法を同じにできる,という仕組みだ.つまり,呼び出し側はオブジェクト生成がコンストラクタによるものなのか,ファクトリーメソッドによるものなのかを気にしなくてよい.ファクトリーを使えば,オブジェクトの生成方法を自由にオーバーライドできる.そういう意味でこれは画期的な機能だ.

ファクトリー自体は通常のファクトリーメソッドの作り方と同じだ.そのクラスまたはサブクラスとなるオブジェクトを生成し,適当な初期化を行った後でそのオブジェクトを返せばよい.例えばSingletonパターンをファクトリーで実装すると次のようになる:

{define-class Singleton
  let instance:Singleton = {Singleton.create}
  {constructor private {create}
  }
  {factory {default}:Singleton
      {return Singleton.instance}
  }
}

上のコードで,instanceはSingletonオブジェクトを保持するクラス変数だ.createという名前のprivateなコンストラクタを使ってこれを初期化する.次のfactory宣言の本体では,もっぱらこのinstanceを返しているだけだ.こうすることで,次のコード:

{value
    let s1 = {Singleton}
    let s2 = {Singleton}
    s1 == s2
}

はtrueと表示される.一見Singletonオブジェクトを2つ作っているように見えるが,実はs1, s2は同じオブジェクトを参照しているのだ.

さて,オブジェクト指向でよく問題になるのが,オブジェクトを生成する側のコードに具象クラスのクラス名を書かなければならない,ということだ.GoFのデザインパターンのうち,オブジェクト生成に関するものはこの問題を解決するために用意されているといってよい.Curlでは,抽象クラスでもfactory宣言が可能なので,利用者側のコードに具象クラスを全く書かないコーディングも実現できる.詳しく調べたわけではないが,ファクトリーを使えばいくつかのパターンは必要なくなるだろうし,SingletonパターンやFlyweightパターンをもっとスマートに実装できるだろう.

Curlの仕様書でファクトリーを見たとき,かなり感心したと同時になぜ今まで思いつかなかったのだろうとくやしい思いをした.Uniform Accessを他のものに適用すれば,面白いことが見つかるかもしれない.

| | コメント (0) | トラックバック (0)

2004.02.08

コンテンツ言語Curl

Curlはリッチクライアント・Webアプリ用プログラミング言語として知られている.しかし,日本ではサーバーライセンスや開発環境が高価なことから,ほとんど普及してないのではないだろうか.ただ,日本語版は有料だが英語版はフリーで試せるようだ:

http://www.curl.com/
(※このサイトでは日本と韓国はダウンロード禁止と書いてあるが,個人/評価用ならかまわないらしい:http://www.curlap.com/html/about/pressroom/ide300.htmを参照)

Curlは一見するとHTMLとスタイルシートを発展させた言語のように見える.例えば,ソースコードとして

Hello {bold World}!

と書けば,

Hello World!

のようにCurlプラグインがインストールされたWebブラウザ上で表示される.おそらくコンテンツの中に中括弧でコードを埋め込めることからコンテンツ言語とよばれているのだろう.これならWebアプリを作らなくても,コード・ジェネレータに利用できるのではないかと考えたが,コマンドラインで動くツールとしては機能が弱いためメリットはないようだ.

ここまで読むと,JavaScriptのようなWebに特化したスクリプト言語,というイメージしか持たない人も多いと思う.ところが,プログラミング言語,特にオブジェクト指向の観点からみてCurlはかなりすごい言語だ.実際次の3つの機能があるということを知って非常に驚いた.

  • 多重継承
  • パラメトライズド・クラス
  • マクロ

C++とlispの影響を受け,いいとこ取りをしているようにみえる.lispといえば括弧アレルギーの人も多いと思うが,適度に括弧を省略した文法を採用しているためそんなにひどいことはない.

上記3つのうち,多重継承については説明するまでもないと思うが,2番目のパラメトライズド・クラスは次のような感じだ.

{let pi-numbers:{Array-of int }=
    {{Array-of int} 3, 1, 4, 1, 5}}

{let numeral:{HashTable-of int, String} =
    {{HashTable-of int, String}
        1, "one", 2 "two", 3, "three"}}

最初のコードでは,intの配列({Array-of int}):pi-numbersを宣言し,初期化している.次のコードでは,キーがint,値がStringのハッシュ({HashTable-of int, String}):numeralを宣言し,同じく初期化を行っている.パラメトライズド・クラスをユーザが定義するのも簡単だし,any型(プリミティブ,オブジェクト型すべてを含む総称型)が使えるため,型付けの弱いスクリプト言語のような柔軟性をもつパラメトライズド・クラスを定義することも可能だ.

一番すごいといえるのがマクロ機能だ.マクロを一言でいえば,ユーザがCurlの文法をどんどん拡張していけるということだ.マクロの定義では,まずパターンと呼ばれるトークンの並びを宣言し,本体に入れ替えるべきコードを書く.僕も実際にテストケース用のマクロを作成してみた:

{testcase {SampleTest}
  {test-method {add}
     {assert-equals 1+1, 2}
  }
}

上記の中で,testcase, test-method, assert-equalsはすべてマクロだ.これらはTestCaseを継承するクラスやテストメソッドに展開される.マクロを定義するのは正直頭がこんがらがってくるが,かなり有用な機能であることは確かだ.

他にも,ファクトリやオプション,関数の引数の扱いなど他の言語にはないような面白い機能があるが,機会があれば別のときに紹介しようと思う.

さて,ここまではCurlをほめるだけだったが,気に入らないところもあるので書いておこう.

まず一番イヤなのがJavaにおけるクラスパスのような概念がないことだ.Curlでは,パッケージに含まれるファイルやライブラリのファイル名をすべて明示的にコードに書かなければならない.つまり,コマンドライン引数や環境変数で外側から教えてやることができない,ということだ.新しいCurlのファイルを作れば,必ずそのファイル名をコードのどこかに書かなければならない.本来make等ビルドツールに任せることまでCurlでやってしまうところが墓穴をほっている気がする.

また,内部クラスがないことも痛い.UnitTestでは,MockObjectを作るために内部クラスを使う.こうすると名前空間が汚染されないからだ.詳しいことは省略するが,先ほど書いたクラスパスがないことと合わせてUnitTestを行うのを非常に困難にしている.後発の言語なら,もっとUnitTestしやすい設計をしてほしかったところだ.

最後に,一番イライラすることを書いておこう.冗長なカンマが許されないことだ.次のコードは,5の後ろにカンマがあるのでコンパイルエラーになる:

{let pi-numbers:{Array-of int}=
    {{Array-of int} 3, 1, 4, 1, 5, }}

これはすぐにでも変更してほしい.本当にイライラするので.

| | コメント (0) | トラックバック (1)

2004.02.01

テストファーストの判断基準

テストファーストが難しい分野にGUIプログラミングがある.この分野には,JUnitの拡張としてJFCUnitやAbbotなどのテスティングフレームワークが用意されているようだ.

数年前にGUI部品を提供するライブラリの保守・機能追加でJFCUnitを使っていたことがある.キー入力やマウス入力をJFCUnitやjava.awt.Robotを使って書くのは難しかったが,おおむねうまくいったように思う.特によかったのは,Java本体のバージョンアップによるバグが一瞬にして見つかったことだ.以前のJavaならテストがすべて通るのに,Javaを新しくするだけでテストが失敗してしまう.すぐJava本体のコードを調べると,フォーカス周りの実装ががらっと変わっている.そこに依存していたコードが原因でライブラリがバグっていたのだ.

その後GUI部品の仕事が終わり,Swingを使ったGUIアプリの仕事になった.このときにはすでにJFCUnitに慣れていたし,ある程度成功を収めていたからGUI部分もテストファーストでやっていけるだろうと思っていた.納期は厳しかったが,ペアプロでJFCUnitの使い方を教えていけばなんとかなるだろうと考えたし,ノウハウがたまれば「JUnit実践講座」に記事を追加してもいいだろうとさえ考えていた.――しかし,それがかなり甘い考えだったことがわかった.

始まってから1ヶ月経っても開発がうまく軌道に乗らないのだ.XPは最初のイテレーションが一番難しい.でも数回イテレーションをまわせば開発スピードが加速度的によくなってくる.それが普通だ.なかなかストーリーが終わらないのはどこか間違ってる.その原因がJFCUnitを使うことのように思えてきた.JFCUnitには,次のような問題がある.

1) テストケースを書くのが難しい

テストファーストでテストを書こうと思っても,テストコードが複雑になりすぎてテストを書くだけで力がつきてしまう.またJFCUnitのテストは単体テストというより受け入れテストに近いので,「テストコードがクラスの仕様書」というテストファーストのメリットを享受しにくい.

2) テストを自動化してもバグが見つかりにくい

(1)と関連するが,JUnitで自動化するといくつかの簡単なパターンのテストで手一杯になってしまう.テストは通っても実際に手で行うといろいろバグが見つかる,ということがよくあった.

3) 同期をとるのが難しい

JFCUnitにはGUIの同期をとるためにawtSleepというメソッドがある.実際にはうまく同期がとれなかった.まだ画面の準備ができてないのにassertメソッドが先に動き誤った結果を評価してしまう.本当はテストが通っているのに失敗する,ということが頻繁に起こってしまった(これは,自分の技術力不足のせいなのかもしれない).

GUIアプリではなくGUI部品の開発でうまくいったのは操作が限定されていたからだろう.GUI部品で複雑なユーザ操作をシュミレートする必要は無い.単にボタンをクリックするだとか,キー入力でフォーカスを移動するだとかその程度だ.メニューを選択し,ダイアログを出して,「追加」ボタンを押してキー入力し,「OK」ボタンを押してダイアログを閉じ,効果を確認する…なんていう複雑な操作は必要ない.こんなことをテストファーストでコードを起こすなんてとんでもない話だ.

ここまで考えた後でメンバーに方針を伝えた.すなわち,「基本的にGUIではテストコードを書くな」と(正確には,「5分考えてダメだったら」という条件つきだったが).メンバー間で「GUIテストは書かない」という方針が行き渡ると開発スピードがどんどん上がっていき,XP本来のドライブ感が取り戻せた.もちろん,GUI周りのバグは残ってしまうが,テストコードをばっさり切ってしまうことでXP本来のリズムに乗ることができたのだ.

さて,ここまでGUI開発でのテストファースト成功例,失敗例を紹介した.一言でいうならこの違いは「リズム感」にあると思う."test a little, code a little"のリズム感だ.失敗した事例はtest a littleで無かった.テストコードを書く負担があまりにも大きく,実装コードを書く余裕がなくなっていたのだ.

テストファーストするかどうか考えるときは,このリズム感が判定基準になると思う.テストファーストすることでリズムが狂ってしまうのなら,それは間違っている.大事なのは,テストファーストを守ることではなくリズムに乗ることだ.テストファーストより,リズムの方が大事だということを認識しておく必要がある.テストが負担になっているのなら,それを軽減するフレームワークが提供できないかも考えておかなければならない.

| | コメント (0) | トラックバック (1)