ある書籍に載っていたサンプルコード
はじめに、下記の書籍に載っている例題を紹介しよう。
入門ソフトウェアシリーズ Java言語
河西朝雄著
ナツメ社
この書籍の例題16(123〜124頁)では、継承の例として、Java言語の日付を表すGregorianCalendarクラスから、独自のDayクラスを作る例が紹介されている。
GregorianCalendarクラスには、日付どうしを比較するequalsメソッドが用意されている。だが、これは時分秒まで比較してしまうため、誕生日の判定には向いていない。そこで、GregorianCalendarクラスを拡張して、新たに月と日だけを比較するcompareメソッドを作る、というのが、この例題の主旨である。
さて、この例題のクラスは、はたして、誕生日を表すデータ型として使うのに適したものであろうか。
import java.util.*; public class Rei16 { public static void main(String[] args) { int month=3,day=13; // 誕生日3月13日 Day t1=new Day(); Day t2=new Day(t1.get(Calendar.YEAR),month-1,day); switch (t1.compare(t2)) { case 0: System.out.println("誕生日おめでとう"); break; case -1:System.out.println("誕生日はまだです"); break; case 1: System.out.println("誕生日は過ぎました"); break; } } } class Day extends GregorianCalendar { public Day(){ super(); } public Day(int y,int m,int d) { super(y,m,d); } public int compare(Day t2) { if (get(Calendar.MONTH)==t2.get(Calendar.MONTH) && get(Calendar.DATE)==t2.get(Calendar.DATE)) return 0; else if (before(t2)) return -1; else return 1; } }
サンプルコードの問題点
前述のサンプルコードには、いくつかの問題点がある。
最大の問題点は、Dayクラスの「年」の扱い方が不明瞭な点だ。その結果、閏年の2月29日の扱いや、前後関係の判定が曖昧になっている。
このサンプルコードのmainメソッドを見ると、誕生日のデータでも、年だけは今日の日付と同じにしている。だが、このやり方では、誕生日が2月29日の人は、平年にこのプログラムを動かすと、誕生日が3月1日に変化してしまう。
しかし、逆に生まれた年をそのまま指定すると、今度は、誕生日が2月29日の人は、閏年にしか一致しなくなる。毎日パソコンを使う人が、このサンプルコードのようなソフトウェアを作り、誕生日には「誕生日おめでとう」と表示するようにしても、4年に一度しか表示されなくなってしまう。
また、このサンプルコードでは、生まれた年をそのまま指定すると、「誕生日は過ぎました」と表示されることはあり得ない。ということは、Dayクラスは、実は必ず、年だけは今日の日付と同じにしなくてはならない、という、暗黙の了解があることになる。
だが、Dayクラスのコンストラクタを見ると、年を指定するようになっているし、compareメソッドを見ると、年が違っていても月と日が同じなら「一致する」と判定されるようになっている。つまり、必ずしも年を一致させなくても良いようにも見える。
これらの問題点を見ると、このDayクラスは、誕生日を表すデータ型としては適切ではないように思える。
このDayクラスは、そもそも、誕生日を表すデータとして作られた訳ではない。月と日だけが同じなら一致する、と判定する処理を追加しただけで、事実上は、GregorianCalendarクラスと同じ、日付を表すクラスである。実際、mainメソッドを見ると、今日の日付を表すのにも、Dayクラスを使っている。独自のDayクラスを作ったのは、単に、Java言語の標準クラスであるGregorianCalendarクラスにはメソッドを追加できないので、やむを得ず継承した、というだけだ。
だが、日付を表す独自のクラスとしては、compareメソッドの意図が不明瞭だ。日付を比べるに際して、時分秒まで比較したくないのであれば、年月日だけを比較するメソッドを追加するのが筋である。だが、このサンプルコードでは、年を無視して、月と日だけを比較している。これは即ち、Dayクラスは日付ではなく、誕生日を表すデータ、と位置付けていることになる。とはいえ、それならば、月と日が一致しない場合の前後関係の判定でも、年を無視するべきだ。
結局、Dayクラスについても、compareメソッドについても、設計者の意図が不明確である。これでは、使う方としても、どのように使えば良いのか困ってしまう。
このようなクラスを使っても、動くプログラムは作ることができるだろう。実際、この例題のサンプルコードを実行してみれば、正しい答えが出る。しかし、これはあくまで、動くだけのプログラムでしかない。
オブジェクト指向的にうまく設計するのであれば、この場合は、明確に「誕生日」を表すクラスを作り、「誕生日」が一致するかどうかを判定するメソッドを作るべきである。
誕生日が一致するとは?
ところで、一口に「誕生日が一致する」と言っても、その定義は一通りではない。一般的には、下記の3つの定義が考えられるだろう。
- 「年」「月」「日」のすべてが一致する。
- 「月」「日」が一致する。
- 「月」「日」が一致する。但し、平年であれば、2月29日生まれの人は、3月1日と一致する。
どの定義が正しいかは、アプリケーションの仕様によって決まる。
誕生日というデータは、いろいろなアプリケーションで使われる、基本的なデータである。基本的なデータを表すクラスは、特定のアプリケーションの仕様に限定されるべきではない。つまり、誕生日を表すクラスは、上記のいずれの定義にも対応できるように設計するべきである。
サンプルコードを直してみよう
前述のDayクラスを、誕生日を表すものとして、これらの3つの定義に対応するように直してみると、次のようになる。なお、メソッドの中身の実装は省略する。
public class Day extends GregorianCalendar { // 「年」「月」「日」のすべてが一致する。 public int compare1 ( Day t2 ); // 「月」「日」が一致する。 public int compare2 ( Day t2 ); // 「月」「日」が一致する。但し、平年であれば、 // 2月29日生まれの人は、3月1日と一致する。 public int compare3 ( Day t2 ); }
しかし、このDayクラスは、1つのクラスに比較メソッドが3つもあり、オブジェクト指向的にうまく設計されたものとは言えない。
下記の図は、比較メソッドの役割を、クラス間の関係としてクラス図に示したものだ。上記の3つの比較メソッドは、いずれも、Dayクラスどうしの関係を示しており、この図では区別できない。このことからも、うまく設計できていないことが分かる。
もっとうまく表現したクラスを作る
クラスやメソッドの役割を明確にするには、生年月日(date of birth)と、誕生日(birthday)を、きちんと分けて考える必要がある。「年」「月」「日」の3つのデータを扱う場合は「生年月日」、「月」「日」だけを扱う場合は「誕生日」であり、この2つは異なるクラスとして表現する。
誕生日が一致するかどうかの定義として、3つの判定方法があった。生年月日と誕生日を分けて考えると、この3つの定義は、次のように言い換えられる。
- 「年」「月」「日」のすべてが一致する。
- 生年月日どうし、または、生年月日と日付の比較。
- 「月」「日」が一致する。
- 誕生日どうしの比較。
- 「月」「日」が一致する。但し、平年であれば、2月29日生まれの人は、3月1日と一致する。
- 誕生日と、日付の比較。
前述のサンプルコードの場合は、3つの定義はすべて、クラス間の関係という意味では、互いに区別できなかった。単に、処理方法が違うというだけであった。
生年月日と誕生日を分けて考えると、3つの定義はそれぞれ、異なるクラス間の関係を表す。つまり、3通りの比較方法の区別を、クラス図という概念的なモデルのレベルで、明確に示すことができる。
生年月日と誕生日を分けて、オブジェクト指向的にうまく表現したクラスは、次のようなものとなる。なお、メソッドの中身の実装は省略する。
public class DateOfBirth extends GregorianCalendar { public Birthday getBirthday ( ); // 「年」「月」「日」のすべてが一致する。 public boolean equals ( GregorianCalendar date ); } public class Birthday { private int month; private int day; public Birthday ( int month, int day ); // 「月」「日」が一致する。 boolean equals ( Birthday birthday ); // 「月」「日」が一致する。但し、平年であれば、 // 2月29日生まれの人は、3月1日と一致する。 public boolean accepted ( GregorianCalendar date ); }