誕生日に学ぶオブジェクト指向

作成:2006年6月6日

吉田誠一のホームページ   >   ソフトウェア工学   >   技術コラム   >   オブジェクト指向

ネットには、今日が誕生日の有名人を教えてくれるサイトがある。こういうサービスは、誕生日のデータをたくさん持っていて、今日の日付と比べて、一致すれば表示するようになっている。

誕生日が一致する人を探すプログラムを作るのは、そんなに難しくはない。誕生日を表すデータとして、「年」「月」「日」の3つの整数を持つ構造体かクラスを作って、あとは月と月、日と日をif文で比べるだけだ。

だが、単に動くだけのプログラムを作るのではなく、オブジェクト指向的にうまく設計しようとすれば、話は別だ。たとえ、誕生日という単純で身近なデータでも、オブジェクト指向的な考えやモデルをうまくクラスとして表現するのは、意外と難しい。

ここでは、誕生日をオブジェクト指向的に『うまく』表現したクラスを作ってみたい。

目次

  1. ある書籍に載っていたサンプルコード
  2. サンプルコードの問題点
  3. 誕生日が一致するとは?
  4. サンプルコードを直してみよう
  5. もっとうまく表現したクラスを作る

ある書籍に載っていたサンプルコード

はじめに、下記の書籍に載っている例題を紹介しよう。

入門ソフトウェアシリーズ 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クラスどうしの関係を示しており、この図では区別できない。このことからも、うまく設計できていないことが分かる。

Dayクラスを直した場合の、3つの比較メソッドとクラスの関係

もっとうまく表現したクラスを作る

クラスやメソッドの役割を明確にするには、生年月日(date of birth)と、誕生日(birthday)を、きちんと分けて考える必要がある。「年」「月」「日」の3つのデータを扱う場合は「生年月日」、「月」「日」だけを扱う場合は「誕生日」であり、この2つは異なるクラスとして表現する。

誕生日が一致するかどうかの定義として、3つの判定方法があった。生年月日と誕生日を分けて考えると、この3つの定義は、次のように言い換えられる。

  • 「年」「月」「日」のすべてが一致する。
    • 生年月日どうし、または、生年月日と日付の比較。
  • 「月」「日」が一致する。
    • 誕生日どうしの比較。
  • 「月」「日」が一致する。但し、平年であれば、2月29日生まれの人は、3月1日と一致する。
    • 誕生日と、日付の比較。

前述のサンプルコードの場合は、3つの定義はすべて、クラス間の関係という意味では、互いに区別できなかった。単に、処理方法が違うというだけであった。

生年月日と誕生日を分けて考えると、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 );
}
        

生年月日と誕生日を分けたクラス図

Copyright(C) Seiichi Yoshida ( comet@aerith.net ). All rights reserved.