[tech]チケット駆動開発

id:kunitさんの[Git] gitだからこそできるチケット駆動開発のやり方を読んでたら、自分たちもチケット駆動開発的なものをやっていることに気がつきました。せっかくなので、ちょっと紹介します。

構成管理はCVSBTSはJIRAを使っています。チケット駆動ということを意識してはいませんが、ルールとして「構成管理にコミットするときには、コミットログにチケットIDと変更概要を記述する」ということを守っています。

日々の開発

日々の開発では、まず機能(ユースケース)ごとにチケットを起票します。このチケットは、管理タスクとして使用します。JIRAでは1レベルのサブタスクを定義する事ができます。この機能を利用して数日で達成できる単位(どちらかというと、CVSのコミットに都合の良い単位や開発者にアサインしやすい単位)で、サブタスクにチケットを分割していきます。サブタスクは、タスクが発生したり、おもいつく度に作成し、作業終了したらクローズします。

開発に着手すると、管理者が開発者にチケットをアサインします(開発者が自分でとりにいくのも可)。開発者は、設計を行い、ローカル環境で実装し、単体テスト終了後、CVSにコミットします。このときコミットログにチケットIDと変更概要を記述します。レビューもチケットベースで行っており、開発者がCVSにコミットした後、レビュー依頼としてチケットをレビュー担当にアサインしています。レビュー担当者は、コミット内容を確認し、レビュー記録をJIRAにまとめます。このチケットを開発者にアサインしています。開発者はコードを修正し、再レビューを依頼したり、単体テストを実施します。このあと、チケットをQA担当にアサインします。QA担当は、機能テストを実施し、不具合がなければ、チケットをクローズします。もし、不具合が見つかれば、QA担当はJIRAに不具合を報告し、開発者にチケットをリアサインします。

バグが発生したら、報告者は必ずチケットを起票し、不具合をJIRAにまとめます。報告したらすぐに、調査依頼として開発者にチケットをアサインします。開発者はバグ原因を調査し、内容をJIRAにまとめ、管理者にアサインします。管理者はバグの内容を精査し、バグの重大度、修正工数から、バグの修正時期を検討します。

バグを修正する場合、管理者が開発者にチケットをアサインします。バグを修正し、単体テストまで実施後、CVSにコミットします。コミット時にチケットIDと変更内容の概要をコミットログに記述します。コミット後、QA担当にチケットをアサインします。QA担当は機能テストを実施し、バグの修正を確認したら、チケットをクローズします。

チケット駆動開発を行ってみて、良かった事

チケット駆動開発っぽいものですが、良かったことは、

  • コミットログから、リリース物の内容を確認できるようになった。
  • コミット物がチケットIDとひもづいているので、あとから変更履歴を追いかけるのが簡単になった。
  • JIRAを見ればソースコードの変更の経緯がわかるようになった。
  • 個人が着手しているタスクが明確になった。
  • 情報がJIRAに集まるため、ますますJIRAに情報が集まるようになった。
  • いつかやらないと行けない仕事もJIRAにチケットとして起票するので、わすれにくくなった。

といった事ですね。また、オープンされたチケットが徐々に減ることで、プロジェクトが進んでいる感じがするのも良いところですね。

チケット駆動開発で注意すること

とは言っても、注意しないと行けない点もそれなりにあります。

大量のチケット保持

起票したチケットが一人の人に大量にアサインされると、管理されなくなるので注意が必要です。一人に100を超えるチケットがアサインされたら、たいてい管理されなくなり、重要な仕事が埋もれてしまったり、忘れ去られてしまったりしています。なるべくこういうことがないように、担当者ごとの保持チケット数は、常にみるようにしましょう。

ゴールの見えないチケット

ゴールの見えないチケットは、進捗が見えない上、いつまでたってもクローズされないので注意しましょう。これは、開発初期の頃良くありました。大きなタスクは数日で達成可能なサブタスクに分割するか、いっそのこと新しいタスクを定義しましょう。

有効期限切れのチケット

起票していたときには意味があったけど、今はもう意味がないチケットが半年もすると溜まってきます。これが有効期限切れのチケットです。有効期限切れのチケットは、定期的にチケットを見直してクローズしています。

厳密なワークフロー

JIRAのように自由にワークフローを定義できるツールを使うとありがちなのですが、大きく、複雑で、厳密なワークフローを定義したくなります。これは出来上がったときは、ホレボレするほど美しいものだったりするのですが、運用にはいると必ず失敗します。厳密すぎて、面倒になり、誰もチケットを起票しなくなルノです。ワークフローはシンプルに、これが鉄則です。シンプルであれば、開発者が工夫して勝手に自分たちに合う形にカスタマイズしてくれます。

どんな開発に向いている?

個人的な印象ですが、1、2ヶ月と短く、頻繁にリリースするプロジェクトに良く合っている気がします。チームの規模も中規模程度、10数人程度までがチケット数が爆発しないためか、合っているような気がします。

当たり前のことばかりかもしれませんが、参考になりますでしょうか?

[book]人生を好転させる「新・陽転思考」

人生を好転させる「新・陽転思考」

人生を好転させる「新・陽転思考」

人生を好転させる「新・陽転思考」を読みました。なかなか良かった本なので、ここで紹介します。
まずは、iTunes Music Storeに行っていただいて、和田裕美さんのWada cafe PodCastの9/5分を聞いてみてください。15:16ごろから、著者ご本人によるはじめに章の朗読があります。朗読を聞いてみてください。著者の思いが伝わる朗読です。本は買わなくても、これだけでも聞く価値がありますので、ぜひ聞いてみてください。僕は著者の思いがダイレクトに伝わってきます。
内容は単純です。物事の捉え方は二つあります。良くない状況でもいいことを見つけるか、悪い事をそのまま受け止めるか?その二つしかないなら、良い方向を考えてみませんか?たったこれだけです。それがいいのかわるいのか僕にはわかりません。ただ、著者の和田さん曰く、どうせ選択するならハッピーな方を選択しようぜというのがこの本の考え方です。
これで運が好転するかどうかはわかりません。だけど、どうせ選択するならハッピーな方を選択しようという考え方はいいと思っています。もし、マイナスなことを考えることが得意なら、ハッピーな事を考えることも同様にできるはずです。この考え方を身につけて前向きに人生をとらえてみませんか?

[tech]HTTP GETメソッドのURIの長さ制限を調べてみた

Webの開発者をやっていると良くいわれていることですが、HTTP GETメソッドを使うなという話を聞きます。セキュリティの点もありますが、HTTP GETメソッドですべてのパラメータをサーバに送れないからというのがその理由のようです。
実際どこからでているのか、現在もそうなのか?という疑問を感じたので、HTTP GETの制限を調べてみました。

RFCではどうなっているの?

まずHTTP1.1のRFC2616を調べました。
とりあえず、日本語訳がありましたので、引用いたします。http://www.studyinghttp.net/cgi-bin/rfc.cgi?2616より

HTTP プロトコルでは、URI の長さにどんな制限も設けていない。 サーバは、自身が持つどんなリソースのURI も扱えなければならないし、もしそのような URI を生成する GET ベースのフォームを用意するなら、無制限の長さの URI を扱えるべきである。 もし、その URI がサーバが処理できるものよりも長ければ、サーバは 414 (Request-URI Too Long) ステータスを返すべきである (section 10.4.15 参照)。


注: いくつかの古いクライアントやプロクシ実装は 255 バイトを超える長さを持つ URI を適切にサポートしていないかもしれないので、サーバはそのような URI に頼る場合は注意を払うべきである。

つまりHTTP GETメソッドにはRFC上、URIに255byteの制限はないのですね。良くいわれているような255byte以上送信できないというのは、ここから来ている誤解ですね。
現実問題、古いクライアントやプロクシの実装を気にする必要がないでしょうから、255バイトというのは気にする必要がないでしょう。

各Webサーバはどうなっているの?

RFC上は、HTTP GETのURIの長さに上限がないことがわかりました。では、Webサーバでは、どこまで扱えるのでしょうか?

apache2.2

apache2.2ではデフォルトは、8190バイトがデフォルトです。しかし、LimitRequestLineを設定することで、変更可能です。http://httpd.apache.org/docs/2.2/mod/core.html#limitrequestlineより引用すると、

このディレクティブは、HTTP リクエスト行内で許容されるバイト数 bytes を指定します。

LimitRequestLine ディレクティブにより、 クライアントからの HTTP リクエスト行の許容サイズを増減できます。 リクエスト行は、HTTPメソッド、URIプロトコルバージョンから成っており、LimitRequestLine はサーバへのリクエストに対して 許容するリクエスURI の長さを制限することになります。 サーバは、GET リクエストのクエリ部分も含めて、リソースの名前が入るに足る大きさを必要とします。

ということで、URIの上限をさらに大きくすることも可能です。

IIS4.0, 5.0

IISはURL Scanをインストールしているかどうかで設定が変わります。レジストリを設定することで、URLの長さを指定することができるようになっています。
http://technet.microsoft.com/ja-jp/library/bb878118.aspxによるとURL Scanをインストールしている場合は、MaxQueryString 2048文字、MaxUrl260文字が上限です。URL Scanをインストールしていない場合は、MaxClientRequestBufferによりIISへのリクエストの総バイト数が指定でき

IIS 4.0 2MB
IIS 5.0 128k
IIS 5.0 Windows 2000 Server SP4 16k

となります。

ブラウザはどうなっているの?

いくらサーバで受け付けられたとしても、ブラウザからリクエストを送信できなければ意味がありません。それではブラウザには制限がないのでしょうか?

IE

IEMicrosoftがURLの上限に関する情報を公開しています。http://support.microsoft.com/?id=208427によると、

Internet Explorer では、URL (Uniform Resource Locator) に使用できる最大文字数は 2,083 文字です。

GET メソッドを使用する場合、最大文字数は 2,083 文字に制限されます (実際のパスも含めた文字数)。

IE3.0〜IE8.0までが対象となっていますので、ほぼすべてのバージョンで2083文字以上の指定ができないと考えた方が良いですね。

Safari

仕様やドキュメントらしいものが見当たらなかったので、TextAreaにデータを入れて、送信して動作を確認しました。
Safari v4.0.3 Mac OSX 10.6.1でテストさせてみましたが、1Mbyteまでは送信可能でした。おそらく制限はないと思われます。

Firefox

仕様らしいものが見当たらなかったので、TextAreaにデータを入れて、送信して動作を確認しました。
Firefox v3.0.11 Mac OSX 10.6.1でテストさせてみましたが、1Mbyteまでは送信可能でした。おそらく制限はないと思われます。
Firefox v3.5.3 でもテストしたのですが、TextAreaにデータを貼付ける方法では1Mbyte程度でブラウザが固まってしまいました。

フレームワーク

Google App Engine(Python)

みんな大好きなGoogle App Engineですが、制限は2047バイトです。dev_appserver.pyのMAX_URL_LENGTHを参照してみてください。

未調査

まとめ

まとめます。

HTTP1.1の仕様上はHTTP GETで転送できるデータ量(=URIの長さ)には上限がありません。Webサーバでデフォルトは制限されていますが、設定で変更することが可能です。しかし、IEにはURI長が2083文字という制限があるため、実質的な上限は2083文字となります。

制限が小さいせいで、IEデファクトスタンダードになっているのですね。勉強になりました。
間違いなどありましたら、コメントにてご連絡をお願いします。

インタープリタのスタックフレームにアクセスし、メソッドの呼び出し元のオブジェクトを取得する

いまさらながらPythonにハマっています。Google App Engineが超楽しいですね。さて今日は、GAEではなくて、Pythonの言語機能を紹介します。環境はPython2.6.1、Mac OSX 10.6.1で試しました。

Pythonではinspectモジュールを使うことで、インタープリタのスタックフレームにアクセスすることができます。これを使って、Pythonで呼び出し元のオブジェクトを取得してみます。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import inspect

def caller():
  try:
    # フレームレコードのリストを取得する
    framerecords = inspect.stack()

    # 二つ前のフレームレコードを取得する
    fremerecord = fremerecords[2] 

    # タプルからフレームを取得する
    frame = framerecord[0]

  # 指定したフレームの引数の情報を取得する
    arginfo = inspect.getargvalues(frame)

  # localsの辞書に格納されたselfオブジェクトを取得する
    return arginfo.locals['self'] if 'self' in arginfo.locals else None
  finally:
    del frame
  return None

class Hoge():
  def methodA(self):
    obj = caller()
    print obj # <__main__.Foo instance at 0x1d5620>
    obj.hoo()

class Foo():
  def __init__(self):
    self.hoge = Hoge()

  def foo(self):
    self.hoge.methodA()

  def hoo(self):
    print "hoo"

foo = Foo()
foo.foo()

caller()メソッドが、呼び出し元のオブジェクトを取得するメソッドです。それではソースコードをみてみます。

最初に呼び出し元スタックのフレームレコードのリストを取り出します。

    # フレームレコードのリストを取得する
    framerecords = inspect.stack()

callerメソッド内からみると取得したいスタックフレームレコードは2つ前のスタックフレームレコードなので、framerecords[2]でメソッドを呼び出したフレームレコードが取得できます。

    # 二つ前のフレームレコードを取得する
    fremerecord = fremerecords[2] 

このフレームレコードは、は長さ6のタプルで、このような構成になっています。

(フレームオブジェクト, ファイル名, 実行中の行番号, 関数名, コンテキストのソース行のリスト, ソース行リストの実行中行のインデックス)

フレームレコードからフレームオブジェクトを取得します。

    # タプルからフレームを取得する
    frame = framerecord[0]

フレームオブジェクトから、指定されたフレームに渡された引数の情報を取得します。

  # 指定したフレームの引数の情報を取得する
    arginfo = inspect.getargvalues(frame)

引数の情報は、長さ4のタプルで次の値を返します:
(引数, キーワード引数, 引数の名前, ローカル変数の辞書)

そこからローカル変数の辞書からselfと呼ばれているオブジェクトを取得すると、呼び出し元のオブジェクトが取得できます。

  # localsの辞書に格納されたselfオブジェクトを取得する
    return arginfo.locals['self'] if 'self' in arginfo.locals else None

最後に循環参照にならないようfinallyブロックでフレームを削除したら処理完了です。

実行結果はfoo->hoge->hooと呼び出しています。

使い道はあんまりなさそうだなあ。ダブルディスパッチをcalleeを渡さずに実行できるぐらいかも。
javaでできるとフレームワークの制限を打ち破れるので結構楽しいかも。

[Scala] java.lang.Dateクラスの最も小さなオブジェクトEarlyTimeを定義する

今日は、Javaのオブジェクトを継承して、Scalaのシングルトンオブジェクトを作成してみます。題材は、RakeのEarlyTimeクラスを使ってみます。
EarlyTimeクラスはすべてのDateオブジェクトの中でもっとも小さいオブジェクトで、どんな小さなDateオブジェクトと比較しても必ず小さくなるシングルトンのValueObjectです。

まず、java.lang.Dateを継承して、Scalaのシングルトンを作成します。

import java.util.Date
object EarlyTime extends Date {
  override def compareTo(other:Date) = {-1}
  override def toString = {"<EARLY TIME>"}
}

次に、DateクラスのcompareToを拡張するMixinを定義します。

trait DateMixin extends Date {
  override def compareTo(other:Date) = {
    if (EarlyTime == other) {
      -(other.compareTo(this))
    } else {
      super.compareTo(other)
    }
  }
}

DateMixinでは、compareToメソッドをオーバーライドし、EaryTimeオブジェクトを引数に取るときに、EarlyTimeオブジェクトのcompareTo演算子を呼び出します。また、EarlyTimeオブジェクトと異なる場合には、元のDateオブジェクトのcompareToを呼び出します。

では、実際に評価してみます。

scala> EarlyTime.compareTo(new Date(java.lang.Long.MIN_VALUE))
res1: Int = -1

scala> EarlyTime.compareTo(new Date(java.lang.Long.MAX_VALUE))
res2: Int = -1

scala> 
scala> (new Date with DateMixin).compareTo(EarlyTime)
res3: Int = 1

scala> (new Date(0) with DateMixin).compareTo(EarlyTime)
res4: Int = 1

scala> (new Date(java.lang.Long.MIN_VALUE) with DateMixin).compareTo(EarlyTime)
res5: Int = 1

確かにEarlyTimeは常に小さいオブジェクトになっています。

これだとDateクラスの宣言が使いにくいので、companionオブジェクトを定義します。Scalaではclassと同名のobjectをcompanionオブジェクトと呼び、クラス生成時の便利メソッドを提供する仕組みを用意しています。たとえば、

scala> List(1,2,3)
res1: List[Int] = List(1, 2, 3)

なんかもcompanionオブジェクトだったりします。

では、applyメソッドを持つDate companionオブジェクトを定義してみましょう。

object Date {
  def apply(xs: java.lang.Long) : Date = {
    new Date(xs.longValue()) with DateMixin
  }

  def apply():Date = {
    new Date() with DateMixin
  }
}

java.lang.Longを引数にとるapplyメソッドと、引数なしのapplyメソッドを定義しました。このapplyメソッドは一致する型のメソッドが暗黙的に呼び出されます。

scala> Date(1L)
res18: java.util.Date = Thu Jan 01 09:00:00 JST 1970

scala> Date()
res19: java.util.Date = Sat Apr 11 11:53:04 JST 2009

最後にListでソートしてみましょう。

scala> List(Date(0L), Date(2000L) , EarlyTime, Date() ).sort((e1,e2) => (e1 compareTo e2) > 0)
list: List[java.util.Date] = List(Sat Apr 11 11:53:43 JST 2009, Thu Jan 01 09:00:02 JST 1970, Thu Jan 01 09:00:00 JST 1970, <EARLY TIME>)

正しくソートされましたね。
今日は、java.lang.Dateクラスを拡張し、ScalaのEarlyTimeクラスを定義してみましたがいかがでしょう?なんとなくScalaの拡張性の高さが伺えるサンプルとなったのではないでしょうか?

[tech] Web開発者がIE8をインストールしたら一番最初にすること

IE8が公開され、そろそろ2週間が経ちました。開発中のサイトをIE8でテストしたり、既存サイトの互換性確認などいたるところで行われているところではないでしょうか?

IE8をインストールしたら、開発やテストをする前に最初に確認するべき項目があります。
それは、IE8の互換モードの設定です。
IE8から、互換モード(IE6,7相当)と、標準モード(Web標準)という2種類のレンダリングエンジンが搭載されるようになりました。しかしなんたることか、デフォルトの設定では、イントラネットは互換モードで動作し、インターネットは標準モードで動作するという設定になっているのです。これでは、ローカル環境で開発/テスト中は、互換モードでテストされ、ステージングや本番環境に上げたら標準モードでアクセスしてしまい、何をテストしたかわからないという状況になってしまうわけです。
マイクロソフトからすれば、既存サイトへの影響を考え「イントラネットは互換で、インターネットは標準で」ということなのでしょうが、デベロッパーからすれば2種類のモードを意識しなければならず、逆に使いにくいことになっています。

さてこのデフォルトの設定変更は、下記のように変更できます。

ツール->互換表示設定を選択し、互換表示設定画面にて、互換モードでイントラネットサイトを表示するのチェックを外す

今後IE8で開発する人は、是非この設定に注意してください。
ちなみに、僕は互換モードのデフォルトの振る舞いを知らずにハマってしまいました。
#もしかして、IE8を使う人には常識?

[Scala] HashMapを拡張する

今日はScalaのHashMapを拡張してみます。
多くのJavaプログラマが不満に思っていると思うのですが、Javaではほとんどのクラスがfinalで定義されていることもあり、既存のクラスを拡張することができませんでした。Scalaの良いところは、既存のライブラリも簡単に拡張できることです。

まず、HashMapを使ってみます。Symbolをキーとして、Intを値に持つHashMapを使ってみます。

scala> var hashMap1 = new HashMap[Symbol, Int]
hashMap1: scala.collection.mutable.HashMap[Symbol,Int] = Map()

scala> hashMap1.put('hoge, 1)
res6: Option[Int] = None

scala> hashMap1.put('foo, 2)
res7: Option[Int] = None

scala> hashMap1
res8: scala.collection.mutable.HashMap[Symbol,Int] = Map('hoge -> 1, 'foo -> 2)

scala> hashMap1.contains('hoge)
res9: Boolean = true

scala> hashMap1.contains("hoge")
<console>:7: error: type mismatch;
 found   : java.lang.String("hoge")
 required: Symbol
       hashMap1.contains("hoge")
                         ^

キーの型をSymbolとStringを間違えてしまいました。Stringでアクセスすると、型が異なるので、評価することができませんでした。StringとSymbolは相互にアクセスできてもいいですね。
ということで、StringでもSymbolでもアクセスできるStringAccessHashMapを作成してみます。

import scala.collection.mutable.HashMap

class StringAccessHashMap[T] extends HashMap[Symbol,T] {
  def put(key: String, elem:T) : Option[T] = { super.put(Symbol(key), elem) }

  def contains(key : String) : Boolean = { super.contains(Symbol(key)) }

  def get(key : String): Option[T] = { super.get(Symbol(key)) }
}

内容は簡単です。キーとしてSymbol型を持つHashMap型を継承し、Stringをキーとしてアクセスするインタフェースを追加しただけです。では実際に使ってみましょう。

scala> var hashMap = new StringAccessHashMap[Int]
hashMap: StringAccessHashMap[Int] = Map()

scala> hashMap.put("hoge", 10)
res0: Option[Int] = None

scala> hashMap.get("hoge")
res1: Option[Int] = Some(10)

scala> hashMap.get('hoge)
res2: Option[Int] = Some(10)

scala> hashMap.contains('hoge)
res3: Boolean = true

scala> hashMap
res4: StringAccessHashMap[Int] = Map('hoge -> 10)

お、これは便利かもしれない。