Baseunits Library

さて、Java Advent Calendar -ja 2010 : ATND 10日目。昨日は、id:yuroyoro でした。二日連続で真っ黒な魔術が紹介されたので、ここは真っ白で実用的な奴をひとつ。

最近Domain Driven Design(DDD)っていう設計手法が、自分の周辺一部で話題になっている。当然、賛否両論なんだけども*1、個人的には好きな考え方でして。ま、詳細は色々な方がブログに書いているので割愛します。興味あれば本読んでみましょう。洋書*2だけどw

Domain-Driven Design: Tackling Complexity in the Heart of Software

Domain-Driven Design: Tackling Complexity in the Heart of Software


で、前置きはここまで。本題いきます。

Javaで「時間」や「日付」を扱う時、みんなどうしてます? まぁ、大抵 java.util.Date とか java.util.Calendar を使うんじゃないかな、と思います。でもこのJavaの時間ライブラリっつーのは、意外とあちこちでdisられがちです。Deprecatedなメソッドだらけだし、Mutable(可変)だったり、継承関係が無茶だったり。まぁ正直使いにくい。

例えば2010年12月22日っていう暦日(日付)を扱いたいだけなのに、「2010/12/22 00:00.00 を java.util.Date で扱って時間部分を無視」とかしてませんか? 時間要素のない、単なる「暦日(ex. 2010/12/22等)」クラス欲しくないですか? 逆に日付要素のない、単なる「時刻(ex. 16:10等)」クラス欲しくないですか? Immutable(不変)なクラスとか欲しくないですか? 「期間(ex. 2010/09/20〜2010/12/22等)」とか「時間量(ex. 3分間, 4ヶ月間等)」クラス欲しくないですか?(ry

昔から、なぜApache commons-dateみたいなライブラリが無いのか、不思議でならんかった。まぁ今でも不思議だ。commons-langにDateUtilsってクラスがあって、少々便利なユーティリティを提供してくれているけど、まぁ全く根本的な解決になっていない感じ。

しかしまぁ、このイケてなさはJavaユーザの間ではほぼ共通認識ではあるようで。JodaTimeやら、それを元にしたJSR-310やらで、日時関連のAPIを整備しようという流れはあるわけです。ただ、このJodaTimeも結構曲者。いわゆる過モデリングなんだと思いますが、あらゆる可能性とニーズに対応しようとするあまり、通常は要らない概念がもりもり入っていて、まぁそれはそれで使いにくいらしい。使ったことないけど。

で。突然だがTime and Money LibraryっていうOSSプロジェクトがあるのです。Javaにおける時間及び金額をモデリングしたクラスライブラリだ。DDDの実例コードという名目で世に出たコード(DDD著者=Time and Money作者=Eric Evans)だけど、個人的に、普通にライブラリとしても有用なコードだと感じた。

しかし残念なことに、Domain Language, Inc.のページを見るところによると「このプロジェクトはもはやアクティブではない(Although this project is no longer active)」と明記されちゃっている。実例コードであることが大事で、実際に使うことをもう既にあまり意識しておらず、メンテナンスを続ける感じじゃないのだと思う。

あぁ、もったいない。中身を見てみると、DDD的に深く考えぬかれた設計が満載ではないか。ということで、このコードをいろいろいじくってみた。

まずね、Javadoc無いとかあり得ないから。スザケンナって思いながら全部にひと通りドキュメントつけました。日本語*3ですけどね。俺が意味を取り違えていて、間違ったことを書いているかもしれないし、ぶっちゃけ未だに意図がよくわからないクラス*4もあるw しかし、それでもとても便利だと思う。

あと、元コードはJava1.4対応だったのでジェネリクス使ってなかったんですね。で、自分なんかはJava5が出た後にJavaを始めた人間なんて、ジェネリクスの無いコードなんて怖くて触れねーよ、的なビビリな訳ですよ。というわけで必要なモンに型パラメータつけました。

また、メソッド名に統一感がない部分があったり、テスト書き足してみたり、FindBugs掛けてみたら赤いところがバグってたので直してみたり。あ、あんな機能欲しいww とか、普通にクラスも追加したりしてます。オーバーホールっていうか、ずいぶん改造しやがったな、というのが近いかもしれない。

というわけで、再構成したものを派生プロダクトとして公開します。あー、OSSってすばらしい。そしてこの派生プロダクトはBaseunits Libraryと名づけますよ。みんな覚えといてね!

まぁ、論よりコードだな。

基本的な日付演算

元プロダクトが舶来なので、アメリカーンな例ですが。

// 暦日仕様。ある暦日がその仕様を満たすかどうか、とかチェックできる
// ここでは「毎年1/15」という仕様、つまりここではキング牧師の誕生日。
// (他にも「毎月第二木曜日」とかもいける。
//  さらに、その日が祝日だったら前営業日ずらしとかも可能。)
DateSpecification mlkBirthday = DateSpecification.fixed(1, 15);

// で、2005/1/15という日付のインスタンスを作る
CalendarDate jan15_2005 = CalendarDate.from(2005, 1, 15);
// この日はMLKの誕生日ですか? → true
assertThat(mlkBirthday.isSatisfiedBy(jan15_2005), is(true));

// 2005年の中(期間=CalendarInterval)で、キング牧師の誕生日を引っ張って来る。
// (仕様を満たす暦日を探す)
CalendarDate mlk2005 = mlkBirthday.firstOccurrenceIn(CalendarInterval.year(2005));
assertThat(mlk2005, is(jan15_2005));

// 誕生日〜逝去日の暦日期間をつくって、その範囲で仕様を満たす暦日を順次取得するイテレータ
CalendarInterval mlkLifetime = CalendarInterval.inclusive(1929, 1, 15, 1968, 4, 4);
Iterator<CalendarDate> mlkBirthdays = mlkBirthday.iterateOver(mlkLifetime);
assertThat(mlkBirthdays.next(), is(CalendarDate.from(1929, 1, 15)));
assertThat(mlkBirthdays.next(), is(CalendarDate.from(1930, 1, 15)));

// っていうか、そもそもキング牧師って何日間生きたの?
assertThat(mlkLifetime.length(), is(Duration.days(14325)));

営業日を考慮するカレンダー

何に使うかピンと来ない? んじゃ、日本のビジネスに密着した例いってみましょう。

BusinessCalendar calendar = new BusinessCalendar();

// 祝日の登録(この登録は、JapaneseBusinessCalendar とかサブクラスつくってコンストラクタで処理しちゃうといいかも)
calendar.addHolidaySpec(DateSpecification.fixed(1, 1)); // 元旦
calendar.addHolidaySpec(DateSpecification.nthOccuranceOfWeekdayInMonth(1, DayOfWeek.MONDAY, 2)); // 成人の日
calendar.addHolidaySpec(DateSpecification.fixed(2, 11)); // 建国記念日
calendar.addHoliday(CalendarDate.from(2010, 3, 21)); // 春分の日(毎年違うので、ひとまず今年分を単独で登録)
calendar.addHolidaySpec(DateSpecification.fixed(4, 29)); // 昭和の日
calendar.addHolidaySpec(DateSpecification.fixed(5, 3)); // 憲法記念日
calendar.addHolidaySpec(DateSpecification.fixed(5, 4)); // みどりの日
calendar.addHolidaySpec(DateSpecification.fixed(5, 5)); // こどもの日
calendar.addHolidaySpec(DateSpecification.nthOccuranceOfWeekdayInMonth(7, DayOfWeek.MONDAY, 3)); // 海の日
calendar.addHolidaySpec(DateSpecification.nthOccuranceOfWeekdayInMonth(9, DayOfWeek.MONDAY, 3)); // 敬老の日
calendar.addHoliday(CalendarDate.from(2010, 9, 23)); // 秋分の日(毎年違うので、ひとまず今年分を単独で登録)
calendar.addHolidaySpec(DateSpecification.nthOccuranceOfWeekdayInMonth(10, DayOfWeek.MONDAY, 2)); // 体育の日
calendar.addHolidaySpec(DateSpecification.fixed(11, 3)); // 文化の日
calendar.addHolidaySpec(DateSpecification.fixed(11, 23)); // 勤労感謝の日
calendar.addHolidaySpec(DateSpecification.fixed(12, 23)); // 天皇誕生日

// それぞれの日が「営業日」にあたるかどうかチェック。
assertThat(calendar.isBusinessDay(CalendarDate.from(2010, 10, 8)), is(true)); // 金曜日
assertThat(calendar.isBusinessDay(CalendarDate.from(2010, 10, 9)), is(false)); // 土曜日
assertThat(calendar.isBusinessDay(CalendarDate.from(2010, 10, 10)), is(false)); // 日曜日
assertThat(calendar.isBusinessDay(CalendarDate.from(2010, 10, 11)), is(false)); // 月曜日だけど体育の日
assertThat(calendar.isBusinessDay(CalendarDate.from(2010, 10, 12)), is(true)); // 火曜日

assertThat(calendar.isBusinessDay(CalendarDate.from(2010, 11, 22)), is(true)); // 月曜日
assertThat(calendar.isBusinessDay(CalendarDate.from(2010, 11, 23)), is(false)); // 火曜日だけど勤労感謝の日
assertThat(calendar.isBusinessDay(CalendarDate.from(2010, 11, 24)), is(true)); // 水曜日

// 振替休日(「国民の祝日」が日曜日にあたる場合、その直後の「国民の祝日」でない日を休日とする)とか、
// 国民の休日(「国民の祝日」と次の「国民の祝日」の間隔が中1日しかなくその中日(なかび)が
//「国民の祝日」でない場合、その日を休日とする)
// なんかには、まだ対応していないけど、DateSpecificationを上手く実装すればどうにかならんかな、と思っている。

// 2010/10/1〜2010/11/30までの間で、営業日だけを返すイテレータ
Iterator<CalendarDate> itr =
        calendar.businessDaysOnly(CalendarInterval.inclusive(2010, 10, 1, 2010, 11, 30).daysIterator());

// 1つ1つ検証するの大変なので、とりあえず文字列にしちゃうよ
StringBuilder sb = new StringBuilder();
while (itr.hasNext()) {
    CalendarDate calendarDate = itr.next();
    sb.append(calendarDate).append(" ");
}

// 土日祝日を除いた日が列挙されている
assertThat(sb.toString(), is("2010-10-01 " +
        "2010-10-04 2010-10-05 2010-10-06 2010-10-07 2010-10-08 " +
        "2010-10-12 2010-10-13 2010-10-14 2010-10-15 " +
        "2010-10-18 2010-10-19 2010-10-20 2010-10-21 2010-10-22 " +
        "2010-10-25 2010-10-26 2010-10-27 2010-10-28 2010-10-29 " +
        "2010-11-01 2010-11-02 2010-11-04 2010-11-05 " +
        "2010-11-08 2010-11-09 2010-11-10 2010-11-11 2010-11-12 " +
        "2010-11-15 2010-11-16 2010-11-17 2010-11-18 2010-11-19 " +
        "2010-11-22 2010-11-24 2010-11-25 2010-11-26 " +
        "2010-11-29 2010-11-30 "));

業態によるカスタマイズも結構できる感じ。

// 不動産屋さんって、日曜祝日も営業してるけど、水曜日はお休みだよね
// この場合isHolidayがtrueでも、isBusinessDayがfalseだとは限らない
BusinessCalendar estateCalendar = new BusinessCalendar() {

    @Override
    public boolean isBusinessDay(CalendarDate day) {
        Validate.notNull(day);
        DayOfWeek dow = day.dayOfWeek();
        return dow != DayOfWeek.WEDNESDAY;
        // デフォルト実装は return isWeekend(day) == false && isHoliday(day) == false;
    }
};

その他

自分が注目してるのは時間ライブラリの方なので、金額は省略。まぁ、同じノリですよ。

まぁ、こんな事やってたから「区間」があーだこーだとか言い出した訳なんですね、俺。

基本情報

使ってみようか、と思ったらこちら。

v1.x系 v2.x系
プロジェクトサイト http://maven.tricreo.jp/site/baseunits http://maven.xet.jp/site/baseunits
Mavenリポジトリ http://maven.tricreo.jp/release http://maven.xet.jp/release
Maven groupId jp.tricreo jp.xet
Maven artifactId baseunits baseunits
ソース置き場 https://github.com/tricreo/baseunits/ https://github.com/dai0304/baseunits/
ライセンス Apache License v2.0 Apache License v2.0

今後

今はひとまず v1.0 をリリースしてあります。単体テストも結構書きましたし、今後もきっちりメンテナンスを続けていく予定*5です。というわけで比較的安心して長期的に使ってもらえるライブラリだと思います。

ただ、今の時点でかなり安定したプロダクトだと思うので、みんながイメージするOSSほど頻繁なアップデートは無いかもしれません。決してメンテナンスしないという意味ではなく、すでに結構成熟しているので触る必要性が小さい、という意味で。

しかし、バグや要望などありましたら、どしどしお寄せください*6。バグ対応はもちろんのこと、要望に関しても方針と矛盾せず、比較的対応が楽なものであれば早めの対応をしたいと思っています。

まぁ、githubなんで、適当にいじってプル・リクエストくれても良いですし。臨機応変に対応します。このエントリのコメントでもOK。ML等は盛り上がったら考えます。クローズなやりとりをお望みでしたら個人的にメールでも。

まとめ

Immutable重要。ということで、明日は最速Eclipseの魔術師、id:Yamashiro0217 です。

*1:正直、ヘビーウェイトな考え方だとは思う。

*2:訳本情報もあります。 http://twitter.com/#!/kohsei/status/17106115712000000

*3:たまにオリジナルの英語docが残ってるけど。

*4:moneyパッケージ内のクラス。

*5:万一会社がこのプロダクトを放り出しやがったら、Jiemamy Projectが受け皿になりますw

*6:基本方針もあるので、全てに応えられる訳ではないですが。