2011年12月22日木曜日

WYDKYDK が読めたよ! ワーイヾ(´ρ`)ノ゛ワーイ

Alistair Cockburn の本に出てくる変な綴りの言葉。意味は分かるけどなんて発音するのか、ずっと分からなかったけど、何の気に無しにググってたら見付けた。

なんかソフトウェア開発とは全然関係ない、オレゴン州の自治会か何かの議事録らしき文書だけど、WYDKYDK ("wid-kee-dik")とある。カタカナにするとウィドキーディクみたいな感じだろうか。

綴りが珍しいのは略語だからで、もとはこんな文。
What you don't know you don't know

簡単な英語だけど、構文が分からない人が意外と多い。順を追って見てみると、

You don't know it.
あなたはそれを知らない
What you don't know.
あなたが知らないもの
You don't know you don't know it.
あなたはそれを知らないという事を知らない
What you don't know you don't know.
あなたが知らないという事を知らないもの
といった感じで、入れ子になってたりして、プログラマには親和性が高い気がするんだが。

ところで、現場ではこの WYDKYDK への認識が足りない人が、例えば WBS を作ったり作らせたりすると、困った事になる。

「これだけ詳細化したんだから、見積りは正確なはずだ」なんて思考様式なんだけど、そもそも認識されていない事を詳細化しようもないし、予測に組み込みようもない。

正解は、何らかの形で現実に実行してみて「知らない事」を「知らなかった事」に変えていく事なんだけど、下手な人はこの当たり前の事ができず、日程にバッファを組み込むといった程度の、稚拙な手に走ってしまう。 まあ現場ごとの文化とか、プロジェクトごとの事情とかもあるけどね。

2011年12月21日水曜日

小事の思案と割れ窓理論

葉隠と言えば、「武士道といふは死ぬ事と見付けたり…別に仔細なし。胸すわって進むなり。」で有名だけど、正直、自分みたいな「すくたれもの」には難しい。

だけど実は葉隠には、そんなムリなやつばかりじゃなくて、普通に実践的なアドバイスも結構ある。

『大事の思案は軽くすべし。/小事の思案は重くすべし。』

これについて自分は、やっぱり開発者で飯を食っているせいか、前半をリスク管理の問題、後半を品質管理の問題と捉えてしまう。

前半の『大事の思案』がなぜ軽いかについては、非常にクリティカルな事態に関しては、いざというときに判断に迷って対応が遅れたりしないように、普段から方針を定めておいて、即断即決できるように備えておくべきだという解釈になる。

後半の『小事の思案』がなぜ重いかについては、三島由紀夫が葉隠入門の中で、アリの穴から堤防が崩れるように、瑣末なことを軽んじる事によって生活の体系や重大な思想までもが台無しになると解説している。

で、もちろん前半も後半もどちらも大事なんだけど、どちらかというと自分なんかには後半の方が、より切実にピンと来る。(管理の比重が大きい人は前者かもしれない。)

例えば、コーディング規約なんかみたいなものでも、「ぎりぎり読めりゃいいじゃん」みたいに適当にしないで、きっちり律儀に守ってかないといけない。と自分は思う。

そこを疎かにするから、規約違反の放置->->内部品質の低下->->外部品質の低下->->製品品質の低下 ってな感じで、結局、また地獄になってくんじゃないかと。


で、この小さい事こそ大事ってのを、別角度から現代風に理論化したのが、割れ窓理論だと思う。

一般には、ジュリアーニ市長の時代にニューヨーク市が治安を回復したときにベースになった理論として有名なアレだけど、Wikipedia によるとこうなる

「建物の窓が壊れているのを放置すると、誰も注意を払っていないという象徴になり、やがて他の窓もまもなく全て壊される」

ささいに見える Duplicate code の放置が、コピペだらけの巨大な糞コードの山に繋がっていくという、まあ、ありがちなあれと同じじゃないだろうか。

で、この割れ窓理論をベースにした政策が「ゼロトレランス」だけど、開発現場に当てはめてみると、さしずめ Refactoring Mercilessly といったところか。

なんて事を考えていると、例えば、「途中return は是か非か」みたいな、一見不毛でアホらしい事についてムキになって議論するのも、けっこう大事というか、度を越さない範囲ならむしろ健全だとも思えてくる。もちろん自分もわりとムキになる方だ。

2011年12月20日火曜日

Spring 3.1 の Cache Abstraction

Spring 3.1 が GA になった。最後に Spring を触ったのはこれを書いたときだから、もう2年近く前になるけど、こんときは 3.0 が出た頃だった。あんまり進んでなかったのかも。

いろいろググってみると、キャッシュがどうかしたとか書いてあるので、寝る前にちょっと試してみる。

package spring.trial1.ex1;

import java.util.HashMap;
import java.util.Map;

import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

class Bar {
   Map<Integer, String> map = new HashMap<>();
   {
      map.put(1, "one");
      map.put(2, "two");
   }
   @Cacheable("bar")
   public String findString(int key) {
      System.out.printf("findString(%d) called%n", key);
      return map.get(key);
   }
}

public class Foo {
   public static void main(String[] args) {
   ApplicationContext ctx = new ClassPathXmlApplicationContext(
         "applicationContext.xml");
   Bar bean = ctx.getBean(Bar.class);

   System.out.println(bean.findString(1));
   System.out.println(bean.findString(2));
   System.out.println(bean.findString(3));
   System.out.println(bean.findString(1));
   System.out.println(bean.findString(2));
   System.out.println(bean.findString(3));
   }
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:cache="http://www.springframework.org/schema/cache"
  xmlns:p="http://www.springframework.org/schema/p"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">
  <cache:annotation-driven />

  <bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
    <property name="caches">
      <set>
        <bean
          class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"
          p:name="bar" />
      </set>
    </property>
  </bean>
  <bean id="bar" class="spring.trial1.ex1.Bar"/>
</beans>
pom.xml の dependency に spring-context 3.1.0.RELEASE と、cglib 2.2.2 を指定して、ビルド→実行したら、以下のような結果が得られる。 (既に Maven Central に入ってるらしく、特に repository を指定したり、ローカルリポジトリに入れたりとかしなくても、普通に 3.1 がダウンロードされる。)
findString(1) called
one
findString(2) called
two
findString(3) called
null
one
two
null

findString の 中身が呼ばれるのは最初だけで、2回め以降はキャッシュが使われているらしいのが分かる。

■ 雑感

JavaConfig/JSR-330 で xml を書かずに済むようになったと思ってたけど、この <cache:annotation-driven> に関しては、やり方が分からない。

仕方なく、昔ながらのapplicationContext.xml を書いてしまった。もしかすると、ちゃんとしたやり方があるのかもしれないけど、部分的にアノテーションで部分的にXMLみたいにマジでなるとしたら、結構イヤかも。

2011年12月18日日曜日

gtk2hs で落書きしてみる

前回の書き込みで、gtk2hs の準備ができたので、もう一歩進めてみる。

10年以上前、Visual C++を使ってた頃、MFC のチュートリアルで Scribble というのがあった。これを、Haskell で試してみた。

ただし、MDI とか ドキュメントの保存とかは割愛して、とりあえず描画の API とマウスイベントのハンドリングだけを気にすることにした。

仕組みは簡単で、マウスボタンが押下されたときから放されるまでのマウスポインタの軌跡を記録し、ウィンドウ描画時に各点を線でつないでいくというもの。コードは以下のようになる。

import qualified Graphics.UI.Gtk as G
import Graphics.UI.Gtk.Gdk.GC
import Graphics.UI.Gtk.Gdk.Events
import Data.IORef
import Graphics.UI.Gtk.Gdk.Drawable

main = do
    G.initGUI
    w <- G.windowNew
    G.onDestroy w G.mainQuit

    da <- G.drawingAreaNew

    isDrawingIORef <- newIORef False
    figuresIORef   <- newIORef [[]]

    G.onExposeRect da (const $ do dw <- G.widgetGetDrawWindow da
                                  gc <- gcNew dw
                                  f <- readIORef figuresIORef
                                  drawFigure dw gc f)

    G.onMotionNotify da True $ \Motion {eventX = x, eventY = y}-> do
      isDrawing <- readIORef isDrawingIORef
      case isDrawing of
        True  -> do  modifyIORef figuresIORef (\f->addPoint f x y)
                     G.widgetQueueDraw da
        _     -> return ()
      return True
  
    G.onButtonPress da $ \Button {eventX = x, eventY = y}-> do
      writeIORef isDrawingIORef True
      modifyIORef figuresIORef (\f->[point x y]: f)  
      return True

    G.onButtonRelease da $ \Button {eventX = x, eventY = y}-> do
      writeIORef isDrawingIORef False
      modifyIORef figuresIORef (\f->addPoint f x y)
      return True
  
    G.containerAdd w da
    G.widgetShowAll w
    G.mainGUI

  where
    point x y = (round x, round y)
    addPoint figures x y = (point x y: head figures): tail figures
    drawFigure _ _ [] = return ()
    drawFigure d g (f:fs) = do {
      G.drawLines d g f; drawFigure d g fs }

Haskell 自体にまだ慣れていないので、いかにも未熟な感じだけど、イベント処理と描画の作法がなんとなく分かってきた。ネット上で API を調べる事にも、だんだん慣れてきた。

ただ、IORef というのが、なんだかしっくり来ない。結局、状態を持ってしまってる事になり、どうも気持ち悪い。初心者なりに勝手に純粋関数型言語に期待していたのは、もっと、引数と戻り値だけでつながっていく感じなんだけど、まだまだ勉強が足りないのかもしれない。

結果としては、マウスボタンの判別をしていないので、右でも左でも書けてしまうけど、一応思ったとおりに動作するものができた。スプーの絵もこんな風に書く事ができる(頭頂部の突起がやや足りない事を除けば、我ながらよく描けたと思う)。

gtk2hs と Glade の相性

gtk2hs を Glade と組み合わせて使うときのメモ。

Haskell の GUI プログラミングを試したくて、テキストボックスに文字列を入れて Enter を押したらラベルに表示されるような簡単なプログラム echo を、gtk2hs を使って書く実験をしてみた。

で、まず Glade というツールで GUI を定義する XMLを生成して、これを適当に書いた Haskell プログラムに読ませたら、こんなエラーが出た

$ ./echo 

(echo:4887): libglade-WARNING **: Expected <glade-interface>.  Got <interface>.

(echo:4887): libglade-WARNING **: did not finish in PARSER_FINISH state
echo: user error (Pattern match failure in do expression at echo.hs:8:5-12)

なんか Glade が生成した GUI定義XML の形式に問題があるっぽい。

調べてみると、Glade の出力形式には libglade と GtkBuilder の二通りの方法があり、gtk2hs が対応しているのは前者という事らしいのだが、ファイルを保存するときに libglade を選んでも、事態は全然変わらない。

さらに調べてみると Glade の3.8系 と 3.10系 で大きな違いがあって、出力XML に関してだと 3.10系では libglade 形式が無くなってるらしい。自分は、特に意識しないまま 3.10 を使っていた模様。

この2つのバージョン間で、保存時のダイアログに以下のような違いがある。

3.10系
3.8系
XML出力の違いは以下のような感じ
3.10系
<?xml version="1.0" encoding="UTF-8"?>
<interface>
  <requires lib="gtk+" version="2.24"/>
  <object class="GtkWindow" id="window1">
    <property name="can_focus">False</property>
    <child>
    …
3.8系
<?xml version="1.0" encoding="UTF-8"?>
<glade-interface>
  <widget class="GtkWindow" id="window1">
    <property name="can_focus">False</property>
    <child>
    …

というわけで、Glade の 3.8 系を使えば、gtk2hs がちゃんと処理できる形式の XML になる。

ただし、うちの環境で使ってる Fedora16 なんかでは、「ソフトウェアの追加/削除」から普通に Glade をインストールすると 3.10系が入って来てしまう。従って、この場合 3.8系を別途インストールする必要がある。

これは、ここから glade3-3.8.1.tar.xz を落としてきて、tar Jxf glade3-3.8.1.tar.xz -> ./configure -> make -> make install のようにすれば、/usr/local/bin/glade-3 が使えるようになる。

ちなみに、以下が実験で使った Haskellコード

module Main where

import Graphics.UI.Gtk
import Graphics.UI.Gtk.Glade
import Graphics.UI.Gtk.Gdk.Events as Evt

main = do
    initGUI
    Just xml    <- xmlNew "echo.glade"
    window      <- xmlGetWidget xml castToWindow "window1"
    onDestroy window mainQuit
    label       <- xmlGetWidget xml castToLabel "label1"
    entry       <- xmlGetWidget xml castToEntry "entry1"
    onKeyPress window $ \(Evt.Key _ eventSent _ _ _ _ _ _ name _) -> do
      keyPressHandler label entry name
      return eventSent
    widgetShowAll window
    mainGUI

keyPressHandler :: Label -> Entry -> String -> IO ()
keyPressHandler label entry "Return" = do
  name <- get entry entryText
  set label [labelText := name]
keyPressHandler _ _ _ = return ()
たかだかこれだけのコードを書くのに、かなり骨を折るが、いちおう動作する。

2011年12月14日水曜日

現場でよくある Agile 認識の齟齬

現場で、開発プロセスをどうしましょうかなんて話をしていると、対話の流れを止めて指摘するほどではないけど、認識の違いが引っかかって嫌なときがある。今日は、そんな事象のメモ。

★ Agile プロセスはユルいものだという認識

ユルいキビしい
なんか違うAgile全般ウォーターフォール
たぶんこうAdaptive Software Development、Crystal ClearXP、Scrum、ウォーターフォール

Agile が総じてユルいのではなく、実はユルいのもキビしいのもある。この見方は、確かものすご〜く前に読んだ Cockburn の本に載ってたものだけど、仮に読んでなかったとしても同じ認識になってた気がする。

ちなみに、同じ High-Discipline プロセスの中でも、XP と ウォーターフォールでは、前者が高効率・高難度で後者が低効率・低難度って事になると思う。寛容な方のグループの Crystal Clear なんかは、ユルい分、XP のような究極的な生産性は得られないけど、その分、負担が少ないって事になると思う。

★ 反復型といえば軽量プロセスだという認識

軽量重量
なんか違う反復型プロセス全般ウォーターフォール
たぶんこうAgile系UP系(AgileUPは除く)、ウォーターフォール

反復型のプロセスはどれもみな軽量だという思い込みもよくあるけど、たいてい「軽量プロセスはドキュメントを作らない」ってドクサも併発してるから、とりあえず反復型にしてみませんかなんて提案しても、習慣による心理的抵抗が大きい。

ウォーターフォール色の強い組織でも、ドキュメント体系を差し当たり尊重したまま、やや長めの期間の反復型プロセスに切り替えた上で、反復型が浸透してきた頃合いを見て、段々と期間を短くしたり文書や儀式を減らして、段階的に軽量にしていくというやり方もある。

2011年12月13日火曜日

Alloy 本

土曜に Amazon のお急ぎ便で注文したら、日曜の夜に届いた。 Alloy本
日曜は暇がなかったので、今日、帰宅してから早速 Alloy Analyzer をダウンロードして、テキストに取りかかる。(単なる jar なので java -jar ですぐ動く。)

下図の左側ペインのようにコードを書き込んで、[Execute]>[Show Metamodel]を実行すると、、、

、、、このようなダイアグラムが得られる。

このダイアグラムは、四角形の配置を変えたり、色やフォントを変えたりして、ある程度見せ方を好きなように変えられるようだけど、どうもダイアグラムはそれほど本質的なものではないっぽい。

まあ、1時間ほどいじっただけだし何も理解できてないけど、上の上の図で左側のペインに書いてある Alloy コードがやっぱり主役っぽい。
これに述語(pred)やファクト(fact)をちょっと書き足しては、それに対してアサーション(assert)をちょっと付け加えて、そしたら実行して右側のペインで確認して、って流れになる模様。そうだとしたら、どこか TDD に似ていてとっつきやすい。

とは言っても、関心の重点は実装ではなくむしろ仕様であって高い抽象レベルで仕様をモデリングする事が目的の技術らしい。
ちなみに、抽象度をレベル分けするときに、伝統的に分析レベル、設計レベル、実装レベルなんて言ったりするけど、Alloy と同じ雰囲気で、なおかつレベルの違う他言語を持ってくると、分析レベル:Alloy、設計レベル:LePUS3、実装レベル:Coq てな感じに当てはまるんじゃないかと思う。

まあ、いきなりいろんな事をやり始めなくても、とりあえず Alloy だけでも、例えば金融システムのビジネスルールとかに適用して、仕様バグを取り除くような事は、結構すぐにでも始められそうな予感。

2011年12月11日日曜日

Haskell+HDBC+MySQL で Hello World

Haskell で MySQL を使ってみるウォーミングアップ。

こんな環境
  • Fedora16
  • GHC 7.0.4
  • MySQL 5.5.18

====

まずテーブルを準備する。適当に mysql で MySQL に接続して以下のようにする。

mysql> create database hdbc_test;
mysql> connect hdbc_test;
mysql> create table greeting (id int not null auto_increment, text varchar(50), primary key(id));
mysql> insert into greeting(text) values ('Hello, world!');
mysql> select * from greeting;
+-----+---------------+
| id  | text          |
+-----+---------------+
|   1 | Hello, world! |
+-----+---------------+
1 row in set (0.00 sec)
テーブルの準備ができたら、次は使用するモジュールだけど、この サイトによると、Haskell から DB を使うには、HDBC、HSQL、HaskellDBと言った選択肢があるらしい。とりあえず HDBC でいってみる。
$cabal install HDBC
$cabal install HDBC-mysql

※ cabal 自体は、Fedora のアプリケーションの追加/削除でインストールした。
※ ghc-pkg という低レベルのコマンドもある。
※ MySQL以外に HDBC でサポートされてるやつを調べるにはここ

できたら、MySQL に接続してみる。
まず ghci を立ち上げて、モジュールを取り込んでからDB接続を取得する。

ghci>  :m +Database.HDBC Database.HDBC.MySQL
ghci> conn <- connectMySQL MySQLConnectInfo{mysqlHost="localhost", mysqlDatabase="hdbc_test", mysqlUser="root",mysqlPassword="XXXX", mysqlPort=3306, mysqlUnixSocket="/var/lib/mysql/mysql.sock"}
ghci>

mysqlUnixSocket に設定するファイル名は、/etc/my.conf に書いてあるのでそれを指定すればいい。成功したら、上のようにエラーメッセージなしで次のプロンプトに移る。

ちなみに、省略時値が既に設定されている defaultMySQLConnectInfo を使えば、この場合だと mysqlHost, mysqlUser, mysqlPort を省略して以下のようにできる。

ghci> conn <- connectMySQL defaultMySQLConnectInfo{mysqlDatabase="hdbc_test", mysqlPassword="XXXXX", mysqlUnixSocket="/var/lib/mysql/mysql.sock"}

※ソースを読みたかったら、ここで見られる

接続が得られたら、いよいよ HelloWorld。

ghci> quickQuery' conn "SELECT * from greeting" []
[[SqlInt32 1,SqlByteString "Hello, world!"]]
うん。できたっぽい。

ちなみに エラー無しで取得できたと思った Connection を、いざ使おうとしたら "No instance for (IConnection Connection)"なんて言われる事がある。自分もそうなったけど HDBC 関連のパッケージを更新したら解消した(参考URL

====

ついでに、もうちょい他の事もやってみる。

■ prepared statement

ghci> stmt <- prepare conn "INSERT INTO greeting(text) VALUES (?)"
ghci> executeMany stmt [[toSql "Good-bye, world..."], [toSql "Hello, another world2!"]]
ghci> quickQuery' conn "select * from greeting"[]
[[SqlInt32 1,SqlByteString "Hello, world!"],[SqlInt32 2,SqlByteString "Good-bye, world..."],[SqlInt32 3,SqlByteString "Hello, another world!"]]
ghci> commit conn
JDBC やってるのと同じだね。

■ メタ情報

ghci> describeTable conn "greeting"
[("id",SqlColDesc {colType = SqlIntegerT, colSize = Nothing, colOctetLength = Nothing, colDecDigits = Nothing, colNullable = Just False}),("text",SqlColDesc {colType = SqlVarCharT, colSize = Nothing, colOctetLength = Nothing, colDecDigits = Nothing, colNullable = Just True})]
うーん…colType は良いけど、colSize がNothing になってるのはどういう訳だろう。ここは varchar (50) を反映していてほしかった。 標準SQL の INFORMATION_SCHEMA で、普通にメタ情報を得ることも、もちろんできる。

■ 日本語

ghci> run conn "UPDATE greeting SET text='こんにちは' WHERE id=1" []
ghci> quickQuery' conn "select * from greeting where id=1"[]
[[SqlInt32 1,SqlByteString "S\147kao"]]
ははは、文字化けした。mysql で見ても化けてる。面倒そうだから後で考えよっと。

--2012/08/12: 同じSQLを prepared statement でやったら問題なく「こんにちは」となる
--2012/08/12: ghci> run conn "UPDATE greeting SET text=? WHERE id=1" [toSql "こんにちは"] で上手く行く

気になるところが幾つかあったけど、最初の試行としてはこんなものだろう。

====

最後に豆知識メモ。

ghci のプロンプトを変えるには、":set prompt "ghci> "と入力すればいい。これを永続化するには、~/.ghc/ghci.conf に同じ事を書く。ただし、どういうわけか、ファイルのパーミッションで、グループに w が付いていると無視される。無視されないようにするには、"chmod g-w .ghc .ghc/ghci.conf "として、書き込み権限を取り除いておけばいい。

あと、いろいろググってると、HaskellDB と HSQL と HDBC とで、似て非なる事柄が一緒くたに引っかかってくるので、酒を飲みながら作業してたりすると、 HDBC.MySQL のソースのつもりで HaskellDB.HDBC.MySQL のソースを読んで小一時間頭をかきむしって苦しむ事になったりする。

2011年12月9日金曜日

Prolog で楽典クイズ

我々が日ごろ聞いてるふつうの音楽は、1オクターブの中に高さの異なる音が12個あって、それぞれの音は「音名」という名前を持っている。

実は、ほとんどの音はそれぞれ三つの音名を持っている。例えば、ミ♯、ファ、ソ♭♭の組み合わせや、レx、ミ、ファ♭の組み合わせは同じ音高であったりする。

ところが、ある音だけは二つの音名しか持っていない。この音が何かわかるだろうか。

ダブルシャープとかダブルフラットを使ってるから、小学校で習うかどうかは分からないけど、たぶん義務教育の範囲には入ってる気がするくらいの基礎的な楽典知識だけで構成された問いだけど、面白いことに、意外と音楽をやってる人でも即座には答えられなかったりする。

ピアノの鍵盤を思い浮かべて単なる図形として捉えたら簡単なんだけど、今日は、これを「寝る前ちょこっとプログラミング」のお題にしてみようと、帰りの電車で思いついた。Prolog でやってみる。

====

note(0, 'C').
note(2, 'D').
note(4, 'E').
note(5, 'F').
note(7, 'G').
note(9, 'A').
note(11, 'B').

notes([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]).

alteration('x', -2).
alteration('♯',  -1).
alteration('♮',   0).
alteration('♭',   1).
alteration('♭♭',  2).

noteWithThreeNames(Names)   :- findNoteNamesByCount(Names, 3).
noteWithOnlyTwoNames(Names) :- findNoteNamesByCount(Names, 2).

findNoteNamesByCount(Names, Count) :-
   notes(Notes), member(Note, Notes),
   collectNoteNames(Note, Names),
   length(Names, Count).

collectNoteNames(Note, Names) :-
  findall(Name, findName(Note, Name), Names).

findName(Note, Names) :- 
  alteration(Alteration, Diff), 
  getNote(Note + Diff, NaturalTone),
  concat_atom([NaturalTone, Alteration], Names).

getNote(N, X) :- N2 is N mod 12, note(N2, X).

素朴な実装だけど、以下のように正しい結果が得られる。
?- noteWithThreeNames(Names).
Names = ['B♯', 'C♮', 'D♭♭'] ;
Names = ['Bx', 'C♯', 'D♭'] ;
Names = ['Cx', 'D♮', 'E♭♭'] ;
Names = ['D♯', 'E♭', 'F♭♭'] ;
Names = ['Dx', 'E♮', 'F♭'] ;
Names = ['E♯', 'F♮', 'G♭♭'] ;
Names = ['Ex', 'F♯', 'G♭'] ;
Names = ['Fx', 'G♮', 'A♭♭'] ;
Names = ['Gx', 'A♮', 'B♭♭'] ;
Names = ['A♯', 'B♭', 'C♭♭'] ;
Names = ['Ax', 'B♮', 'C♭'].

?- noteWithOnlyTwoNames(Names).
Names = ['G♯', 'A♭'] ;
false.

====

音楽の話題でいえば、厳格対位法という、こんなのとは比較にならないくらいパズルっぽいやつがある。これは与えられた音の配列(定旋律)に対して、たくさんの「禁則」をくぐり抜けてごく限られた音の配列(対旋律)を見つけ出していくという、古典音楽作曲の伝統的なトレーニングなんだけど、視点を変えるとパズルの「川渡り問題」に似てなくもない。

たぶん、とっくにどこかのプログラマが挑戦してるだろうけど、もうちょいヒマができたら自分でもやってみたい気がする。

2011年12月6日火曜日

Jaskell このやろう!

名前だけはずいぶん前から知ってたけど、あまり興味が無くて放置していた Jaskell。

『プロダクティブ・プログラマ』で紹介されて高評価だったから、さぞかし有望株なのだろうと思いきや…

誰も使ってる気配がない…

まあダウンロードはできるから、ちょっと試してみようとしたけど、そもそも動いてくれない。

Exception in thread "main" java.lang.NoSuchMethodError: jfun.parsec.Parsers.plus(Ljfun/parsec/Parser;Ljfun/parsec/Parser;)Ljfun/parsec/Parser;
 at jfun.jaskell.JaskellParser.(JaskellParser.java:299)
 at jfun.jaskell.JaskellParser.instance(JaskellParser.java:1377)
 at jfun.jaskell.JaskellParser.parseExprOrLib(JaskellParser.java:1436)
 at jfun.jaskell.Jaskell.parseExprOrLib(Jaskell.java:2356)
 at jfun.jaskell.Jaskell.parseExprOrLib(Jaskell.java:2376)
 at jfun.jaskell.Jaskell.eval(Jaskell.java:2469)
 at jfun.jaskell.Jaskell.evalInputStream(Jaskell.java:2657)
 at jfun.jaskell.Jaskell.evalResource(Jaskell.java:2555)
 at jfun.jaskell.Jaskell.evalResource(Jaskell.java:2531)
 at jfun.jaskell.Jaskell.importPrelude(Jaskell.java:1990)
 at jfun.jaskell.shell.Shell.getShellRuntime(Shell.java:42)
 at jfun.jaskell.shell.Shell.main(Shell.java:35)
ちなみにこの例外は、java コマンドから Jaskell Shell を起動しようとしたときにも、Jaskell インスタンスを生成してそいつにスクリプトを読ませようとしたときにも、 どっちでも発生する。

第一、起動の仕方らしきものにたどり着くまでだいぶかかった。プロダクティブ・プログラマで紹介されている URL、「http://jaskell.codehaus.org/」 は、Jaskell でググっても一番上にくるやつだけど、動かし方がどこにも全然書いてない。

jaskell-1.0.jar を実行したら何か起こるだろうと思ったけど、うんともすんとも言わない。MANIFEST.MFにも、やっぱなんも書いてなかった。

しょうがないから、JavaDoc を読んでたら Jaskell クラスというのがあって、こいつが文字列やファイルを評価する eval () メソッドを持ってるから、インスタンス作って eval() したら、上の例外。

半泣きでググってたら、Jaskell Shell というのを見つけるが、トップページからリンクされてる訳でもなく、どこが本物のプロジェクトページだか、訳が分からない。で、動かしてみたら、また上の例外。

まあ、何個かある jar の組み合わせを変えてみるとか、再コンパイルしてみるとかで、解決しない事もないんだろうけど、もういい。心折れた。降りるわ。

それにしても Neal Ford さんは、なんでこんなの紹介したんだ…。
普通に、Haskell 勉強して行こうやって結論しか出てこない。

2011年12月4日日曜日

Groovy+HttpClient で Redmine の wiki 更新

Redmine の wiki を、HTTP のクライアントプログラムで更新する方法を考えてみた。言語は何でもいいが、ここでは Groovy を使ってみた。

====

例えば、以下のようなコードで、

class Test {
   def static content = """\
h1. 大見出し

なんとかこんとか
${ new Date().toString() }
"""
      public static void main(args) {
      new RedmineWikiRewriter()
         .login("user001", "password")
         .startEdit("project001", "Http_post_test")
         .saveEdit(content)
   }
}
下の画像のように、wiki ページが更新されるような クラス RedmineWikiRewriter を考えてみる。

  • login() では、与えられたユーザ名とパスワードでログインして、Redmine から返されるクッキーを保持する。
  • startEdit()は、指定のプロジェクトの指定の wiki ページを開く。これはページのバージョンを得るために必要
  • saveEdit()は、wiki コンテンツをマルチパートの HTTP POST で送信する
コードは以下のようなものになる

class RedmineWikiRewriter {
   def static UTF8 = Charset.forName("UTF-8")

   def HTTPBuilder http = new HTTPBuilder('http://localhost:3000/')

   def redmineSession
   def authenticityToken
   def contentVersion 
   
   def project
   def wikiEntry
   
   public RedmineWikiRewriter login(String username, String password) {
      http.get(path : '/login', query : [q:'Groovy'] ) { resp, reader -> 
         authenticityToken = reader.BODY.DIV.DIV
            .findAll { it.id="login-form" }.depthFirst()
            .grep { "authenticity_token"== it.@name.toString() }[0]
            .@value.toString()
      }
      http.request(POST) {
         uri.path = '/login'
         send URLENC, [
            username:username, password:password, 
            authenticity_token:authenticityToken]

         response.success = { resp ->
            redmineSession = resp.getAllHeaders()
               .find {"Set-Cookie"==it.name.toString()}
               .value.split(";")
               .find {it.toString().startsWith("_redmine_session")}
            }
      } 
      this
   }
   RedmineWikiRewriter startEdit(String project, String wikiEntry) {
      this.project = project
      this.wikiEntry = wikiEntry 

      http.request(GET) { resp ->
         uri.path = "/projects/"+project+"/wiki/" + wikiEntry + "/edit"
         headers.'Cookie' = redmineSession
         response.success = { res, reader ->
            contentVersion = reader
               .depthFirst()
               .grep{"content_version"==it.@id.toString() }[0]
               .@value.toString()
         }
      }
      this
   }
   void saveEdit(String content) {
      DefaultHttpClient httpclient = new DefaultHttpClient();
      def uri = "http://localhost:3000/projects/"+ project +"/wiki/" + wikiEntry

      HttpPost httppost = new HttpPost(uri)
      httppost.addHeader('Cookie', redmineSession)
      
      MultipartEntity mpe = new MultipartEntity(
         HttpMultipartMode.BROWSER_COMPATIBLE, 
         "----WebKitFormBoundaryfmexSJQ5daIXdA04", 
         UTF8);
      addPart(mpe, "commit",          "Save");
      addPart(mpe, "project_id",       project);
      addPart(mpe, "action",          "update");
      addPart(mpe, "_method",          "put");
      addPart(mpe, "authenticity_token", authenticityToken)
      addPart(mpe, "id",             wikiEntry)
      addPart(mpe, "content[text]",    content)
      addPart(mpe, "content[version]", contentVersion)
      addPart(mpe, "controller",       "wiki")
      
      httppost.setEntity(mpe);

      httpclient.execute(httppost);
   }
   static void addPart(MultipartEntity entity, String name, String value) {
      entity.addPart(name, new StringBody(value, UTF8));
   } 
}

■ 振り返り

  • saveEdit() でも HttpBuilder を使いたかったが、なぜか思うように動かない。仕方なく、DefaultHttpClient を使用
  • wiki エントリの新規作成でも動くかどうかは未確認
  • たかだかこれくらいのコードでも、やってみると予想外に難しい。Redmine の Rubyコードを読んだり、HTTP の Header を調べたりいろいろ手間がかかった。あと multipart の POST も意外と難しい
  • 仕組みはだいたい分かったので、他の言語でも試してみたい。もともとやりたかったのは、Excel 上の VBA からデータを整形して wiki に載せるというのを自動化すると言う事だった。

2011年12月3日土曜日

Fantom でサンタクロース問題

前回のせんだみつおゲームで Fantom の Actor の使い方が何となく分かったので、サンタクロース問題でもやってみようと思う。クリスマスも近いしな。まあ、本物のプログラマには、そんなの関係ないけどな。

====

SantaClausProblem.main()は、サンタ、トナカイ、小人を生成して起動する

using concurrent

class SantaClausProblem {
  Void main() {
    ActorPool pool := ActorPool()

    Santa santa  := Santa(pool)
    Deer[] deers := (1..9).map |Int n->Deer| { Deer(pool, n, santa) }
    Elf[] elves  := (1..10).map |Int n->Elf| { Elf(pool, n, santa) }

    deers.each |Deer deer| { deer.send("vacation") }
    elves.each |Elf elf| { elf.send("home") }

    while (!pool.isStopped) { Actor.sleep(1sec) }
  }
}

Player は登場人物共通のコード。ActorUnderSanta はトナカイと小人の共通コード。

Actor
 └ Player
     ├ Santa
     └ ActorUnderSanta
         ├ Deer
         └ Elf
const class Player: Actor {
  new make(ActorPool pool) : super(pool) {}

  Void sleepRandom(Range r) { Actor.sleep(1sec* Int.random(r)) }
}
const class ActorUnderSanta: Player {
  const Int number
  const Santa santa

  new make(ActorPool pool, Int number, Santa santa) : super(pool) {
    this.number = number
    this.santa = santa }

  Str name() { this.typeof().name + number  }
}

トナカイと小人はこんな感じ

const class Deer: ActorUnderSanta {
  new make(ActorPool pool, Int number, Santa santa)
    : super(pool, number, santa) {}

  override Obj? receive(Obj? msg) {
    if ("vacation" == msg) { sleepRandom(1..10); santa.send(this) }
    return msg
  }
}

const class Elf: ActorUnderSanta {
  new make(ActorPool pool, Int number, Santa santa)
    : super(pool, number, santa) {}

  override Obj? receive(Obj? msg) {
    if ("home" == msg) { sleepRandom(2..15); santa.send(this) }
    return msg
  }
}

サンタはこんな感じ

const class Santa: Player {
  new make(ActorPool pool) : super(pool) {}

  override Obj? receive(Obj? msg) {
    echo (((ActorUnderSanta)msg).name + " が来た")
    
    if (msg is Deer) addDeer((Deer)msg)
    else if (msg is Elf) addElf((Elf)msg)
    
    return msg
  }
  Void addDeer(Deer deer) {
    Deer[] deers := get("deers", (Deer[])[,])
    deers.add(deer)
    wakeUpIfConditionIsMet
  }
  Void addElf(Elf elf) {
    Elf[] elves := get("elves", (Elf[])[,])
    elves.add(elf)
    wakeUpIfConditionIsMet
  }
  Void wakeUpIfConditionIsMet() {
    if (9 == getDeers().size) deliverToys
    else if (3 <= getElves().size) meetInStudy
  }
  Void deliverToys() {
    echo("おもちゃを配る")
    sleepRandom(1..2)
    getDeers().each|Deer deer| { deer.send("vacation") }
    Actor.locals["deers"] = (Deer[])[,]
  }
  Void meetInStudy() {
    echo("小人と打ち合わせ")
    sleepRandom(1..2)
    Elf[] elves := getElves()
    elves.eachRange(0..2, |Elf elf| { elf.send("home") })
    Actor.locals["elves"] = elves.removeRange(0..2)
  }
  Deer[] getDeers() { (Deer[])Actor.locals["deers"] }

  Elf[] getElves() { (Elf[])Actor.locals["elves"] }

  Obj get(Str key, Obj defaultValue) {
    Obj? result := Actor.locals[key]
    if (null == result) {
      result = defaultValue
      Actor.locals[key] = result
    }
    return result
  }
}
こんな結果が得られる
$ fan santaClausProblem.fan
Deer6 が来た
Deer1 が来た
Elf2 が来た
Elf9 が来た
Deer9 が来た
Deer2 が来た
Deer8 が来た
Deer3 が来た
Elf3 が来た
小人と打ち合わせ
Elf5 が来た
Elf8 が来た
Deer4 が来た
Deer7 が来た
Elf1 が来た
小人と打ち合わせ
Deer5 が来た
おもちゃを配る
Elf2 が来た
Elf7 が来た
Elf9 が来た
小人と打ち合わせ
Deer9 が来た
Deer5 が来た
Elf4 が来た
Elf6 が来た
Elf3 が来た
小人と打ち合わせ

2011年11月29日火曜日

Fantom でせんだみつおゲーム

Youtube 観てたら、古いテレビの録画で、せんだみつおゲームをやっていた。やっぱりプログラマだから書いてみたくなる。言語はこないだ見つけた、Fantom にしてみる。

こんな感じ

  • Actor を5つ生成して、5名の参加者に見立てる
  • メインのスレッドでは、無限ループしながら Actor にタイミングを出しつづける
  • Actor は、"SENDA", "MITSUO", "NAHANAHA"を出力しながら、ランダムに次の番のアクターを指名する

using concurrent

class SendaMitsuoGame {
  Void main() { play }

  Void play() {
    pool := ActorPool()
    Actor[] actors := (1..5).map { Player(pool, it) }

    actors[0].send("next1")
    actors.each { it.send(actors.toImmutable) }

    messages := ["senda", "mitsuo", "nahanaha"]
    while (true) {
      message := getHeadAndRotate(messages)
      actors.each { it.send(message) }

      Actor.sleep(1sec)
    }
  }
  Str getHeadAndRotate(Str[] m) { m.add(m.removeAt(0)).last }
}

const class Player : Actor {
  const Int number

  new make(ActorPool p, Int number) : super(p) {
    this.number = number }

  override Obj? receive(Obj? msg) {
    if (msg is List) { Actor.locals["all"] = msg }
    else if (msg == "senda") doSenda
    else if (msg == "mitsuo") doMitsuo
    else if (msg == "nahanaha") doNahanaha
    else if (0==((Str)msg).index("next")) {
        setMyTurn(((Str)msg)["next".size..-1].toInt)
    }
    return null
  }

  Bool isNext(Int n) {
    diff := (n - number).abs()
    return 1 == diff || 4 == diff
  }

  Bool isMyTurn() { Actor.locals["myTurn"] }

  Void setMyTurn(Int f) {
    Actor.locals["myTurn"] = (number == f)
    Actor.locals["nextOnesTurn"] = isNext(f) }

  Void doSenda()  { callAndAssign("SENDA") }

  Void doMitsuo() { callAndAssign("MITSUO") }

  Void doNahanaha() {
    if (Actor.locals["nextOnesTurn"])
        echo("" + number + ": NAHANAHA");
  }

  Void callAndAssign(Str call) {
    if (!isMyTurn()) return

    next := nextOne()
    echo("" + number + ": " + call + " -> " + next);

    ((Actor[])Actor.locals["all"]).each { it.send("next" + next) }
  }

  Int nextOne() {
    next := Int.random(1..4)
    if (next < number) return next
    return (++next < 6) ? next: 1
  }
}

出力はこんな感じになる。ミスる事も無く、延々とせんだみつおゲームを繰り返す。適当な確率でミスさせる事も、もちろんできるけど、割愛。

$ fan senda.fan 
1: SENDA -> 2
2: MITSUO -> 5
1: NAHANAHA
4: NAHANAHA
5: SENDA -> 1
1: MITSUO -> 3
4: NAHANAHA
2: NAHANAHA
3: SENDA -> 5
5: MITSUO -> 4
5: NAHANAHA
3: NAHANAHA
4: SENDA -> 3
3: MITSUO -> 1
5: NAHANAHA
2: NAHANAHA
1: SENDA -> 4
4: MITSUO -> 5
1: NAHANAHA
4: NAHANAHA
しかし夜遅くまで、俺、何書いてんだ・・・

2011年11月25日金曜日

Groovy + POI

Groovy では、クロージャを使って若干の下準備(下例では TableBuilder)をしておくと、以下のようなコードが書ける。

static void main(args) {
   new TableBuilder().with {
     sheet(   "test"
         )(   style(header)
         )(   "No"   )(   "名前"      )( nextRow
         )(   style(normal)
         )(   1      )(   "琵琶湖"    )( nextRow
         )(   2      )(   "多来加湖"  )( nextRow
         )(   3      )(   "富内湖"    )( endTable   )
   }.write(new FileOutputStream("sample.xls"));
}

TableBuilder の中では、Apache POI を使っていて、実行すると 下のような Excel ファイルが sample.xls に書き出される。

カッコの開き方が気に入らないが、しょうがないらしい。下の A、B は等価だけど、C は別ものに解釈される。上例を C のようなカッコのに直すと、動作しない。

ABC
   ("A1")("B1")(
    "A2")("B2")
    ("A1")("B1"
   )("A2")("B2")
   ("A1")("B1")
   ("A2")("B2")

TableBuilder は下のコードのようになる。

class TableBuilder {
   def book
   TableBuilder() { book = new HSSFWorkbook() }
   def nextRow = { cell, style ->
      cursor.curry(cell.offset(1, -cell.columnIndex), style)
   }
   def endTable = { cell, style -> book }
   def cursor = { cell, style, arg ->
      if (arg instanceof Closure) { arg(cell, style) }
      else { 
         cell.setCellValue(arg)
         if (null != style) cell.cellStyle = style 
         cursor.curry(cell.next(), style) 
      }
   }
   def start(sheet){ cursor.curry(sheet.cellAt(0, 0), null) }
   def sheet(name) { start(book.createSheet(name)) }
   def style(styleClosure) {
      { cell, style-> cursor.curry(cell, styleClosure(book))}}
}

また、POI の API が Groovy コードの中で馴染むように、Mixin でメソッドを追加した。

class CellMix {
   def next() { offset(0, 1) }
   def offset(r, c) {
      sheet.cellAt(rowIndex + r, columnIndex + c) }
}
class RowMix {
   def cellAt(c) { getCell(c) ?: createCell(c) }
}
class SheetMix {
   def rowAt(r) { getRow(r) ?: createRow(r) }
   def cellAt(r, c) { rowAt(r).cellAt(c); }
}
class CellStyleMix {
   def borderMethodName = { s-> "setBorder" + s.capitalize() }
   def setBorder = { arg, edge -> 
      owner."${borderMethodName(edge)}"(arg."$edge" ?:BORDER_NONE); 
      setBorder.curry(arg) }
   def setBorder(Map arg) {
   setBorder.curry(arg)("top")("left")("bottom")("right") }
}

カッコを連鎖させるやり方にしたのは、単にクロージャとカリー化を試したかっただけで、実は、普通のクラスと leftShift() のオーバライドだけで、 下のような C++風の書き方もできて、こっちの方がむしろスッキリしたコードになる。

sheet("test") << 
   "A1" << "B1" << endRow <<
   "A2" << "B2" << endTable

2011年11月23日水曜日

Fantom

Info Q を読んでたら、Scala は先行き暗いねって記事があった。その内容自体は、まあ一理あるねくらいだけど、その問題提起をしている人が、気に入ってる言語として Scala と対比してる Fantom というのがあって、ちょっと調べてみた。

ここでサンプルコードを読んでみると、特にキャッチーな感じはしないけど、その分、実用性を念頭に置いてる気がする。知的な遊びのためよりむしろ割と泥臭い現場でも使えるように考えられてる印象。C# が分かる Javaプログラマで、面倒くさがりな人向けって感じか。言語仕様とか文法のパラダイムよりも、バイトコードやモジュールの扱いみたいなアーキテクチャ寄りの面に長所があるのかもしれない。

Linux にインストールするには、

  • オフィシャルサイトのトップページからダウンロードして、
  • 適当なところに展開して、fantom-1.0.XX フォルダの中に入る
  • adm/unixsetup を実行して bin/下のファイル群に実行権限を付ける
  • bin/fan -versionを実行して、動作確認
  • 必要に応じて PATH にbin/を追加する

以下のようにして、FWT という GUI 上の HelloWorld ができる。

  • ファイル FwtHello.fan に以下のコードを書く
    using fwt
    class FwtHello : Test
    {
      Void main()
      {
        Window { Label { text = "Hello world" }, }.open
      }
    }
  • $ fan FwtHello.fan で以下のようなちっこいウィンドウが表示される。

====

全然、Fantom とは関係ない話だけど、Fantom についていろいろググってて、Fantom で迷路の最短経路をとくパズルをやってる人がいたので、その先をたどると面白い記事を見つけた。

問題は「人材獲得作戦・4 試験問題ほか」に載ってて、この問題をどのように使ったかという顛末(「人材獲得作戦・3」)まで書かれていてとても面白い。

ただ、「問題の性質からいって、キューやリストが必要なのは明らかなのに、言語にCを使う人が相当多い。」ってところだけ残念。以下のようにすればオブジェクト指向も関数型も論理型も関係なく簡単に解ける。

  • S の上下左右に隣接する空のマスに1 を書き込む
  • 1 に隣接する上下左右の空のマスに2 を書き込む
  • ・・・
  • N に隣接する上下左右の空のマスにN+1を書き込む
  • Gに着いたら、隣接するマスを N からカウントダウンする形で戻れば完成

コードは簡単だから省略。C 言語はもちろん、多分 COBOL でも書ける。

Squeak の右クリメニュー

Fedora のソフトウェアの追加/削除から Squeak をインストールするとバージョン 3.10 の Mysqueak ってのが入ってくるけど、かなり古いものらしい。 [→参照]

で、squeak.org のここから Squeak All-in-One をダウンロードすると、バージョン 4.2 が入っている。これを適当なところで展開して、squeak.sh を実行すれば、環境が立ち上がる。

ちなみに今使っている Linux 環境では、特に設定しなくても、右クリで 'do it'を含むコンテキストメニューがポップアップされたけど、古いやつはマウスの設定を変える必要があった。

  • 何もない背景のとこで左クリック
  • World のメニューが出てくるので[open...]を選択
  • サブメニュー[Preference Browser]を選択
  • "mouse"でフィルタすると [swapMouseButtons] ってのが出てくるので、enabled にチェックを入れる

Windows 版だと、今のバージョンでもそんな設定が必要になる事もあるらしい。[→参照

2011年11月21日月曜日

LePUS3

昨日のブログを書くために、Wikipedia の Strategy パターンのエントリを開いてみたら、見慣れないものがあった。

なんかかっけえ。フリーメイソンとかみたいだ。三角のとことか。

LePUS3 というダイアグラムらしい。五十音的には、レプスさんくらいの感じだろうか。

Wikipedia によると

  • UML と同様のオブジェクト指向モデリング言語である。
  • formal specification language(形式仕様記述言語) の一つである。
  • First-order predicate logic(一階述語論理)のサブセットである
  • Codechart とも呼ばれる
  • いいところ
    • スケーラビリティ :少しの記号を用いた小さな Codecharts で、大規模なプログラムをモデリングできる
    • 自動検証 :設計と実装を同期させて、自動的に適合性を検証できる
    • 可視化 :リバースでソースコードから Codecharts を生成しやすい
    • パターンの実装 :プログラムがデザインパターンを実装しているかどうか、自動的に判断できる
    • 設計抽象化 :実装の詳細に取りかかる前に抽象的なプログラムを表現できる
    • 総称 :特定の実装ではなく、設計のモチーフとしてデザインパターンを表現できる
    • 厳密性:設計者は Codecharts によるモデルを厳密に検証できる

いつのまにか、Wikipedia のプログラム関連のエントリの、至る所で LePUS3 が使われている。全然、気がつかなかった

オフィシャルサイトはここ

この本で紹介されている。
Codecharts

自分は注文したけど、しかし高い。
7300円もか。そんなにもか・・・

2011年11月20日日曜日

OSGi で GoF の Strategy

久しぶりに OSGi でもいじってみるかと思い立つ。

Wikipedia に載ってる Strategy パターンの サンプル・コードでも OSGi で動かしてみよっかなと。今回は、Equinox でやってみる。

OSGi 環境を作る

  • 適当な場所に osgi-test フォルダを作る
  • osgi-test フォルダ 下に plugins フォルダ を作る
  • Eclipse の plugins下から以下のファイルを osgi-test/plugins フォルダ 下にコピーする。
    • org.eclipse.osgi_*.jar
    • org.eclipse.equinox.ds_*.jar
    • org.eclipse.equinox.util_*.jar
    • org.eclipse.osgi.services_*.jar
うちの環境だとこんな感じになる
$ cd /tmp/osgi-test
$ tree
.
└── plugins
    ├── org.eclipse.equinox.ds_1.3.1.R37x_v20110701.jar
    ├── org.eclipse.equinox.util_1.0.300.v20110502.jar
    ├── org.eclipse.osgi.services_3.3.0.v20110513.jar
    └── org.eclipse.osgi_3.7.1.R37x_v20110808-1106.jar

起動して、他のプラグインも追加しておく

$ java -jar plugins/org.eclipse.osgi_3.7.1.R37x_v20110808-1106.jar -console

osgi> ss
Framework is launched.
id State       Bundle
0 ACTIVE      org.eclipse.osgi_3.7.1.R37x_v20110808-1106

osgi> install file:///tmp/osgi-test/plugins/org.eclipse.osgi.services_3.3.0.v20110513.jar
Bundle id is 1
osgi> install file:///tmp/osgi-test/plugins/org.eclipse.equinox.util_1.0.300.v20110502.jar
Bundle id is 2
osgi> install file:///tmp/osgi-test/plugins/org.eclipse.equinox.ds_1.3.1.R37x_v20110701.jar
Bundle id is 3

osgi> start 1
osgi> start 2
osgi> start 3
osgi> ss
Framework is launched.
id State       Bundle
0 ACTIVE      org.eclipse.osgi_3.7.1.R37x_v20110808-1106
1 ACTIVE      org.eclipse.osgi.services_3.3.0.v20110513
2 ACTIVE      org.eclipse.equinox.util_1.0.300.v20110502
3 ACTIVE      org.eclipse.equinox.ds_1.3.1.R37x_v20110701

環境が出来たので、以降ではプラグインを作って動かしてみる。先に書いたとおり、Wikipedia にある Strategy のサンプルコードをネタにして、Strategy、ConcreteStrategyAdd、ConcreteStrategyMultiply、Context を連動させてみる。

Strategy の作成〜インストール

まずは、Strategy プラグインプロジェクトの作成

  • [New]->[Plugin-Project]
  • Project Name には "ex1.osgi.strategy.strategy"を指定、Activetor 無し、tempate 不使用で[Finish]
  • MANIFEST.MF の [Runtime]タブを開いて、[Exported Packages]に"ex1.osgi.strategy.strategy" を追加する。
  • 以下の Wikipedia から持ってきた、以下のコードを追加する
    package ex1.osgi.strategy.strategy;
    public interface Strategy {
       int execute(int a, int b);
    }
できたら以下のように Export しておく。
  • プロジェクトのコンテキストメニューから、[Export]->[Deployable plug-ins and fragments] を選択
  • [Destination] / [Directory]に、osgi-test フォルダを指定する
  • [Options] で、"Package plug-ins as individual JAR archives"のみチェックされている事を確認して[Finish]
エクスポートしたら、OSGi に install しておく
osgi> install file:///tmp/osgi-test/plugins/ex1.osgi.strategy.strategy_1.0.0.201111202011.jar
Bundle id is 4
osgi> refresh
osgi> ss
Framework is launched.
id State       Bundle
0 ACTIVE      org.eclipse.osgi_3.7.1.R37x_v20110808-1106
1 ACTIVE      org.eclipse.osgi.services_3.3.0.v20110513
2 ACTIVE      org.eclipse.equinox.util_1.0.300.v20110502
3 ACTIVE      org.eclipse.equinox.ds_1.3.1.R37x_v20110701
4 RESOLVED    ex1.osgi.strategy.strategy_1.0.0.201111202011
ss や refresh といったコマンドの意味は、OSGi コンソールから help と打てば、説明が出てくる。

ConcreteStrategyAdd の作成〜インストール

  • [New]->[Plugin-Project]
  • Project Name には "ex1.osgi.strategy.add"を指定、Activetor 無し、tempate 不使用で[Finish]
  • MANIFEST.MF の [Dependencies]タブを開いて、[Required Plugins]に"ex1.osgi.strategy.strategy" を追加する。
  • 以下のようにコード追加する。
    package ex1.osgi.strategy.add;
    import ex1.osgi.strategy.strategy.Strategy;
    
    public class ConcreteStrategyAdd implements Strategy {
        public int execute(int a, int b) {
            System.out.println("Called ConcreteStrategyAdd's execute()");
            return a + b;  // Do an addition with a and b
        }
    }
  • プロジェクト直下に OSGI-INF フォルダを追加する
  • OSGI-INF フォルダのコンテキストメニューから、[New]->[Component Definition]を選択
  • "Component Definition Information" / Class に ex1.osgi.strategy.add.ConcreteStrategyAdd を指定して[Finish]
  • component.xml の Service タブで、[Provided Services]に ex1.osgi.strategy.strategy.Strategy を追加する
これも Export して、OSGi にインストールしておく。

Context の作成〜インストール

次は ConcreteStrategyMultiply をやる前に、Context に行ってみる

  • [New]->[Plugin-Project]
  • Project Name には "ex1.osgi.strategy.context"を指定、Activetor 無し、tempate 不使用で[Finish]
  • MANIFEST.MF の [Dependencies]タブを開いて、[Imported Packages]に"ex1.osgi.strategy.strategy" を追加する。
  • 以下のコードを追加する。Wikipedia のサンプルコードでは、Context#executeStrategy() を実行するのは StrategyExample#main() だけど、面倒だから strategy が関連付けられたときに Context 自身が executeStrategy() のテストコードを実行するようにした。
    package ex1.osgi.strategy.context;
    import ex1.osgi.strategy.strategy.Strategy;
    
    public class Context {
       private Strategy strategy;
     
       public synchronized void setStrategy(final Strategy strategy) {
          this.strategy = strategy;
          testExecuteStrategy();
       }
       public synchronized void unsetStrategy(final Strategy strategy) {
          if (strategy == this.strategy) this.strategy = null;
       }
       public int executeStrategy(int a, int b) {
          return strategy.execute(a, b);
       }
       public void testExecuteStrategy() {
          final long l = System.currentTimeMillis();
          final int a = (int) (l % 10);
          final int b = (int) (l / 10 % 10);
          final int result = executeStrategy(a, b);
          System.out.printf("execute(%d, %d) = %d", a, b, result);
       }
    }
  • プロジェクト直下に OSGI-INF フォルダを追加する
  • OSGI-INF フォルダのコンテキストメニューから、[New]->[Component Definition]を選択
  • "Component Definition Information"/Class にex1.osgi.strategy.context.Context を指定して[Finish]
  • component.xml の [Service]タブで、[Referenced Services] に Strategy を追加する
  • 追加した Strategy を選択して[Edit]、"Bind"にsetStrategy、"Unbind"にunsetStrategy を指定する
できたら、これもエクスポートして、OSGiにインストールしておく。


動作確認

とりあえず、ここまでで動かしてみる。

osgi> start 4
osgi> start 5
osgi> start 6
Called ConcreteStrategyAdd's execute()
execute(3, 1) = 4
osgi> 
期待通りに動作する。(実は、start 4 は無くても良いし、start 5 と start 6 の順番は逆でも良い。)

ConcreteStrategyMultiply の追加と動作確認

最後に、ConcreteStrategyMultiply を付け加えて、ConcreteStrategyMultiply と差し替えてみる。

  • [New]->[Plugin-Project]
  • Project Name には "ex1.osgi.strategy.multiply"を指定、Activetor 無し、tempate 不使用で[Finish]
  • MANIFEST.MF の [Dependencies]タブを開いて、[Required Plugins]に"ex1.osgi.strategy.strategy" を追加する。
  • 下のようにコード追加
    package ex1.osgi.strategy.multiply;
    import ex1.osgi.strategy.strategy.Strategy;
    
    public class ConcreteStrategyMultiply implements Strategy {
        public int execute(int a, int b) {
            System.out.println("Called ConcreteStrategyMultiply's execute()");
            return a * b;   // Do a multiplication with a and b
        }
    }
    
  • プロジェクト直下に OSGI-INF フォルダを追加する
  • OSGI-INF フォルダのコンテキストメニューから、[New]->[Component Definition]を選択
  • "Component Definition Information"/Class に net.yasuabe.osgi.ex1.strategy.multiply.ConcreteStrategyMultiplyを指定して[Finish]
  • component.xml の Service タブで、[Provided Services]に ex1.osgi.strategy.strategy.Strategy を追加する
これもエクスポートして、OSGiにインストールしておく。

エクスポートしたら、Context から実行される Strategy を、Add から Multiply に変えてみる。

osgi> ss
Framework is launched.

id State       Bundle
0 ACTIVE      org.eclipse.osgi_3.7.1.R37x_v20110808-1106
1 ACTIVE      org.eclipse.osgi.services_3.3.0.v20110513
2 ACTIVE      org.eclipse.equinox.util_1.0.300.v20110502
3 ACTIVE      org.eclipse.equinox.ds_1.3.1.R37x_v20110701
4 ACTIVE      ex1.osgi.strategy.strategy_1.0.0.201111202011
5 ACTIVE      ex1.osgi.strategy.add_1.0.0.201111202028
6 ACTIVE      ex1.osgi.strategy.context_1.0.0.201111202042
7 RESOLVED    ex1.osgi.strategy.multiply_1.0.0.201111202123

osgi> stop 5
osgi> start 7
Called ConcreteStrategyMultiply's execute()
execute(3, 1) = 3
osgi> ss
Framework is launched.
id State       Bundle
0 ACTIVE      org.eclipse.osgi_3.7.1.R37x_v20110808-1106
1 ACTIVE      org.eclipse.osgi.services_3.3.0.v20110513
2 ACTIVE      org.eclipse.equinox.util_1.0.300.v20110502
3 ACTIVE      org.eclipse.equinox.ds_1.3.1.R37x_v20110701
4 ACTIVE      ex1.osgi.strategy.strategy_1.0.0.201111202011
5 RESOLVED    ex1.osgi.strategy.add_1.0.0.201111202028
6 ACTIVE      ex1.osgi.strategy.context_1.0.0.201111202042
7 ACTIVE      ex1.osgi.strategy.multiply_1.0.0.201111202123
osgi> stop 7
osgi> start 5
Called ConcreteStrategyAdd's execute()
execute(4, 3) = 7
osgi> 
Context を起動したまま、Strategy を差し替えられるのが確認できる。

雑感

今まで、何回か、いわゆるパッケージ開発案件に関わってきたけど、納品先個別の要件の扱い方について、何パターンかの手法がある。

一番泥臭いのだと、トランクのソースコードに個別要件対応のコードを入れてしまって、条件文で切り分けるというベタベタのローテク手法がある。「バカじゃね?」って思う人も多いかもしれないけど、実際の現場では結構多い。

それよりは、少しはマシな手法だと、納品先別にブランチを切って個別要件はそこに入れていくという事になる。まあ、これも知的な方法とは言いにくいけど、割と普通に行われている。

もっと良い方法としては、パッケージ共通部分と個別にカスタマイズ可能な部分について最初に見通しを立てた上で、カスタマイズ可能な部分がプラガブルになるようにアーキテクチャを設計する手法がある。

この手法をとると、機能の差し替えや追加、また管理面においてもリスクが少なくて、柔軟なソフトウェアになるんだけど、この場合にもさらに、プラグインの方式を自分で作るか、既存の仕様や製品を使うかで道が分かれる。

必ずしも自作にこだわる必要がないとしたら、OSGi がかなりの有力候補なのではないかと思う。(個人的には、はっきり言って、かなり高い割合で自作の道を取るのは不正解だと思うけど。)

2011年11月19日土曜日

Eclipse + Groovy + NekoHTML

Redmine や Trac の wiki などを使ってプロジェクト内で情報共有している HTMLベースのコンテンツを、プログラムで取り扱うやり方を考えていて、Groovy でも使ってみようかと思い立つ。

で、やっぱり Eclipse で使いたいのでプラグインを入れる。Indigo なら update site は ここで、Required とマークされている Groovy Eclipse ってのがあるから、これを選択してインストール。

インストールできたら、お試しプロジェクトを作る。パッケージエクスプローラ上の右クリから New -> Other と進むと、Groovy Project がリストに出てくるから、選択して適当に操作すると、Groovy Project が生成される。

適当に Hello World を書いて、main() メソッド上の右クリから[Run As]を選択すると、サブメニュー、[1 Groovy Console], [2 Groovy Script], [3 Java Application]が表示される。どれを選んでも、同じコードが同じように実行される。

これで、Eclipse で Groovy が使えるようになった。

Groovy 自体、たいして使ったことがないので、練習がてら「wikipedia の適当なページから「他の言語」にリストアップされている言語を、標準出力に書き出す」という簡単なお題をやってみる事にする。

Groovy で HTTP GET や HTML/XML を扱う方法をググりながら、以下のようなコードを書いてみた。

class Ex2 {
   static void main(args){
      new XmlSlurper(
            new org.cyberneko.html.parsers.SAXParser())
         .parse("http://ja.wikipedia.org/wiki/秋刀魚")
         .depthFirst()
         .grep{it.name() == 'DIV' && it.@id == 'p-lang'}[0]
         .DIV.UL.LI.each { println it.text() }
   }
}
Eclipse から実行すると、秋刀魚を解説している10個の言語の名前が書き出される。あまり便利ではなかったが、デバッガも一応使える。

XmlSlurper のコンストラクタに渡しているものは、NekoHtml の SAXParser というやつで、HTML にタグを補完して well-formed な XML として扱えるようにしてくれる。

使えるようにするには、ここから zip か tgz をダウンロードして、中の nekohtml.jar と xercesImpl.jar (または xercesMinimal.jar )を、~/.groovy/lib 下に置けばいい。

2011年11月13日日曜日

論理パズル で Prolog のウォーミングアップ

テストケース(開発者が作る、いわゆる DeveloperTest のテストケースじゃなくて、開発はやらないテスト部隊が使うテストケース)を作るための小道具として Prolog を使うと便利かもと思って、自宅の環境に SWI-Prolog を用意してみた。

Fedora の「ソフトウェアの追加/削除」でリストアップされてたので、選択して適用するだけでインストールできた。

本当は、与えられた条件にしたがって、縦に要因、横にテストケースを並べた直交表もどきの表を生成するような仕組みを作りたいんだけど、その前にウォーミングアップしてみる。

手元にある論理パズル集の中から簡単なものを選んで、Prolog で解いてみる事にする。

お題は以下のようなもの。

  • とあるミスコンで、好美、恵美、明美、麻美、宏美が予選通過した
  • なかよし4人組が以下のように優勝者を予想した
    • まゆみ:好美と恵美ではないだろう
    • 大貴:恵美か明美だろう
    • 崇臣:好美か麻美だろう
    • やす美:好美と麻美ではないだろう
  • 予想が的中した人数は2名だった。
優勝したのはだれか?

あまり Prolog に詳しいわけではないけど、素朴に書くと以下のようになるのではないだろうか。
% 出場者
contestant(akemi). 
contestant(asami). 
contestant(hiromi). 
contestant(emi). 
contestant(yoshimi).

% 誰が誰の優勝を予想しているか
expect(mayumi, akemi). expect(mayumi, asami). expect(mayumi, hiromi).
expect(daiki, akemi). expect(daiki, emi).
expect(takaomi, yoshimi). expect(takaomi, asami).
expect(yasumi, emi). expect(yasumi, akemi). expect(yasumi, asami).

% 結局、優勝者は2名の予想者に予想された出場者だった
winner(Contestant):- expectors(Contestant, 2).

% 優勝者と予想者数の関係
expectors(Contestant, Count):-
  contestant(Contestant),  
  countExpectators(Contestant, [mayumi, daiki, takaomi, yasumi], Count).

% 優勝者を予想した人を数える
countExpectators(_, [], 0 ).
countExpectators(Contestant, [Expector|OtherExpectors], Count) :-
  expect(Expector, Contestant), 
  countExpectators(Contestant, OtherExpectors, Count2),
  Count is Count2 + 1.
countExpectators(Contestant, [Expector|OtherExpectors], Count) :-
  not(expect(Expector, Contestant)),
  countExpectators(Contestant, OtherExpectors, Count).
これを SWI-Prolog に読ませて、以下のようにすると答えが出る。
?- winner(Winner).
Winner = emi .
ちなみに明美の優勝を予想した人数は、上で定義した述語 expectors 使って、以下のように求められる。
?- expectors(akemi, Count).
Count = 3 .
逆に、同じ expectors を使って、3名が優勝を予想した出場者を求めることもできる。これがPrologの面白いところでもある。
?- expectors(W, 3).
W = akemi ;
W = asami ;
false.

====

ソフトウェアのテストケースを作ろうとすると、あるテスト対象の振る舞いに影響を与える条件群について、すべての条件を掛け合わせると現実的でないテストケース数になったりする

なので、少ないテストケース数でなるべく各条件を網羅できるように工夫する事になるんだけど、その辺りの作業がちょっとしたパズルみたいになってくる事がある。

で、せっかくプログラミングを生業としているのだから、プログラムでなんとかしようかって発想になるんだけど、手続き型言語を使うと、やりたい事とコード上の表現がいまいち直截性に欠けていたりする。

あまり流行ってはいなけど、こういう時は Prolog が割と使えそうな気がしないでもない。

2011年11月10日木曜日

Fedora のアップグレードで平常心の訓練

Fedora 16 が昨日か一昨日リリースされたので、Fedora15 からアップグレードしてみた。

まず、Fedora のサイトに書いてあるアップグレード手順どおりにやってみる。

rpm --import https://fedoraproject.org/static/A82BA4B7.txt
yum update yum
yum clean all
yum --releasever=16 --disableplugin=presto distro-sync

うん。やっぱり、うまくいかない。
--skip-broken が使えないからとか何とかメッセージが出て、途中で止まる。

まあ、初めはいつもこんなもんだ。俺は、ぜんぜん動じない。で、ちょっと調べて以下のようにして再挑戦。

yum --releasever=16 --disableplugin=presto distro-sync --skip-broken

今度は、 「Transaction Check Error で、gnome-icon-theme-symbolicgnome-power-manager が keyboard-brightness-symbolic.svg のアレやら何やら(意訳)」とか言ってくる。 こやつめ、ハハハ!

よく分からないけど、試しに以下のようにしてみた(gnome-icon-theme-symbolicとか言うやつは、この際、忘れる)。

yum --releasever=16 --disableplugin=presto distro-sync --skip-broken --exclude=gnome-icon-theme-symbolic

うん、なんだか上手く行ってるっぽい。エラーも無く yum が終了。再起動してログインしたら、Fedora 16 のデスクトップ画像が見えた。端末から cat /etc/fedora-release とやってみたら、 Fedora release 16 (Verne)と出た。

うん、これは出来ておるね。出来たっぽいね。

まあ、いちいち書いてはいないけど本当はもっと試行錯誤して、正直つらかった。よく覚えてないけど、気がつくと頬が濡れてたから、俺ちょっと泣いてたのかもしれん。まあでも、どうやら一応、Fedora 16 にアップグレードできた。


で、まずは 「ソフトウェアの追加/削除」で Eclipse を Indigo に変える。実はこれが、そもそもの目的だった。 うまくインストールから起動まで確認できて一段落。ほっとする。

更に、他のいろんなもの全部をアップグレードしたくて、「ソフトウェアの更新」を起動すると、700個近く検出された。かまわず[Install Updates]押下。

・・・って、ま〜たかよ。また、gnome-icon-theme-symbolic と gnome-power-manager が干渉してるとか言ってきた。

今度は、 yum remove gnome-power-manager で、電力管理の何かを削除してみる。多分使ってないし、要るなら後でなんとかすればいいし。
で、再度、[Install Updates]。

すると今度は、「libnih が GLIBC とナントカカントカ」だの言ってきやがって、ギギギ、てめえコノヤ……まあ、短気は損気とも言うからな。

rpm -q --whatrequires libnih で見てみると、別にどこからも依存されてる形跡がないので、yum remove してしまう。

再び、[Install Updates]を再試行すると、今度はどんどん進む。これは今渡こそ、上手く行ってるっぽい。

小一時間で 700 近い更新が適用。清々しい気分。再起動しろというので、にこやかにOKボタン押下。

で、しばらく待ってると、いつものようにユーザ選択のポップアップが表示された。ユーザ名を選んでからパスワードを入力。

…そしたら、困った顔のマシンの画像で「ああ、何かがおかしいです、ログインし直してください」とか、メッセージを出してくる。
はあ?何だ、その言い草?しかし、何度やってもログインできず、同じ戯言を繰り返してくる・・・

何度やっても、何度やっても、何度やっても、何度やっても、何度やっても、何度やっても、何度やっても、

おぎゃーーーーーーー!!!


…ひとしきり地団太を踏んで暴れると、嗚咽も収まり、だんだん冷静さが戻ってきた。いつまでもクヨクヨしない。泣いたってログインできないよって、ばあちゃん言ってたし、俺は大人だしな。

で、なんとなく SELinux のせいだろうと思ったので、無効になるようにしたいのだけど、ログインできない状態でどうすれば良いか分からない。 幸い予備のマシンがあったからググったら、シングルユーザモードというのがあって、Windows の safe mode みたいな事ができるらしい。

なんか、起動中に良い感じのタイミングで「E」キーを押すと、カーネルの選択画面になるので、そこで single を追加指定すれば root でログインできる(詳しいやり方は、シングルユーザモードで検索するとなんぼでも出てくる)。

で、CUI ログインしたら、 vim /etc/sysconfig/selinuxで SELinux の設定ファイルを開いて、「SELINUX=enforcing」から「SELINUX=disabled」に書き換える。

で、もっかい再起動。
今度こそ、ちゃんとログインできた。
ほっとしたが、えらく手間がかかったなあ・・・。

まあ、こういった アップグレード関連のアレコレとかって、Linux そのものが好きでベータ版から使ってる人には、大半が常識的で造作ない事なんだろうけど、自分はすごい苦手。こういった作業はたまにしかしないし、してもさっぱり記憶が定着しないし、本当、弱点だわ。

# Fedora 自体は気に入っているが

2011年11月5日土曜日

FEST+JavaScript で半自動 Swing テスト

あ〜、暇だから、Swing フォームを JavaScript で動かすやり方でも考えてみっかな。

====

例えば、打鍵+目視に基づいた手動画面テストが既に実施されていたり、チェックリストとかテスト仕様書が出来ている場合に、作業負荷を減らすために、後から部分的に自動化を導入して、半自動テストにしたい場合がある。

ところでfest という、Swing アプリを Javaコードから機能テストするための API セットがあり、Javaコード からUIイベントを発生させて画面上のコンポーネントを操作するAPIが一揃い入っている(AWT の Robot をベースにしたものらしい)。

また、JavaScript コードを Javaプログラムから動かす仕組みは、Java 1.6 から備わっていて、特に何か特別なライブラリを必要とせずに、簡単に使えるようになっている。

これらを利用すれば、テスト対象フォームの外で JavaScript を実行して、フォーム上の要素を制御することができるはず。

■ 実験台

実験台として、以下のフォームを考えてみる。

こんな仕様
  • テキストボックスの名前は "field1"
  • チェックボックスの名前は "check1"
  • ボタンの名前は "button1" で、押下すると "check1"がチェックされている場合は field1 を大文字化した文字列、チェックされていない場合は field1 そのままの文字列を、標準出力に書き出す。

実験台だから単純なフォームにしたけど、実際の現場での開発対象フォームは入力項目が何十個もあって、実行ボタンを押すまでに必要な打鍵動作が多く、1回なら問題ないけど、同じようなテストケースや回帰テストをしたりすると心身ともに疲れたりする。やはり、そういうのは、何とかして省力化したい。

■ 方針

まず、テスト対象フォームを開くと同時に、以下のような別ウィンドウを開く。

で、ここで JavaScript を入力して Excecute ボタンを押下すると、手動の打鍵と同等の JavaScript が実行されるようにしたい。

■ 実装

JavaScript 入力用のウィンドウ ScriptInputWindow は、最初にテスト対象ウィンドウの FrameFixture を受け取る。

public static void main(String[] args) {
   JFrame frame = showWindow(new TestTargetWindow("Test Target"), 0);
   FrameFixture window = new FrameFixture(frame);
   showWindow(new ScriptInputWindow(window), 3000);
}
public static JFrame showWindow(JFrame frame, int xpos) {
   frame.setSize(250, 150);
   frame.setLocation(xpos, 0);
   frame.setVisible(true);
 
   return frame;
}
見ての通りテスト対象ウィンドウは TestTargetWindow だけど、ScriptInputWindow と TestTargetWindow は同じテストハーネスのプロセスから実行する事になる。もしかすると、開発対象システムに固有な何か特別な親ウィンドウとか、ランチャーとかを介さないと、テスト対象ウィンドウ単体ではフォームを開けないと心配する人もいるかもしれないが、大抵の場合は instrumentation ベースのモックツールを使えば何とかなるし、そんなに難しくもない。

ScriptInputWindow はまず最初に、テスト対象フォーム上のコントロールを、JavaScript 用の ScriptEngine に登録する。ScriptEngine も ScriptInputWindow が保持しておく。

public ScriptInputWindow(FrameFixture window) {
   super("JavaScript input window");

   initializeControlls();  

   this.jsEngine = new ScriptEngineManager().getEngineByName("JavaScript");
   registerComponents(window, window.component(), 0);
}

@SuppressWarnings("restriction")
private void registerComponents(
      FrameFixture window, Container container, int indent) {

   for (Component component: container.getComponents()) {
      String name = component.getName();
      if (null != name) {
         ComponentFixture comp = selectComponent(window, name);
         if (null != comp) this.jsEngine.put(name, comp);
      }
      if (component instanceof JComponent)
            listComponents(window, (Container)component, indent + 1);
   }
}
selectComponent() は ContainerFixture から name に一致する ComponentFixture を選んでくるメソッドだけど、面倒なので省略。

ボタンを押すと書き込んだJavaScriptコードを実行する部分は以下のようなもの。

executeButton.addActionListener(new ActionListener() {
   public void actionPerformed(ActionEvent arg0) {
      ExecutorService e = Executors.newSingleThreadExecutor();
      e.execute(new Runnable() {
         public void run() {
            try {
               String text = ScriptInputWindow.this.textArea.getText();
               ScriptInputWindow.this.jsEngine.eval(text);
            } catch (Exception e) {
               e.printStackTrace();
             }
          }
      });
   }
});

■ 試行

以下のコードを書き込んで実行すると、テキストボックスの内容が "123abcABC" に変わり、チェックボックスがチェックされ、ボタンが押され、標準出力に”123ABCABC”が出力される。
field1.setText("123abcABC");
check1.check();
button1.click();

■ 応用

  • フォーム上のコントロールの操作以前に、まずコントロールの名前を知ってなきゃならないんだけど、必要な時に階層状に表示してテスターに見せる機能もあった方が良いかもしれない。これは registerComponents でやってる事をそのままどっかに書き出すだけだから簡単。
  • 自動打鍵の結果として得られた画面上の更新項目を、比較しやすい形式でどこか(TextArea とかクリップボードでもファイルでも)に書き出す仕組みもあると便利かもしれない。グリッドとかツリーとかあると、ちょっと手間がかかるかも。
  • 自動打鍵後に、用意しておいた期待値と自動的に比較する仕組みがあると、かなり全自動に近づいてくる。
  • 手動打鍵の記録なんかまでやるとなると、それは難しい。商用製品でも買った方がマシかもしれない(下記の参考記事をみるとFESTでも対応作業中みたいな書きっぷりだけど、どうなったのか不明)。

■ 参考サイト

2011年11月3日木曜日

仲間外れはどれか?

下の図は、仲間外れを見つける問題例だけど、小学校受験のためのものだから幼稚園児向けということになる。

仲間外れといっても、一応、少なくとも野菜ではあるのだから、問題の趣旨を一旦置いておけば、あながち間違った括りとは言えなくもない。

でも、じゃあ、「カボチャ」の代わりに、「八百屋さん」だったり「プログラマ(好きな食べ物:肉、嫌いな食べ物:野菜)」とかだったとしても、同じカテゴリって言い張る奴っているのだろうか。まあ、こんなアホな質問はした事はないけど、身の回りには多分いないと思う。つうか、いたら嫌だ。

ところが、それに相当するようなソースコードなら、プロの開発現場でもしょっちゅう見かける。今日はそんな事を書いてみる。

====

こんなコードがあったとする。

このコードには、ダメなところがいくつかあるけど、特にまずい問題点を、ちゃんと技術者っぽい言葉で説明できるだろうか。

/** 色を指定できるカーペット。 */
public class Carpet {
   private final int color;
   public Carpet(int color) {
      this.color = color;
   } 
   //…
}

/** システムで使える色の集合  */
public class Colors {
   private static final Map COLORS = new HashMap();
   static {
      COLORS.put("赤", 0xFF0000);
      COLORS.put("緑", 0x00FF00);
      COLORS.put("青", 0x0000FF);
   };
   private Colors() {} 
   public static int colorNameToRgbValue(String colorName) {
      return COLORS.get(colorName); 
   }
   public static List createCarpets(String[] colorNames) {
      List result = new ArrayList();
      for (String colorName: colorNames) {
         result.add(new Carpet(COLORS.get(colorName)));
      }
      return result;
   }
}

Colors は本当は enum を使うべきだけど、サンプルの都合上普通のクラスにした。また、そもそもユーティリティクラスがまずいってのは、取り敢えずこの際、放っておく事にする。

Colors という色のセットを表すクラスに、色に関係ない事はないけど明らかに異質でレベルの違う Carpet という概念を扱うメソッド、createCarpets() が定義されている。

こういうとき、ちょっと堅苦しい言い方だけど、「凝集性が低い」という(もちろん低いほどダメ)。

もっと言うと、暗合的凝集度論理的凝集度の間くらいになると思われ、レベルとしてはサイテーに近い。(技術者っぽくない言葉でいうと、暗合的凝集度は「なんで同じクラスに入れてんのかイミフ」なやつで、論理的凝集度は「言い訳できない事ないけど必然性に乏しい」やつ。)

この凝集性と表裏一体の概念として、結合度というのもある。

上のコードは結合度が高い(こっちは高いほどダメ)のだけど、実質的にグローバル関数といえるメソッド群がクラス変数という実質グローバルな情報を介して結びついている点で「共通結合」であり、6段階あるうちの悪い方から2番目で、やはりかなり酷いコードと言える。

さらに、凝集性や結合度はオブジェクト指向隆盛以前の、構造化手法全盛期からある設計評価基準だけど、オブジェクト指向に関連の強い言葉「責務」を用いて表現すれば、「createCarpets によって、Colors が一つ以上の異なる責務を負っている事が問題」と言う事もできる(責務:言うまでもなく CRC の R(Responsibility))。

つうわけで、上のコードは単純過ぎる例ではあるものの、技術的には「低凝集・高結合度で、複数の責務を負った下手糞なコード」だと言える。まあ、そういうコードや設計を放置しておくと、その先どうなるかについて、技術者じゃない人になら説明する必要があるけど、技術者同士なら説明不要だと思う(思いたい)。

ただし、実はクソヘタプログラマだけがこんなコードを書いているのではなくて、そこそこの技術者/チームでも、開発期間の中で生じるいろんな行きがかり上、たまたまこんな感じになってしまって、そのまま直す機会を逸してしまってる場合もある。

まあ、でも。そんな時のためのリファクタリング技術なわけで、ちゃんとテストコードを実行するという前提(当たり前だけど、無ければ必ず書き足す)さえ守れば、なんぼでも体質改善すれば良い。

だけど、そもそもダメだって事が認識できないとなると、いろんな意味でかなりやばい。説明するこっちも、クラスの責務から説明すればいいのか、結合度・凝集度から説明すればいいのか、それとも小学校受験の仲間外れ問題まで遡って説明しなければならないのかと考えてしまって、なんかもうキッツイわ…

2011年11月1日火曜日

Eclipse と JMockit でテストファースト

以前のブログで、他の2つのクラスとのコラボレーションを含む簡単なクラスのテストをJMockitを使って書いたけど、その時は、敢えてレガシーコードに後付けテストを追加するような感じで書いてみた。

やろうとしている事は以下のようなものだった。

  1. 作ろうとしているのは Foo というクラスで、文字列を返すメソッド hoge() を持っている
  2. Foo#hoge() が返す文字列は、Baz#hoge() から取得したものである
  3. ただし、Baz のインスタンスは、Bar のインスタンスを経由しないと使えない
これを以下に、テストファーストで実装してみる。ただし今日は、Eclipse を極力活用するという趣向でやってみる。

====

まず①の クラス Foo とメソッド hoge()だけど、以下の様に、まず最初にテストクラスを書く。

public class FooTest {
   @Test public void hoge() {
      Assert.assertEquals("dummy", new Foo().hoge());
   }
}
まだクラス Foo が無いから当然コンパイルエラーの赤波が表示されるので、以下のように仮実装する。
 public class Foo {
   public String hoge() {
      return null;
   }
}
ただし、これをそのままタイプしてはいけない。Eclipse を使うという前提なのだから Ctrl+1を2回(クラスに一回、メソッドに一回)使うだけでタイプせずに済むので、これを活用する。
とりあえず、ここまでで JUnit を実行して、テストが失敗するのを見てから、hoge() の戻り値を"dummy"に変えて、テスト成功に変わるのを確認しておく

で次に、② の Baz#hoge() への委譲。これもテスト・コードから書く。

public class FooTest {
   @Test public void hoge() {
      new NonStrictExpectations() {
         Baz baz;
         {
            baz.hoge(); result = "dummy"; times = 1;
         }
      };
      Assert.assertEquals("dummy", new Foo().hoge());
   }
}
ここでももちろん、Baz も Baz#hoge() も未だ無いわけだから赤波が表示されるけど、さっきと同様に Ctrl+1 を 2回使って Baz の空実装を Eclipseに生成させる。ただし今度は Baz#hoge() の中身を以下の様に書き換える。
public class Baz {
   public String hoge() {
      //FIXME [your user name] Nov 1, 2011
      throw new AssertionError();
   }
}
自分の環境では、この FIXMEコメントから AssertionError送出の2行は、Java エディターのテンプレートに登録してあるので、fixme とタイプして Ctrl+1を押せば、その日の日付とログインしているユーザ名で2行挿入される。

で、赤波が消えたら、また JUnit を実行して Baz#hoge() が呼ばれていないというアサーション例外が上がるのを確認し、Foo#hoge() を以下の様に書き換える。
public String hoge() {
   return new Baz().hoge();
}
書いたらまた JUnitを実行して、今度は緑になるのを確認。

最後に、③ の Bar インスタンスを経由して Baz インスタンスを得るところ。これもやっぱりテストコードから。

public class FooTest {
   @Test public void hoge() {
      new NonStrictExpectations() {
         Baz baz;
         Bar bar;
         {
            bar.baz(); result = baz; times = 1;
            baz.hoge(); result = "dummy"; times = 1;
         }
      };
      Assert.assertEquals("dummy", new Foo().hoge());
   }
}
またここでも Baz#hoge() と同様に、Ctrl+1 をクラスとメソッドにそれぞれ適用して空実装を生成し、FIXME付きのアサーション例外送出コードを書いておく。

赤波が消えたらJUnit実行。Bar#baz() が呼ばれてないってアサーション例外が上がったら、おもむろに Fooを以下のように書き換える。
public class Foo {
   private final Bar bar = new Bar();
   public String hoge() {
      return this.bar .baz().hoge();
   }
}
但し、これも Ctrl+1 (→ Create field 〜)を活用し、無駄なタイピングをせずに編集する。できたら、Junit を実行しになることを確認。

というわけで、完了。

====

ちなみに、この間からかじり始めた Coq のチュートリアルの第2回で、結論から1個ずつ論理を遡って証明を完成させていく手順が紹介されていたけど、テストファーストと似ているところがあると思った。

2011年10月29日土曜日

暇だから MongoDB でも入れてみるか

せっかくの休日、Eclipse の Xtext を試してみたかったのに、Indigo はコアダンプ吐いて即死するし、Helios はプロジェクトの作成に失敗するわで、15分くらいで心が折れた。

で、代わりの暇つぶしとして、最近 NoSQL について下調べしていて見かけた MongoDB とか言う奴でもいじってみようかなと思いつく。この辺りを参考(飽くまでも参考)にして、Fedora15 の 32bit 環境でやってみた。

実は、Fedora で管理しているMongoDB が「ソフトウェアの追加と削除」で最初からリストアップされてるんだけど、これをインストールしても、なんかちゃんと起動しなかった。

====

まずはインストールだけど、配布元の 10gen を Fedora (というか yum)に認識させるために、ファイル /etc/yum.repos.d/10gen.repo を作って以下の記述を追加する。

[10gen]
name=10gen Repository
baseurl=http://downloads-distro.mongodb.org/repo/redhat/os/i686
gpgcheck=0

そうすると、「ソフトウェアの追加と削除」にリストアップされるから、以下を選択して[適用]

  • mongo-10gen-server-2.0.1-mongodb_1
  • mongo-10gen-2.0.1-mongodb_1

インストールされたら、起動してみたい所だけど、その前に、なんかデータを保存するフォルダを作っとく必要があるらしい。

$ sudo mkdir -p /data/db/
$ sudo chown `id -u` /data/db

準備ができたので、Fedora の[サービス]ツールで mongod を開始する。

端末で、mongo を起動して動作確認。

$ mongo
> one = {name: "one", value: 1}
> two = {name: "two", value: 2}
> three = {name: "three", value: 3}
> db.things.save(one)
> db.things.save(two)
> db.things.save(three)
> db.things.find()
{ "_id" : ObjectId("4eabd63586daea21618266ca"), "name" : "one", "value" : 1 }
{ "_id" : ObjectId("4eabd63786daea21618266cb"), "name" : "two", "value" : 2 }
{ "_id" : ObjectId("4eabd63a86daea21618266cc"), "name" : "three", "value" : 3 }
> db.things.find({name:"one"}, {_id:0})
{ "name" : "one", "value" : 1 }
> db.things.find({value:2}, {name:1})
{ "_id" : ObjectId("4eabd63786daea21618266cb"), "name" : "two" }
> db.things.find({value:3}, {name:1,_id:0})
{ "name" : "three" }

ちゃんと動いてるらしい。

次は、java コードから動かしてみたい(チュートリアル)。

2011年10月27日木曜日

JavaScript + XSLT の小技

ここ数年、一部界隈でアンチXMLみたいな動きがあったりするが、現実には XML はいろんな場面で使われている。中には数万行のものがあったりするけど、さすがにそのサイズになると、直接、内容を読もうとすると、結構不便だったりする。

というわけで、最近重宝している ブラウザと Javascript と XSLT を使った小技を書いてみる。

こんな感じの XML があるとする。

<?xml version="1.0"?>
<numerals>
  <numeral><arabic>1</arabic><jp>壱</jp><en>one</en></numeral>
  <numeral><arabic>2</arabic><jp>弐</jp><en>two</en></numeral>
  <numeral><arabic>3</arabic><jp>参</jp><en>thee</en></numeral>
</numerals>

これを XSLT で HTML に変換して、ブラウザ上で左下のようなリストを表示し、それぞれのリスト要素に設定されているハイパーリンクをクリックすると、右下の様に詳細が表示されるといった事をしたい。

数字2
日本語
英語two

以下、その方法。

====

リストを表示する XSLT は以下のようなものになる。ポイントは<a>要素を生成しているところで、javascript 関数 showDetail()に arabic 要素を渡すようなリンクが作られる。

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:template match="/">
    <ul>
      <xsl:for-each select="numerals/numeral">
        <li>
          <xsl:element name="a">
            <xsl:attribute name="href">
              javascript:showDetail(<xsl:value-of select="arabic"/>)
            </xsl:attribute>
            <xsl:value-of select="jp"/>
         </xsl:element>
        </li>
      </xsl:for-each>
    </ul>
  </xsl:template>
</xsl:stylesheet>

で、次に詳細を表示するためのXSLTは以下で、ポイントはパラメータ arabic をグローバルで宣言しているところ。

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:param name="arabic"/>
  <xsl:template match="/numerals/numeral">
    <xsl:for-each select=".[arabic=$arabic]">
      <table>
       <tr>
         <td>数字</td>
         <td><xsl:value-of select="arabic"/></td>
       </tr>
       <tr>
         <td>日本語</td>
         <td><xsl:value-of select="jp"/></td>
       </tr>
       <tr>
         <td>英語</td>
         <td><xsl:value-of select="en"/></td>
       </tr>
      </table>
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

いよいよ、Javascript と それを含むHTMLだけど、ソースは以下のようなものになる。

<html>
 <head>
  <script type="text/javascript" src="sarissa.js"></script>
  <script type="text/javascript">
   function loadXmlFile(fileName) {
     var result = Sarissa.getDomDocument();
     result.async = false;
     result.load(fileName);
     return result;
   }
   function replaceResultArea(xmlDom) {
     var serializer = new XMLSerializer(); 
     var output = serializer.serializeToString(xmlDom.documentElement);
 
     document.getElementById("resultArea").innerHTML = output;
   }
   function showList() {
     var xml = loadXmlFile("test.xml");
     var xslt = loadXmlFile("ul.xsl");

     var processor = new XSLTProcessor();
     processor.importStylesheet(xslt);
     var xmlDom = processor.transformToDocument(xml);

     replaceResultArea(xmlDom);
   }
   function showDetail(arabic) {
     var xml = loadXmlFile("test.xml");
     var xslt = loadXmlFile("detail.xsl");

     var processor = new XSLTProcessor();
     processor.importStylesheet(xslt);
     processor.setParameter("", "arabic", arabic);
     var xmlDom = processor.transformToDocument(xml);

     replaceResultArea(xmlDom);
   }
  </script>
 </head>
 <body onload="showList()"/>
   <div id="resultArea"/>   
 </body>
</html>

冒頭で参照している sarrisa.js はXMLを扱うための JavaScript ライブラリで http://dev.abiss.gr/sarissa/ からダウンロードできる。ただし Chrome がどうもサポートされてないっぽい。本当にそうなら残念(まあ、それほど困らないけど)。

以上、「(1)パラメータを与えつつ、(2)動的に XSLT を適用して、(3)ブラウザ上で表示を切り替える」方法。サンプルなんで、簡単なものだけどなんぼでも応用がきく。(例えば、静的ソース解析ツールが吐いたXML形式のレポートを分析したり、XMIで出力したUMLのドキュメントを解析してメトリクスを試算したりとか。)

ちなみに、個人的には XML それ自体に対する忌避感みたいなのは、全くといって良いほど無い。他の大抵の技術要素と同じく、上手く使えば役に立つし、下手に使えば役に立たないという単純な問題。

2011年10月24日月曜日

簡単な後付けテストで基本を再確認

こんなクラスがあるとする。

public class Foo {
   private final Bar bar = new Bar();
   public String hoge() {
      return this.bar.baz().hoge();
   }
}
見ての通り、Foo は メソッド hoge()の中で、関連オブジェクト bar から取得した baz に、hoge() 処理を移譲している(日本語にすると、なんのこっちゃだが…)。

Bar と Baz は以下のような感じで後回しにしてある。

public class Bar {
   public Baz baz() {  throw new AssertionError();  }
}
public class Baz {
   public String hoge() {  throw new AssertionError(); }
}

ここで、Foo#hoge() をテストするにはどうするか。つまり以下の/* ??? */ のところに何を入れたらテストできるか。

public class FooTest {
   @Test public void hoge() {
      /* ??? */
      Assert.assertEquals("hello", new Foo().hoge());
   }
}

とりあえず2個だけ例を書いてみると(ちょっとずつニュアンスの違うものを何通りも書けるが)、以下のようになる。

@Test public void hoge1() {
   new Expectations() {
      @Mocked("baz") Bar bar;
      @Mocked("hoge") Baz baz; {
         bar.baz(); result = baz;
         baz.hoge(); result = "hello";
      }
   };
   Assert.assertEquals("hello", new Foo().hoge());
}
@Test public void hoge2() {
   new NonStrictExpectations() {
      Bar bar;
      Baz baz; {
         bar.baz(); result = baz; times=1;
         baz.hoge(); result = "hello"; times=1;
      }
   };
   Assert.assertEquals("hello", new Foo().hoge());
}
JMockit を使い始めたばかりだと、結構、とまどうんじゃないかと思う。デバッガで追ってみると、例えば "result = baz" のところで、初期化していないはずの baz に何故かインスタンスが設定されているし、代入されたと思った result が null のままだったりして、普通の Java ソースの感覚で読んでいると訳が分からなかったりする。

とは言ってもまあ、すぐ慣れるし、そうなると逆に、JMockit 無しのテストを考える事が、時折難しく感じられてくる。

ただ、JMockit を使うと後付けテストもかなり簡単になるにはなるけど、やっぱりテストファーストが基本というのは押さえておいた方が良い。この例はもともと簡単だから、それほど大変なテストコードにはならないけど、もっと複雑なものになると、後付けとテストファーストではテストコーディングの生産性が大きく違ってくる。

====

テストファースト版は、こっちに書いた。

2011年10月23日日曜日

Fedora 15 + JavaDB

自宅マシンで javaDB を使って試したいことがあったんだけど、OpenJDK には JavaDB が入っていないらしい。「ソフトウェアの追加と削除」にもリストアップされない。という訳でRPMからインストールしてみた。

まず、ここからダウンロード

で、Oracle サイトのここを見てインストール

$ chmod +x javadb-10_5_3_0-linux-rpm.bin 
$ ./javadb-10_5_3_0-linux-rpm.bin 
# su
# cd javadb-10.5.3.0/
# rpm -ivh sun-javadb-*.rpm

ij コマンド で起動して、試してみる

ij> connect 'jdbc:derby:testdb1;create=true'; 
ij> create table t1 (col1 int primary key, col2 varchar(10));
ij> insert into t1 (col1, col2) values (100, '俺'));
ij> select * from t1;
COL1       |COL2      
----------------------
100        |俺   

まあ問題なさそう。
※実はエラーメッセージとか help とかが文字化けしてたけど、javaコードから使うのがメインだから放置。

2011年10月18日火曜日

Coq をインストールしてみた

今日、現場の昼休みに、最近噂の Coq のチュートリアルを見つけたので、早速インストールしてみる。

Fedora 15 でアプリケーションの追加と削除を開くと、Coq 8.3がリストアップされてきたので、チェックを入れて適用。

さらに、端末を開いて coqide と打ち込んでみると、ひとしきり追加のインストールが実行されたあと、CoqIDE が開いた。 とりあえず、チュートリアル 一回目の練習問題を解いてみるが、初回なんで難しくない。

結構面白い。明日もやろっと。

2011年10月17日月曜日

リファクタリング再読 4章 テストの構築

このブログで、何度も引用してきたけど、10年以上前に読んだリファクタリングが、今読んでも面白い。

面白いと言っても、当たり前の事が普通に書いてあるだけなんだけど、忘れた頃に読み返すと、自分が現場でいつも言っている事と大部分一致していて、若い頃に受けた影響ってやっぱりずっと残るんだと実感する。(さすがに、このレベルの名著ですら、やはり10年も経つとある程度の陳腐化は避けられず、全てに同意はできるわけではないけど…)

今日は 『4章 テストの構築』から、何点か引用して考えてみたい。

====

テストを完全に自動化して、その結果もテストにチェックさせること。(P.90)
xUnit Patterns の言葉で言うと、Manual Interventionは止めようねって事で、まあ、JUnitの基本中の基本。とはいっても、@Test メソッドの中で普通のアサーションで書ける比較を、標準出力に書き出して目視確認してたおバカさんを、こないだ発見した。こんなレベルの人が「JUnit 使用歴○○年です!」なんつってプロジェクトに入り込んできちゃったりするから、この業界っておそろしい。


テストを頻繁に実行せよ。コンパイル時にはテストを局所化して、一日に最低一度は全てのテストを実行せよ。(P.94)
今は普通に CIサーバでコミット時にテスト走らせたり、incremental test とかも普及してるから、一日一度はさすがに10年前って感じがするが、なるべく頻繁に実行すべしってのが、この文の趣意。まあ、なんぼ言っても、他人のテストコードをコケさせておきながら、コンパイルが通ってるってだけでテストもせず、平気でコミットする間抜けが減るどころか永遠に増え続けてるから、気が滅入るが…。


バグレポートを受け取ったら、まずそのバグを明らかにするための単体テストを書け。(P.97)
これは今も昔も良いプラクティスで、余り反論する人を見たことがないけど、数年前までは、いざバグを再現させるテストコードを書こうにも、そもそもテスタビリティが低すぎて、異常に難易度が高い場面もあった。今は、instrumentation を活用したテストフレームワークが普及しているから、そうでもないけど。(テストツールのテスト能力が高まっただけで、ソースコードのテスタビリティは大して進歩していないのが悲しいが)


ここでお話しするのは「単体テスト」です。これはプログラマの生産性を向上するために行うものです。これで品質管理部門が満足するとしても、それは副作用に過ぎません(p.96)
リファクタリング〜xUnit の文脈の中で、「機能テスト」と対置されて語られる「単体テスト(UnitTest)」って、ウォーターフォール・モデルの単体テストを単に自動化しただけのものとはかなり違うんだけど、この勘違いは今でもかなり根強い。 自分が、現場で説明する時には、「外部品質じゃなくて内部品質をあげるためのツールだから」とか言う事がある。分からない人はどう言い換えてもやはり分かってくれないけど、わかる人はなるほどそうかと納得してくれたりする。


不完全なテストでも、書いて実行する方が、実行できない完全なテストよりもましだ。
テストを書けば自分が楽になるって認識が成立しないとプログラマはテストを書きたがらないわけで、つまり下手なプログラマって、テストコードによって自分で自分の作業を楽にする事ができないプログラマなんだけど、その手の輩に無理やりテストを書かせようとするから無理なカバレッジ基準が制定されて、後付けでパスを通そうとするから、却って生産性が下がる。そんなのどうせ、まともなアサーションが書かれるわけないのにさ。

本当は、自覚して UnitTest を使えてるプログラマが、どこにどの程度テストコードを書くか自分で判断すればいい事なんだけど、最低辺も含めて、いろんな人が集まるプロジェクトでは、うーんやっぱ難しいのかな…


大事な事は、一番怪しいと思う部分をテストすることです。それが最も効率の良いテスト方法です。(p.97)
失敗の恐れのある境界条件を考えて、そこを集中的にテストせよ。(p.99)
失敗すると予想される時に、例外が上がることをテストし忘れないこと。(p.100)
境界条件や例外条件なんかをテストするにしても、テスト駆動/テストファーストでやるのが、結局は一番楽で確実なんだけど、現場のレベルによってはいくら言い聞かせても浸透しない。後付けでテスト書いたって、カバレッジだけに必死になって、どこが「怪しい」かなんて完全に眼中の外になってしまう事が分かりきってるんだけどね。


テストで全てのバグが見つからないからといって、テストを書くのを止めてはならない。ほとんどのバグはテストで補足される。
そもそも開発者が要件を誤解していたり、忘れていたりしたら、そういった認識エラーは本体コードと同様にテストコードにも織り込まれてしまうから、開発者テストだけで捕捉するのは論理的に不可能。そういった限界がある事を認識した上で、機能テストも絡めた全体的なテスト計画を立てるのが常道。


====

あと、引用ではないけどテスト関連で付け加えると、『リファクタリング』に載ってるリファクタリング・パターンのほとんどに「コンパイルしてテストする」と言う手順が組み込まれている事も、改めて確認できる。

この10数年で、リファクタリングという言葉が普及したおかげもあってか、動いてるコードに手を入れる心理的ハードルが昔より低くなったけど、「動いているコードはいじるな」っていう昔からの不文律を乗り越えて良いのは、テストコードという前提あってこそという、基本中の基本が蔑ろにされ始めてるのを感じる。テストもせずにコードをいじって、CIサーバから常時テスト失敗メールが飛んできてるのに何とも思わない連中が、どんどん増えてきている気がする。

出来ている現場の出来てる人達はびっくりするかもしれないけど、まだまだこんな感じの現場が多いんだよな… いかん、また眠れなくなってくる…

2011年10月10日月曜日

自動テストのレベル分け

最近は、「xUnit で UnitTest を書いてカバレッジとってます」なんて、どいつもこいつも謳ってるけど、お前らマジかと。それで出来てるつもりなのかと…?

というわけで、秋の朝がさわやかなので、自動テストの出来てる度合いの段階区分を考えてみたい。

  Level 3: 出来てる:常時テスト成功、高カバレッジ

ちゃんとできてるプロジェクトの出来る子ちゃん達からしたら、取り立てて言及することもない当たり前の状態だと思う。

"Clean Check-In" が普通に守られているから、当然、テストも常時全件成功する。まあ基本中の基本。

で、TDD/TestFirst で開発してるから、開発の最初期から高カバレッジ(計測対象範囲内でのカバレッジ基準充足)で始まり、進捗に伴いソースコード量が増えていく中でも、高カバレッジを維持したまま推移する。


Level 2:怪しい:ほぼ常時テスト成功、低カバレッジ

"常時"かつ"全件"、テストが成功してはいるけど、カバレッジが低いプロジェクト。まあ、後付けでテストを書こうとすると、えてしてそんな感じになる。

原因としては、単にスキルがないから仕方なく後付けになったり、確信的にテストをサボっていたり、いろいろあるにせよ、結果的に以下のような感じになる。

①:テスト・コーディングに凄い余計な時間が掛かる。テスト・ファーストでやってさえいれば自然に得られたはずの保守性・メンテ性が備わってない低テスタビリティ・コードに無理やりテストコードを書いてく事になるから、まともな生産性は無理。

② :テスティング・ポイントがウヤムヤになる。本体コードを書いていた時点では脳内にあったはずのコーディング意図がとっくに揮発した状態で、テストコードを後付けする破目になる。何をテストしてるのか本人が分かってない状態。

③ :テストコードがザルになる。①で指摘した生産性の低いテストコードを、②で指摘した曖昧状態のプログラマが書いてくわけだから、「何かをテストする」事ではなくカバレッジを通す事が自己目的化してしまい、結果、Assertion も Expectation も不十分で、単に実行経路に含まれただけの空虚なテストコードになってしまう。

まあ、後付けテストの全てがそんな糞テストコードばかりとは限らないけど、テストコードは本体コードより先に書いとくに越したことはない。それが普通なのだと認識するだけで、悪い事がいろいろ避けられて良い事がいろいろ増えてくる。


Level 1:下手糞:常時テスト失敗、低カバレッジ

低カバレッジでも、取り敢えずテストが存在して差し当たり成功していれば、まだ見どころはある。また、たまたま間違えてテストを壊す事もあると思うが、そんなのすぐ直せば良い事で別に問題じゃない。

だけど、テストが通らないコードをコミットするのが常態化していたり、CIサーバでテストが失敗してるのに放置されていたりしたら、それはかなりの低スキル・チームの疑いがある。

実は、そうしたプロジェクトでは、Level 3 の状態を志してはいたもののスキル不足で健闘虚しくって感じじゃなくて、 Level 1 の状態で普通・正常だと思ってる開発者が大勢を占めていたりする。酷いのになると「今までに経験したプロジェクトではそれが普通だったし、テストが壊れても気にしない方がリファクタしやすい。」なんて事を真顔で主張したりする(リファクタするためにこそテストコードが必要だという最低限の常識を弁えていれば、そうした発言は出ないんだけど…)。

そうやって考えてくと、そもそも技術者の○○使用経験とか○○歴とかって何なのだろうという問題に突き当たるが、これは別の機会に考えてみたい。あと、Level 1 で生じている問題には、余りにも劣化した形で普及し実践されている『リファクタリング』もあるのだけど、これも別の機会に考える。

朝から、気が滅入ってきた。山でも歩いてこよっと。

2011年10月9日日曜日

Eclipse から XSLT 2.0 を使うやり方

今更だけど、Eclipse の XSL Developer Tool で XSLT2.0 を使うやり方が分かったのでメモっとく。
  • まず、Saxon home edition の新しいやつ (saxonhe9-3-0-8j.zip)をダウンロード。
  • ダウンロードしたZIPを適当なとこに展開して、中の saxon9he.jar を出しておく。
  • Eclipse のメニューを [Window]>[Preference]>[XML]>[XSL]>[Java Processors] ってたどる。
  • Add を押して開いたダイアログボックスで、だいたい以下の様な感じで入力。追加したやつのチェックボックスを選択しておく。
これで使えるようになる。 試しに、昔のポストで書いた XSLT で平均と分散を計算するやつを、XSLT2.0 を使ってやってみる。
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output indent="yes" />
  <xsl:template match="/list">
    <xsl:variable name="result">
      <xsl:call-template name="stat">
        <xsl:with-param name="items" select="item" />
        <xsl:with-param name="subtotal" select="0" />
        <xsl:with-param name="count" select="0" />
      </xsl:call-template>
    </xsl:variable>
    <average>
      <xsl:value-of select="$result/avg" />
    </average>
    <varianece>
      <xsl:value-of select="$result/sumOfD2 div $result/count" />
    </varianece>
  </xsl:template>
  <xsl:template name="stat">
    <xsl:param name="items"/>
    <xsl:param name="subtotal"/>
    <xsl:param name="count"/>
    <xsl:choose>
      <xsl:when test="$items">
        <xsl:variable name="t">
          <xsl:call-template name="stat">
            <xsl:with-param name="items" select="$items[position() > 1]" />
            <xsl:with-param name="subtotal" select="$subtotal + $items[1]" />
            <xsl:with-param name="count" select="$count + 1" />
          </xsl:call-template>
        </xsl:variable>
        <sumOfD2><xsl:value-of select="($items[1]-$t/avg)*($items[1]-$t/avg)+$t/sumOfD2"/></sumOfD2>
        <count><xsl:value-of select="$t/count"/></count>
        <avg><xsl:value-of select="$t/avg"/></avg>
      </xsl:when>
      <xsl:otherwise>
        <sumOfD2>0</sumOfD2>
        <count><xsl:value-of select="$count"/></count>
        <avg><xsl:value-of select="$subtotal div $count"/></avg>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>
</xsl:stylesheet>

前回と同じ入力XMLから同じ結果が得られることが確認できた。

前にやった時は、結果ツリーフラグメントの制約から template の結果を文字列にして返さざるを得なくて煩わしかったけど、XSLT 2.0 の temporary tree では 普通の XML データとして扱えるので、やってる事をより直截に表現したコードになった。

2011年9月29日木曜日

リファクタリング本の Cyclomatic complexity

『リファクタリング』 を読んだ人なら、以下のコードに見覚えがあるかもしれない。第一章「最初の例」でのリファクタリング実演のために用意された架空の「長すぎるメソッド(Long method)」で、このメソッドを含む一群のコードを改善する過程で、リファクタ術が披露される。

まあ、実演のためのサンプルだから、本気でバカ長いメソッドを掲載するわけにはいかないだろうけど、それでも全然簡潔ではないし見た目も悪い。

自分なら普通、こういうのを製品コードに残したりはしないけど、それは単に自分にとって綺麗とか汚いといった審美的な感覚だけではなくて、他の開発者のミスリーディングを引き起こしたり生産性を下げたりといった、つまり他人様に迷惑をかける真似はすべきでないという、割と真面目というか普通な社会人的常識からでもあったりする。

  public String statement() {
      double totalAmount = 0;
      int frequentRenterPoints = 0;
      Enumeration rentals = _rentals .elements();
      String result = "Rental Record for " + getName() + "\n";
      while (rentals.hasMoreElements()) {
         double thisAmount = 0;
         Rental each = (Rental) rentals.nextElement();
         
         switch (each.getMovie().getPriceCode()) {
         case Movie.REGULAR:
            thisAmount += 2;
            if (each.getDaysRented() > 2)
               thisAmount += (each.getDaysRented() - 2) * 1.5;
            break;
         case Movie.NEW_RELEASE:
            thisAmount += each.getDaysRented() * 3;
            break;
         case Movie.CHILDRENS:
            thisAmount += 1.5;
            if (each.getDaysRented() > 3)
               thisAmount += (each.getDaysRented()  - 3) * 1.5;
            break;
         }
         frequentRenterPoints ++;
         if ((each.getMovie().getPriceCode() == Movie.NEW_RELEASE) &&
               each.getDaysRented() > 1) frequentRenterPoints ++;
         
         result += "\t" + each.getMovie().getTitle() + "\t" +
         String.valueOf(thisAmount) + "\n";
         totalAmount += thisAmount;
      }
      result += "Amount owed is " + String.valueOf(totalAmount) + "\n";
      result += "You earned " + String.valueOf(frequentRenterPoints) +
      " frequent renter points";
      
      return result;
   }

ところで、このコードの扱いにくさを「計測」するとどうなるか。よく使われている尺度としては、Cyclomatic Complexity があるが、どんなもんだろう。

ちなみに、CheckStyle の解説では、

  • 1-4: considered good
  • 5-7: ok
  • 8-10: consider re-factoring
  • 11+: re-factor now!
とある。

で、実際に測ってみると出てきた複雑度は 9 だった。"consider re-factoring"という事になるから、本の中での実演のネタとしては、正に丁度良かったという事になる。

さらに本に従って最後の段階までリファクタを施したコードの CyclomaticComplexity を測ってみると、結局 2 にまで低減していた。さすがにこれくらいまでやると、変な引け目も後ろめたさもなくソース・コードを共有できる。

で、せっかくある程度客観的にコードの良し悪しがでるのだから、自分だけじゃなくチームにも、簡潔で扱いやすいコードを書く方向で前向きに頑張ってほしいと思うのだけど、いろんな人が集まる現実のプロジェクトではそうも言ってられない場合も多い。

特に、すでにある程度コードが書き貯まったプロジェクトで、途中で CyclomaticComplexity チェックを適用したりなんかすると、「11+」どころか、妥協して15以上とかで設定しても警告が大量発生して、結局「今回は諦めようぜ」って感じになる。

人生って厳しいよね。

2011年9月18日日曜日

依存 jar を変更したらどうなるか

あるアプリケーションがビルド時に参照していた ライブラリの jar を変更して、そのアプリケーション再コンパイルせずに実行したらどうなるか。

====
以下のようなライブラリ・コードを書き、コンパイルして foo.jar に丸めておく。
package p1;

public class Foo {
  public String bar() {
    return "hello";
  }
}
で、このライブラリを使う以下のようなクライアントコードを書いて、-cp に foo.jar を指定してコンパイルする。
package p2;

import p1.Foo;

public class Client {
  public static void main(String[] args) {
    System.out.println(new Foo().bar());
  }
}

コンパイルされたクラスを java コマンドで -cp に foo.jar を指定して実行すると、標準出力に hello と出力される。


さて、ここで クラス Foo を変更して、つまり foo.jar の中身を変えて、再コンパイルせずに再度 Client#main() を実行したらどうなるか。

(1) メソッドの中身を変えてみる

  public String bar() {
    return "good-bye";
  }
問題無し。標準出力に good-bye と表示される。

(2) メソッドの名前を変えてみる

  public String Bar() {
    return "hello";
  }
メソッド p1.Foo.bar()Ljava/lang/String が見当たらないって事で、NoSuchMethodError が送出される。リフレクションのコーディングでよく catch したりするNoSuchMethodExceptionではなく、NoSuchMethodError が投げられてきた。

(3) メソッドの引数を変えてみる

  public String Bar(String s) {
    return "hello";
  }
これは (2) と同じ

(4) メソッドの戻り値を変えてみる

  public Object Bar() {
    return "hello";
  }
これも (2) と同じ。戻り値も識別される。

(5) throws を追加してみる

  public String bar() throws IOException {
    throw new IOException("test");
  }
これは意外にも普通に実行されて、IOExceptionが送出されてスタックトレースされる。意外というのは、これを再コンパイルしようとすると、コンパイルエラーが出るからで、Client#main() に throws を追加するか、bar() を try-catch で囲むかしないと、コンパイルが通らない。でも、コンパイルは通らなくても実行時のメソッド呼び出しは成功する。

(6) メソッドの可視性を変えてみる

  private String bar() {
    return "hello";
  }
これは IllegalAccessError が送出される。つまり、一応 p1.Foo.bar()Ljava/lang/String の存在は識別された上で、アクセスに失敗して例外が発生した模様。

(7) クラスの可視性を変えてみる

class Foo {
…
(6) と同様だが、クラス p1.Foo の存在を識別したあとに、アクセスするところで失敗している。

(8) 定数を変えてみる

メソッドは以上のような感じで、フィールドもだいたい想像がつく。で、ここでちょっと定数を試してみる事にする。

まずライブラリコードを以下のように変える。

package p1;

public class Foo {
  public static final int C = 100;
}
次に、クライアントコードを以下の様に変える
package p2;

import p1.Foo;

public class Client {
  public static void main(String[] args) {
    System.out.println(Foo.C);
  }
}

両方コンパイルして実行すると、標準出力に100が実行される。

ここでライブラリコードを以下の様に修正し、jar を作り直す。

  public static final int C = 200; 
で、クライアントコードを再実行すると、標準出力に 200 が出力されると思いきや、実際には変更前と同じ100が出力される。
javap で見てみると、バイトコードに定数 100 が埋め込まれているのがわかる。なるほど定数はこういう扱いらしい。
public static void main(java.lang.String[]);
  Code:
   0: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream;
   3: bipush 100
   5: invokevirtual #3; //Method java/io/PrintStream.println:(I)V
   8: return 

(9) null値 を試してみる

ライブラリ・コード を以下のように変更して、両方とも再コンパイルして実行してみる。
package p1;

public class Foo {
  public static final String C = null;
}
標準出力には、null が出力される。

ここでライブラリ・コードを、public static final String C = "a"; に変更して、クライアントを実行してみる。

(8) の結果から、null が出力される結果を類推してしまうが、実際には "a" が出力される。理由は、Java 言語仕様として null 定数として扱われないためコンパイル時には byte コードに直接書き込まれず、実行時に初めて Foo.Cの値を読むことになるかららしい。javap は以下のようになる。
public static void main(java.lang.String[]);
  Code:
   0: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream;
   3: getstatic #3; //Field p1/Foo.C:Ljava/lang/String;
   6: invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
   9: return