第 7 回 ポリモーフィズムとデザインパターン

本日の内容


このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。

7-1. ポリモーフィズム

親クラスのメソッドは子クラスでオーバライドできます。 そして、子クラスのインスタンスを親クラスの変数で参照していても、メソッ ドを呼び出すと子クラスでオーバライドしたメソッドが呼び出されます。 これをポリモーフィズムと言います。 既に、 toString メソッドなどで多用してきたテクニックです。

例7-1


class A {
    protected String value;
    public A(String value){
	this.value = value;
    }
    public String getValue(){
	return "A has "+value+".";
    }
    @Override public String toString(){
	return "I am A.";
    }
}
class B extends A{
    public B(String value){
	super(value);
    }
    @Override public String getValue(){
	return "B has "+value+".";
    }
    @Override public String toString(){
	return "I am B.";
    }
}
class C extends A{
    public C(String value){
	super(value);
    }
    @Override public String getValue(){
	return "C has "+value+".";
    }
    @Override public String toString(){
	return "I am C.";
    }
}
class Rei {
    private static void show(A[] array){
	for(A a : array){
	    System.out.println(a);
	}
    }
    public static void main(String[] arg){
	A[] array = { new A("an apple"), new B("an orage"), new C("a peach")};
	show(array);
	for(A a : array){
	    System.out.println(a.getValue());
	}
    }
}

これをポリモーフィズムを使わずに、記述すると非常に面倒で見づらいプログ ラムになります。 上記の例で、全て A 型のオブジェクトとし、表示を A, B, C と切り替えるた めに、列挙型とし、 switch 文で分岐するように書くことができます。


enum Type { typeA, typeB, typeC }
class A {
    private Type type;
    private String value;
    public A(Type t, String value){
	type = t;
	this.value = value;
    }
    public String getValue(){
	String ret="";
	switch(type){
	case typeA:
	    ret = "A has "+value+".";
	    break;
	case typeB:
	    ret = "B has "+value+".";
	    break;
	case typeC:
	    ret = "C has "+value+".";
	    break;
	}
	return ret;
    }
    @Override public String toString(){
	String ret="";
	switch(type){
	case typeA:
	    ret = "I am A.";
	    break;
	case typeB:
	    ret = "I am B.";
	    break;
	case typeC:
	    ret = "I am C.";
	    break;
	}
	return ret;
    }
}
class Rei {
    private static void show(A[] array){
	for(A a : array){
	    System.out.println(a);
	}
    }
    public static void main(String[] arg){
	A[] array = { new A(Type.typeA, "an apple"),
		      new A(Type.typeB, "an orage"),
		      new A(Type.typeC, "a peach")};
	show(array);
	for(A a : array){
	    System.out.println(a.getValue());
	}
    }
}

このようにデータ型のすべてのメソッドの中に switch, case 文を書いて処理 を分岐する必要があります。 このようなプログラミングはオブジェクト指向言語においては誤りとも言える ものです。 つまり、データ型に対して switch-case や if 文で区分して処理を行うよう なメソッドは全てポリモーフィズムで記述すべきということです。

7-2. テンプレートメソッドデザインパターン

例7-1 において、出力する文字列はほとんど同じです。 そして、各オブジェクトにおいて異なるのはオブジェクトクラスにおける名前 のみです。 「オブジェクトごとに名前を切り替える」というのは前節で説明した通りポリ モーフィズムで片付けることになります。 つまり、各オブジェクトで、 getName なるメソッドを持てば、その getName メソッドを使用して出力文字列を作成することができます。 この場合、各クラスで共通の文字列を作成するので、親クラスで文字列を作成 します。 なお、各クラスで文字列を作成するためだけに使用するメソッドは protected 修飾をし、外部からの使用は禁止します。


class A {
    protected String getName(){
	return "A";
    }
    protected String value;
    public A(String value){
	this.value = value;
    }
    public String getValue(){
	return getName()+" has "+value+".";
    }
    @Override public String toString(){
	return "I am "+getName()+".";
    }
}
class B extends A{
    public B(String value){
	super(value);
    }
    @Override protected String getName(){
	return "B";
    }
}
class C extends A{
    public C(String value){
	super(value);
    }
    @Override protected String getName(){
	return "C";
    }
}
class Rei {
    private static void show(A[] array){
	for(A a : array){
	    System.out.println(a);
	}
    }
    public static void main(String[] arg){
	A[] array = { new A("an apple"), new B("an orage"), new C("a peach")};
	show(array);
	for(A a : array){
	    System.out.println(a.getValue());
	}
    }
}

この、「サブクラスで表示文字列を返すメソッドをオーバライドし、親クラスで、 そのメソッドを使用した文字列を返す」手法をテンプレートメソッド デザインパターンと言います。 なお、本来のテンプレートメソッドでは親クラスはインスタンス化しない抽象 クラスで構成します。

例7-2

お金を扱うクラスとして日本円を扱う Yen とアメリカドルを扱う Dollar を 考えます。 これらはどちらもお金ですから、お金のクラス Money を作ると is-a 関係に なります。 したがって、それぞれの共通の処理は Money クラスで定義し、Yen 、 Dollar それぞれのクラスが継承するようにします。 なお、Money 自体はインスタンス化する必要がありませんので、抽象クラスと して定義します。 その上で値を保持し、 toString メソッドで金額を表示するようにします。

さて、金額の表示ですが、 Yen であれば「100円」、 Dollar であれば「$50」 のように通貨記号の前置、後置の区別があります。 このため、 Money クラスの値 value に対して、通貨記号を付けたものを toString メソッドで返すにはどうすれば良いでしょうか?

ここでテンプレートメソッドを使います。 前置する記号と後置する記号を Money クラスではそれぞれ abstract メソッ ドとして宣言します。 そして、 Yen と Dollar クラスでそれぞれ実際にオーバライドして定義しま す。 このようにして作成したのが下記のプログラムです。


abstract class Money {
    protected double value;
    protected Money(double value){
	this.value = value;
    }
    abstract protected String getPrefix();
    abstract protected String getPostfix();
    @Override public String toString(){
	return getPrefix()+String.valueOf(value)+getPostfix();
    }
}
class Dollar extends Money{
    public Dollar(double value){
	super(value);
    }
    @Override protected String getPrefix(){
	return "$";
    }
    @Override protected String getPostfix(){
	return "";
    }
}
class Yen extends Money{
    public Yen(double value){
	super(value);
    }
    @Override protected String getPrefix(){
	return "";
    }
    @Override protected String getPostfix(){
	return "円";
    }
}
class Rei {
    public static void main(String[] arg){
	Yen y = new Yen(100);
	Dollar d = new Dollar(50);
	System.out.println(y);
	System.out.println(d);
	Money[] array = {y,d};
	for(Money m : array){
	    System.out.println(m);
	}
    }
}

7-3. ストラテジデザインパターン、関数オブジェクト

前回、比較を行うのに、比較子というオブジェクトを考えました。 これは、実際に並べ変えを行う java.util.Arrays.sort や java.util.Collections.sort などの処理において重要な「比較」という操作 を抽象化し、後でユーザが自由に与えるようにしたものです。 このように、特定の処理において、オブジェクトで機能を指定するような方法 をストラテジデザインパターンと言います。 Java の場合次のようなプログラムを組みます。

  1. compare などひとつ共通のメソッド名を決めておきます。
  2. そのメソッド名のみを持つ抽象クラスを定義します。
  3. 抽象クラスのオブジェクトを受け取り、そのメソッドを使用する処理を 記述します。
  4. 処理を利用する場合、抽象クラスを派生して、実際にメソッドを実装した クラスを作ります。
  5. メソッドを実装したクラスのオブジェクトを、その処理に渡して実行させ ます。

なお、抽象クラスは実装をしないひとつのメソッド名のみを持ちますので、 Java の場合、 interface で定義します。

例7-3

以前演習で行った、複数の combination に対するテストをこのストラテジ デザインパターンで行うことを考えます。 メソッド名はそのまま combination とし、抽象クラス名(interface) は Combi とします(同じような名前にしたのには特に意味はありません)。


interface Combi {
    int combination(int n, int m);
}
class Rei {
    private static void test(Combi c){
	final int[][] indata = { { 0,0}, {2,1}, {10,3} };
	final int[] outdata = { 1, 2, 120};
	for(int i = 0; i < indata.length; i++){
	    if(c.combination(indata[i][0],indata[i][1])==outdata[i]){
		System.out.println("Ok");
	    }else{
		System.out.println("NG");
	    }
	}
    }
}

このようにすると、 Combi を implement して、 combination を実装したク ラスのオブジェクトは、この test メソッドに与えることができます。

7-4. 事例研究 HTTP サーバ

さて、ここで少し実用的なアプリケーションに関して考えましょう。

Web サーバの一部の機能を実現することにします。 ここでは厳密な話をするわけではないので、もっとも単純な HTTP/1.0 につい て説明します。 なお、HTTP/1.1 の仕様にあるように、実際のアプリケーションを作るには HTTP/1.1 に準拠する必要があります (HTTP/1.0 は廃止になってませんが、新規のアプリケーションを作ることは推 奨されていないということです)。

Web サーバは次のような働きをします。

  1. クライアントから TCP の 80 番に接続を受ける
  2. 「GET ファイルパス HTTP/1.0(改行)(改行)」というメッセージを受け取 る
  3. 与えられたファイルパスに応じて返答メッセージを作成する
  4. 返答メッセージをクライアントに返して接続を切る

このうち、今回はファイルパスを与えられて、返答メッセージを作成するとい う作業をプログラムにしようと思います。

メッセージの仕様

返答メッセージは三つの部分に別れます。

ステータス行
ヘッダ部
...
(空行)
ボディ部

ステータス行

ステータス行は次のようになっています。

HTTP/1.0 ステータスコード ステータスを表す言葉

例えば、ファイルが存在して正常にファイルを送るときは次のようになってい ます。

HTTP/1.0 200 Ok

一方、ファイルが存在しなくてエラーメッセージが送られるときは次のように なっています。

HTTP/1.0 404 Not Found

ここでは単純のためにこの二つの状態のみを扱うことにします。 つまり、ファイルが存在すれば 200 でファイルを返すメッセージを作る。 一方、ファイルが無ければ 404 でファイルがないというエラーメッセージを 作ることにします。

ヘッダ部

ヘッダとはボディに含まれるコンテンツのメタデータが書かれます。 ヘッダは各行がフィールドと呼ばれ、何行にも渡って記述されます。 そして空行で終了し、ボディ部が始まります。 フィールドは「フィールド名: フィールド値」という書式になっています。 最低限必須なヘッダフィールドは以下の通りです。

content-type の処理はここでは本質ではないので 「text/plain; charset=shift_jis」に決め打ちすることにします。 さて、残りのヘッダに関してはファイルを送る時とデータを送る時で扱いかた が違います。

ファイル送信時

ファイルを送る際は次のような意味になります。

Date
ファイル作成日時
Content-Length
ファイルの内容のバイト数
エラーメッセージ送信時

一方、エラーメッセージを送る際は次のような意味になります。

Date
エラーが発生した日時(現在時刻)
Content-Length
ボディ部に書くエラーメッセージのバイト数

ボディ部

ボディ部はファイルが存在する場合はファイルの内容そのものになります。 一方、エラーメッセージに関してはユーザが読んで理解できるような(自然言 語の)メッセージを送ります。

オブジェクト分析

ファイルパスからメッセージを作成するプログラムを作ります。 具体的にはメッセージのオブジェクトを作成し、 toString メソッドで全メッ セージが得られるようにします。

さて、メッセージにはファイルの内容とエラーメッセージの二つがあります。 つまり、「ファイルの内容」と「エラーメッセージ」は共に「メッセージ」に 対して is-a 関係にあることになります。 そこで、 Message クラスとそのサブクラスである FileConent クラスと ErrorMessage クラスを作ります。 ここで、Message クラスの toString メソッドはこの二つのサブクラスに応じ て出力を変化させるので、テンプレートメソッドを使います。 とりあえず、 Message クラスでの toString メソッドは次のようになります。


abstract class Message {
  @Override public String toString(){
    return getStatusLine()+"\n"+getHeader()+"\n"+getBody();
  }
  abstract protected String getStatusLine();
  abstract protected String getHeader();
  abstract protected String getBody();
}

なお、ステータス行は一行のため文字列の最後に改行を付けません。 また、ヘッダとボディの間には空行を入れますので、上記のように二箇所に改 行が入ります。

さて、ファイルパスからメッセージのオブジェクトを作るわけですが、作られるオブ ジェクトは FileContent のインスタンスか、または ErrorMessage のインスタン スのいずれかになります。 つまり、指定したクラスのインスタンスを生成するコンストラクタは使えませ ん。 そこで、 Message クラスにファクトリ(インスタンス生成の static メソッド) を用意します。 そして、 FileContent と ErrorMessage のコンストラクタは protected にな ります。

また、ErrorMessage クラスは、今回は一種類のエラーメッセージしか作りま せんが、HTTP/1.0 のステータスコードにおいてエラーはいくつもあります。 そこで、404 メッセージ決め打ちではなく、拡張性を考えて、ステータスコー ドをインスタンスに与えると、それに応じたエラーメッセージを持つインスタ ンスを生成することにします。

一方で、FileContent には File オブジェクトをコンストラクタに与えて生成 します。 FileContent のコンストラクタの仕事は、ファイルの内容を読み出して、ファ イルの更新日付を取得することです。 ファイルの取得に失敗する可能性がありますので、コンストラクタは FileNotFoundException と IOException を投げることになります。

ステータスはエラーになってもならなくても使います。 また、 HTTP/1.0 という単純なプロトコルでもさまざまなステータスがあり、 今回の Ok と NotFound 以外もあります。 上記の Web サーバの仕事の分析でもステータス自体が名詞として登場しまし たが、ステータス自体をオブジェクトとします。 ステータスクラスではステータスコードで「ステータスを示す言葉」も「ユー ザへのエラーメッセージ」も管理するとします。 但し、ステータスコードでデータを管理するには HashMap が便利ですが、こ れは前処理でデータを保存しておく必要があります。 そのため、インスタンスを生成しインスタンスへのメソッドで各データにアク セスすることにします。 インスタンスは一個でいいので、シングルトンデザインパターンを使います。

Status クラス


class Status {
    private static Status instance;
    private HashMap<Integer,String> word;
    private HashMap<Integer,String> message;
    public static Status getInstance(){ // シングルトン
	if(instance == null){
	    instance = new Status();
	}
	return instance;
    }
    private Status(){
	word = new HashMap<Integer,String>();
	message = new HashMap<Integer,String>();
	word.put(200,"Ok");
	word.put(404,"Not Found");
	message.put(404,"Not Found");
    }
    public String getWord(int statusCode){
	return word.get(statusCode);
    }
    public String getMessage(int statusCode){
	return message.get(statusCode);
    }
}

このようにすると、各メッセージから getStatusCode メソッドで得られたス テータスコードによりステータス行を作ることができます。 これもテンプレートメソッド的な表現になります。


class Message {
  abstract protected int getStatusCode();
  private String getStatusLine(){
    return "HTTP/1.0 "+getStatusCode()+" "
      +Status.getInstance().getWord(getStatusCode());
  }
}

getHeader も同様にテンプレートメソッドで作ります。 ヘッダの文字列を作るのに println が使えると便利なので、 StringWriter と PrintWriter を使用します。


    private String getHeader(){
	StringWriter sw = new StringWriter();
	PrintWriter pw = new PrintWriter(sw);
	pw.println("Content-Type: text/plain; charset=shift_jis");
	pw.println("Date: "+getDate());
	pw.println("Content-Length: "+getBody().length());
	pw.close();
	return sw.toString();
    }
    abstract protected String getDate();

FileContent クラスは File オブジェクトを引数に取るコンストラクタと、 getStatusCode, getDate, getBody を持ちます。 コンストラクタは FileNotFoundException と IOException を投げます。

FileContent

FileContent では与えられた File オブジェクトより FileInputStream オブ ジェクトを作り、ファイル全体を読みます。 但し、失敗したときに発生した Exception は処理せず、呼出側にそのまま伝 えます。 ファイルを全て読んだら body, date, errorCode を所定の値に設定します。


class FileContent extends Message {
    private String body;
    private Date date;
    private int errorCode;
    protected FileContent(File f) 
	throws FileNotFoundException, IOException {
	super();
	FileInputStream fis = new FileInputStream(f);
	CharArrayWriter caw = new CharArrayWriter();
	int data;
	while((data = fis.read())!=-1){
	    caw.write(data);
	}
	body = caw.toString();
	date = new Date(f.lastModified());
	errorCode = 200;
    }
    protected String getBody(){ return body; }
    protected String getDate(){ return date.toString(); }
    protected String getStatusCode(){ return errorCode; }
}

ErrorMessage

一方、 ErrorMessage は与えられたエラーコードを元に、 Status クラスから 情報を検索してメッセージを作ります。


class ErrorMessage extends Message {
    private String body;
    private Date date;
    private int errorCode;
    protected ErrorMessage (int errorCode) {
	super();
	this.errorCode = errorCode;
	body = Status.getInstance().getMessage(errorCode);
	date = new Date();
    }
    protected String getBody(){ return body; }
    protected String getDate(){ return date.toString(); }
    protected String getStatusCode(){ return errorCode; }
}

ここで、二つのクラスにおいて、 body, date, errorCode, getBody(), getDate(), getStatusCode() が共通になっています。 そこで、テンプレートメソッドの形が変則的になりますが、 変数を protected にして、 Message クラスに移動し、またメソッド もそのまま Message クラスに移動します。 但し、将来、追加したサブクラスで get... をオーバライドする可能性があり ますので、テンプレートメソッドを壊して変数を生で使うことは避けます。

最後に Message クラスでインスタンスを生成する static メソッド getInstance を作成します。 これは文字列を与えられたら、 FileContent オブジェクトを作成しますが、 その際に FileNotFoundException が出たら ErrorMessage オブジェクトに差 し替えるものです。 なお、 IOException も本来は処理しなければなりませんが、ここでは放置し ます。


class Message {
    public static Message getInstance(String filePath)
    throws IOException {
	File f = new File(filePath);
	Message message;
	try {
	    message = new FileContent(f);
	}catch(FileNotFoundException e) {
	    message = new ErrorMessage(404);
	}
	return message;
    }
}

完成したプログラムと、テスト用のプログラムは 別ページ に示します。

7-5. 演習問題

演習7-1

HTTP のメッセージを作るプログラムにおいて、 IOException を検知したら 「500 Internal Error」というエラーメッセージが出るようにプログラムを改 造しなさい。


坂本直志 <sakamoto@c.dendai.ac.jp>
東京電機大学工学部情報通信工学科