2012年04月18日

不変クラスについてあれこれ考える


"不変クラス"とはその名の通り、状態が生成時から決して変化しないクラスをいいます。不変クラスの代表格はStringですが、Stringクラスは内部の状態を変更できる術を外部に一切公開していません。

あるインスタンスが生成された時点の状態から絶対に変化しないという確証がプログラミングを簡略化することを想像するのは難しいことではありません。不変クラスは確実にスレッドセーフです。どのスレッドから共有されてもなんの問題も引き起こしません。また、オブジェクトの属性にちなんだ変数名をつけても問題なく、ソースが分かりやすくなります。しかし、オブジェクトを変化させないというルールをAPI使用者のコーディングや規約のみによって保とうとするのはまず無理でしょう。業務アプリケーションは大人数でコーディングを行いますので、完全に強制できる仕組みがない以上、規約はせいぜい気休め程度でしかありません。

今日は不変クラスについて勉強したいと思います。不変クラスはどのような条件を満たす必要があるのか、どう使用していけばいいのかを考えていきたいと思います。

不変クラスの条件

手元にある『Effective Java 第2版』によると不変クラスは以下の条件を満たす必要があるとされています。

@オブジェクトの状態を変更するためのいかなるメソッドも提供しない。
Aクラスが拡張できないことを保証する。
Bすべてのフィールドをfinalにする。
Cすべてのフィールドをprivateにする。
D可変コンポーネントに対する独占的アクセスを保証する。

@は単にsetterにあたる物や、メンバ変数の状態を変更できる公開メソッドを作成してはいけませんよ。ということです。Aはクラスがextendsによって拡張できないことを保証すればいいわけですから、クラスをfinalするのがもっとも簡単です。その他の方法としては、コンストラクタをprivateにしてstaticファクトリメソッドを提供するという方法があります。B〜Cはそのままの意味ですね。分かりにくいのがDですが、これはサンプルを見ながら確認していきます。まずは、@〜Cだけを考えて不変クラスを作成していきます。

不変クラスの悪例
以下に示すのは不完全な不変クラスです。悪い例なので注意。
/**
 * 不完全な不変クラス
 * @author ragtimer
 */
public class Person {
	/**
	 * 名前.
	 */
	private final String name;
	/**
	 * 体重.
	 */
	private final double weight;
	/**
	 * 身長.
	 */
	private final double height;
	/**
	 * 誕生日.
	 */
	private final Date birth;
	/**
	 * 友達.
	 */
	private final List<Person> friends;

	/**
	 * ビルダー
	 */
	public static class Builder{

		//必須パラメータ
		/**
		 * 名前.
		 */
		private final String name;
		/**
		 * 体重.
		 */
		private final double weight;
		/**
		 * 身長.
		 */
		private final double height;
		/**
		 * 誕生日.
		 */
		private final Date birth;

		//オプションパラメータ
		/**
		 * 友達.
		 */
		private List<Person> friends = null;

		/**
		 * 必須項目を作成するコンストラクタ.
		 */
		public Builder(String name,double weight,double height,Date birth){
			this.name = name;
			this.weight = weight;
			this.height = height;
			this.birth = birth;
		}
		/**
		 * オプション項目(友達)を設定する.
		 * @param friends
		 * @return
		 */
		public Builder friends(List<Person> friends){
			this.friends = friends;
			return this;
		}

		/**
		 * ビルドメソッド.
		 * @return
		 */
		public Person build(){
			return new Person(this);
		}
	}
	/**
	 * privateコンストラクタ
	 * @param builder
	 */
	private Person(Builder builder){
		this.name = builder.name;
		this.weight = builder.weight;
		this.height = builder.height;
		this.birth = builder.birth;
		this.friends = builder.friends;
	}
	/**
	 * 友達表示.
	 */
	public void printFriends(){
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy'年'MM'月'dd'日'");
		if(this.friends != null){
			for(Person friend : friends){
				System.out.println(friend.name + "!" + sdf.format(friend.birth).toString() + "生まれ!");
			}
			System.out.println(this.name + "!" + sdf.format(this.birth).toString() + "生まれ!");
			System.out.println("超平和バスターズはずっとなかよし!!");
			System.out.println();
		}
	}

	//以下アクセサー
	public String getName() {
		return name;
	}
	public double getWeight() {
		return weight;
	}
	public double getHeight() {
		return height;
	}
	public Date getBirth() {
		return birth;
	}
	public List<Person> getFriends() {
		return friends;
	}
}
ちょっと引数が多いのでBuilderパターンを使いました。最初に言っておくと、これは中途半端な不変クラスです。前述した不変クラスの条件のうち@〜Cは満たしていますが、Dの「可変コンポーネントに対する独占的アクセスを保証する」を満たしていません(このクラスはfinalではないですがAは満たしてます。継承先から呼び出し可能なコンストラクタがありませんので、子クラスを実装できないからです)。ではどうDを満たしていないのか。クライアントコードを書いて攻撃してみます。
public static void main(String[] args) throws ParseException {
	//超平和バスターズを作成
	Person jintan
		= new Person.Builder("じんたん", 55.0, 165.0, toDate("1995/9/18")).build();
	Person anaru
		= new Person.Builder("あなる", 49.0, 164.0, toDate("1995/11/18")).build();
	Person yukiatsu
		= new Person.Builder("ゆきあつ", 68.0, 181.0, toDate("1995/7/20")).build();
	Person tsuruko
		= new Person.Builder("つるこ", 47.0, 170.0, toDate("1995/5/10")).build();
	Person poppo
		= new Person.Builder("ぽっぽ", 89.0, 185.0, toDate("1995/1/16")).build();

	List<Person> chouHeiwaBusters = new ArrayList<Person>();
	Collections.addAll(chouHeiwaBusters, jintan,anaru,yukiatsu,tsuruko,poppo);

	//みんなをメンマの友達に
	Person menma
	= new Person.Builder("めんま", 36.0, 147.0, toDate("1995/1/16"))
											.friends(chouHeiwaBusters).build();

	//友達表示
	menma.printFriends();

	//----------------------//
	//悪意あるragtimerの攻撃//
	//----------------------//
	Person ragtimer
		= new Person.Builder("らぐたいまあ", 74.0, 181.0, toDate("1981/8/18")).build();
	//みんないなくなれ
	chouHeiwaBusters.clear();

	//超平和バスターズの一員に
	menma.getFriends().add(ragtimer);

	//めんまとふたりっきり・・・
	menma.printFriends();
}
private static Date toDate(String str) throws ParseException{
	Date date = DateFormat.getDateInstance().parse(str);
	return date;
}
実行結果はこちら。↓↓↓
じんたん!1995年09月18日生まれ!
あなる!1995年11月18日生まれ!
ゆきあつ!1995年07月20日生まれ!
つるこ!1995年05月10日生まれ!
ぽっぽ!1995年01月16日生まれ!
めんま!1995年01月16日生まれ!
超平和バスターズはずっとなかよし!!

らぐたいまあ!1981年08月18日生まれ!
めんま!1995年01月16日生まれ!
超平和バスターズはずっとなかよし!!

見事に中身を変えられてますね。要するに、不変クラス(にしたいクラス)が持っている可変コンポーネントの参照をクライアントが操作できることが問題なわけです。問題になるのはbirthメンバとfiriendsメンバ、つまりDateとListですね。この2つは可変クラスですから、内部メンバの参照をクライアントが操作できるのが好ましくないわけです。これがDのいう「可変コンポーネントに対する独占的アクセス」を崩されている状態ですね。

ソース上の問題は2箇所あります。ひとつめは可変コンポーネントを設定しているところ、つまりコンストラクタとBuilderの設定メソッドです。参照をそのまま自フィールドに入れています。これは、クライアントが後で渡したインスタンスに対して変更を加えると、作成したオブジェクトにまで影響が及ぶということを意味しています。上記のmain()の中で、chouHeiwaBusters変数をclear()している攻撃が有効になります。

もうひとつはアクセサーです。可変オブジェクトの参照をそのまま返していますので、同様の問題が発生します。menma.getFriends()でfriendsを呼び出し、それにadd()してるのがその問題点をついてます。ではこれらの問題点を解消していきます。

防御的にコピーする
この問題を解消するには「防御的コピー」という方法を使用します。それぞれ、以下のように修正します。
・値を設定している箇所(Date)
public Builder(String name,double weight,double height,Date birth){
	this.name = name;
	this.weight = weight;
	this.height = height;
	this.birth = new Date(birth.getTime());		//防御的にコピー
}
・値を設定している箇所(List)
public Builder friends(List<Person> friends){
	//防御的にコピー
	List<Person> copyList = new ArrayList<Person>();
	copyList.addAll(friends);
	this.friends = copyList;
	return this;
}
・アクセサー(Date)
public Date getBirth() {
	//防御的にコピー(clone()を使用してもOK)
	return new Date(birth.getTime());
}
・アクセサー(List)
public List<Person> getFriends() {
	//防御的にコピー(clone()を使用してもOK)
	List<Person> copyList = new ArrayList<Person>();
	copyList.addAll(friends);
	return copyList;
}
値を設定するときもアクセサーから参照を返す時も、毎回別のオブジェクトを作成しています。つまりクライアントはいくら設定した変数やアクセサから取得した変数に変更を加えても、このクラスの内部構造には何の影響もありません。これで完璧な不変クラスになりました。ただし、ひとつ注意があって、値を内部構造に設定する際はclone()を使用してはいけないということです。クライアントから引き渡されたクラスが悪意あるDate/Listのサブクラスであり、clone()が攻撃者によってオーバライドされていてもPersonクラスでは感知できないからです。設定する箇所さえキチンとコピーすれば、アクセサーはclone()でかまいません。なぜならそのクラスはjava.util.Date、あるいはjava.util.ArrayListに確定するからです。では実際にclone()を使用した場合の攻撃をみてみます。

内部構造設定時にclone()を使うと・・・
例えば先ほどのDateを誤ってこう修正したとします。
public Builder(String name,double weight,double height,Date birth){
	this.name = name;
	this.weight = weight;
	this.height = height;
	this.birth = (Date)birth.clone();		//誤ってclone()を使用
}
コンストラクタでclone()を使ってますね。では悪意を持ってクライアントコードを書きます。まずは邪悪なDateから
/**
 * 邪悪なDate
 * @author ragtimer
 *
 */
public class EvilDate extends Date{
	public static List<Date> EVIL_DATE_LIST = new ArrayList<Date>();
	/**
	 * 悪意あるcloneの実装
	 */
	@Override
	public Date clone(){
		Date date = (Date) super.clone();
		EVIL_DATE_LIST.add(date);
		return date;
	}
}
clone()を実装して、内部のstatic領域に作成した参照を隠し持っていますね。続いてはmainです。
	public static void main(String[] args) throws ParseException {
		//超平和バスターズを作成
		Person jintan
			= new Person.Builder("じんたん", 55.0, 165.0, toDate("1995/9/18")).build();
		Person anaru
			= new Person.Builder("あなる", 49.0, 164.0, toDate("1995/11/18")).build();
		Person yukiatsu
			= new Person.Builder("ゆきあつ", 68.0, 181.0, toDate("1995/7/20")).build();
		Person tsuruko
			= new Person.Builder("つるこ", 47.0, 170.0, toDate("1995/5/10")).build();
		Person poppo
			= new Person.Builder("ぽっぽ", 89.0, 185.0, toDate("1995/1/16")).build();

		List chouHeiwaBusters = new ArrayList();
		Collections.addAll(chouHeiwaBusters, jintan,anaru,yukiatsu,tsuruko,poppo);

		//みんなをメンマの友達に
		Person menma
		= new Person.Builder("めんま", 36.0, 147.0, toDate("1995/1/16"))
												.friends(chouHeiwaBusters).build();
		//友達表示
		menma.printFriends();
		
		//----------------------//
		//悪意あるragtimerの攻撃//
		//----------------------//
		for(Date date : EvilDate.EVIL_DATE_LIST){
			//全員を1920年生まれのジジババに!
			date.setYear(20);
		}

		//あの頃は楽しかったねえ・・フガフガ
		menma.printFriends();
	}
	private static Date toDate(String str) throws ParseException{
		Date date = DateFormat.getDateInstance().parse(str);
		EvilDate eDate = new EvilDate();
		eDate.setYear(date.getYear());
		eDate.setMonth(date.getMonth());
		eDate.setDate(date.getDate());

		return eDate;
	}
}
実行結果はこちら↓↓↓
じんたん!1995年09月18日生まれ!
あなる!1995年11月18日生まれ!
ゆきあつ!1995年07月20日生まれ!
つるこ!1995年05月10日生まれ!
ぽっぽ!1995年01月16日生まれ!
めんま!1995年01月16日生まれ!
超平和バスターズはずっとなかよし!!

じんたん!1920年09月18日生まれ!
あなる!1920年11月18日生まれ!
ゆきあつ!1920年07月20日生まれ!
つるこ!1920年05月10日生まれ!
ぽっぽ!1920年01月16日生まれ!
めんま!1920年01月16日生まれ!
超平和バスターズはずっとなかよし!!
みんな爺さん婆さんになってしまいました。これが内部構造を設定する際にclone()を使用する危険性です。

不変クラス/防御的コピーの使いどころ
不変クラスはどういった時に使えばいいでしょうか?『Effective Java 第2版』では「正当な理由がない限り、クラスは全て不変にすべきである」と書いてあります。確かに、でき得る限り不変にすればバグの確率は下がりますね。ただ、今私は悪意をもってクラスの不変性を崩そうと外部から攻撃していましたが、ほとんどの場合実装者に悪意などありません。ありませんが、意味がわからずAPIを修正されて不変クラスが崩される可能性もありますし、先ほどのアクセサーを通して内部構造が取得できていると信じ込み、それらに対して変更を加えることで仕様を満たしたと満足される可能性もあります。従って、Javadocや然るべきドキュメントに不変であることを明記しておいたほうがいいかもしれません。今回は便宜上DateやListを使用していますが、日付の情報を持つならばDate.getTime()で取得されるlongを持ったほうが健全ですし、「友達」といった明らかに可変な概念を不変クラスの内部構造にすべきなのかという問題はもちろんあります。これはあくまでサンプルです。

防御的コピーに関しては、アクセサーに要求されるたびに毎回オブジェクトが生成されるという性能的リスクがあるのは確かです。しかし、不変クラスは状態が固定されているため、オブジェクトの再利用がされやすいという性能的メリットもあります。どちらを優先するかは状況しだいでしょうが、防御的コピーそのものは不変クラスに限らず重要です。例えば、このPersonクラスに友達を追加するadd(Person)メソッドを追加したとします。その瞬間、このクラスは不変ではありませんが、それを理由に防御的コピーをやめるべきではないでしょう。なぜなら、「内部構造が変更できるのはadd(Person)からのみである」という保証は品質やデバッグの観点から重要であることには変わりないからです。

今日はこのへんで。なおこの記事は『Effective Java 第2版』を大いに参考にしています。凄くためになる本なのでよかったらどうぞ。あとメンマ大好き。ペロペロしたい(^ω^)
posted by sandman at 21:57| Comment(1) | Java | このブログの読者になる | 更新情報をチェックする

2012年04月15日

継承についてあれこれ考える


Java言語に限らず、オブジェクト指向を学ぶ上で最初の関門となるであろう『継承』。Javaを書いている人であれば、知ってて当たり前の文法ですが、その詳しい意味をきちんと説明できるか?と問われると答えに詰まる人も多いのではないでしょうか?よく新人研修などでは犬クラスやら猫クラスやら、現実にあるものでクラスや継承の説明をすることが多いですが、それだけで継承が正しく使いこなせるかというと甚だ疑問です。

そこで、今日はこの継承、とりわけJava言語の継承についてよく考えてみることにします。まずは基本的な文法からおさらいしていきましょう。

@継承はクラス宣言に「extends 親クラス名」と表記することで実現する。
A子クラスは親クラスのpublic、protectedなフィールド、およびメソッドを継承し、使用することができる。
B子クラスは親クラスのメソッドを再定義(override)することができる。但しフィールドは再定義できない。
Cコンストラクタはpublicであっても継承されない。
D子クラスは親クラスの「型」も継承している。つまり、親クラスの変数に代入可能である。

これらはJava言語における継承の文法的な規則になります。しかし、よほどセンスのある方でなければこれらの文法的な知識だけで継承を思いのまま操るというのは難しいのではないかと思います。実際、私もこれらの事実は新人のときから知識としてはなんとなく知っていましたが、とてもじゃないですが「継承がわかっている」という感覚はありませんでした。

では今はどうなのかというと、「ある程度は分かっている」と言ってもいいかなと思っています。そう思えるようになったのは、上記に列挙した継承の文法を知っているというだけでなく、継承をもう少し概念的に捉えることができるようになったからです。私がここに記しておきたいのも文法的知識の確認ではなく、この概念的なお話です。

継承は階層関係を構築するためのもの

では「継承の目的とはなんぞや」という問いを考えます。ここでは文法的な定義を答えるのではなく、もう少し概念的に考えてみましょう。ひとつの答えとして、文法的な定義はともかく、「継承とは要するにクラス同士の階層関係を構築するためのものである」と言えると思います。当たり前だろ、という感じもしますね。しかし重要な点はここからもうひとつ踏み込んで、この階層関係は2つの側面を持っているということを理解するところにあります。その2つの側面とは、ひとつが「機能の拡張」、もうひとつが「機能の実装」です。もっともこれだけではなんのことやら分からないので、順を追って説明していきます。

機能の拡張

前述した通り、あるクラスを継承した場合、子にあたるクラスは親クラスの機能を継承することができますが、それだけでなく、さらに親クラスの機能を拡張することもできます。以下の単純な例を見てみます。以下は初期化時に引数で渡された文字を標準出力する「Print」クラスと、それを拡張した「MultiPrint」クラスです。
/**
 * 親にあたるクラス。
 * @author ragtimer
 *
 */
public class Print {
	/**
	 * 出力文字列。
	 */
	private final String output;

	/**
	 * コンストラクタ。
	 * @param output
	 */
	public Print(String output) {
		this.output = output;
	}

	/**
	 * 与えられた引数を標準出力
	 * @param hoge
	 */
	public void print(){
		System.out.println(output);
	}
}
/**
 * Printの拡張クラス。
 * @author ragtimer
 */
public class MultiPrint extends Print {
	/**
	 * コンストラクタ。
	 * @param output
	 */
	public MultiPrint(String output) {
		super(output);
	}

	/**
	 * 引数で渡された回数分Print.print()をコール
	 * @param num
	 */
	public void multiPrint(int num){
		for(int i = 0; i < num; i++){
			print();
		}
	}
}

見ての通り、Printというクラスを継承し、multiPrint()というメソッドの追加を行っています。これが前述した継承の二つの目的の一つ目「機能の拡張」です。この場合親クラスであるPrintの機能を引継ぎ、さらに新しい機能を追加するという目的で継承を使用しているわけです。

機能の実装

続いてのサンプルは、ヘッダ、本文、フッタを出力するプログラムです。抽象クラスを用いています。まずは親に当たるクラスを見てみましょう。

・親にあたるクラス
/**
 * 「ヘッダー、コンテンツ、フッターの順に何かを出力する」抽象クラス
 * @author ragtimer
 *
 */
public abstract class Print {

	/**
	 * コンストラクタ.
	 * @param contents
	 */
	public Print(String contents){
		this.contents = contents;
	}

	/**
	 * 出力する本文のコンテンツ
	 */
	private final String contents;

	/**
	 * ヘッダーの出力
	 */
	protected abstract void printHeader();

	/**
	 * フッターの出力
	 */
	protected abstract void printFooter();

	/**
	 * コンテンツの出力
	 */
	protected abstract void printContents();

	/**
	 * 出力メソッド
	 */
	public void print(){
		//ヘッダの出力
		printHeader();
		//本文の出力
		printContents();
		//フッターの出力
		printFooter();
	}

	/**
	 * ゲッター
	 * @return
	 */
	public String getContents() {
		return contents;
	}
}

抽象メソッドとして定義されたprintHeader()、printFooter()、printContents()を実装しているメソッドprint()内で使用しています。当然、3つの抽象メソッドの実装が確定していないのでこのままではこのクラスを実際に使用することはできません。では、この抽象クラスを実装していきます。

・実装クラス
/**
 * Printの実装。JOJO。
 * @author ragtimer
 */
public class JojoPrint extends Print {

	private final String PRE = "オラオラオラ!!";

	/**
	 * コンストラクタ
	 * @param contents
	 */
	public JojoPrint(String contents) {
		super(contents);
	}

	@Override
	protected void printHeader() {
		for(int i = 0; i < getContents().length() + PRE.length(); i++){
			System.out.print("▽");
		}
		System.out.println();
	}

	@Override
	protected void printFooter() {
		for(int i = 0; i < getContents().length() + PRE.length(); i++){
			System.out.print("△");
		}
		System.out.println();
	}

	@Override
	protected void printContents() {
		System.out.println(PRE + getContents());
	}
}
/**
 * Printの実装。Dio。
 * @author ragtimer
 */
public class DioPrint extends Print {

	private final String PRE = "無駄無駄無駄!!";

	public DioPrint(String contents) {
		super(contents);
	}

	@Override
	protected void printHeader() {
		for(int i = 0; i < getContents().length() + PRE.length(); i++){
			System.out.print("▼");
		}
		System.out.println();
	}

	@Override
	protected void printFooter() {
		for(int i = 0; i < getContents().length() + PRE.length(); i++){
			System.out.print("▲");
		}
		System.out.println();
	}

	@Override
	protected void printContents() {
		System.out.println(PRE + getContents());
	}
}

特に意味もないプログラムですが、これでPrintがめでたく実装できました。実装したのはJojoのセリフを出力するJojoPrintとDioのセリフを出力するDioPrintです。ではこの連中を実際に使ってみます。

・Mainプログラム
public static void main(String[] args) {

	//JOJO
	Print print1 = new JojoPrint("スタープラチナ!!");
	print1.print();

	//Dio
	Print print2 = new DioPrint("ザ・ワールド!!");
	print2.print();
}

・実行結果
▽▽▽▽▽▽▽▽▽▽▽▽▽▽▽▽▽
オラオラオラ!!スタープラチナ!!
△△△△△△△△△△△△△△△△△
▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼
無駄無駄無駄!!ザ・ワールド!!
▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲

JojoPrintもDioPrintも同じPrint型変数で受け取れることに注意してください。つまりこれが前述した文法のD「子クラスは親クラスの"型"も継承している。つまり、親クラスの変数に代入可能である。」の意味であり、「多態性(ポリモーフィズム)」の正体です。いま見てきたソースはデザインパターンで言うところの「template methodパターン」になります。具体的な実装は子クラスに任せるという手法です。これが継承の二つ目の目的である「機能の実装」です。

クラス階層の混在

これで継承が構築するクラス階層の2つの目的「機能の拡張」と「機能の実装」の説明が完了しました。今まで説明したことを聞いて理解した上で、「確かにその通りだけどだからなんなの」という感想を抱く人もいるかもしれません。そもそもなぜこのようなことを意識しなくてはならいのでしょうか?

その答えは「この2つのクラス階層が混在している場合に問題が発生するから」です。ひとつ前のtemplate methodパターンを以下のように改造することを考えて見てください。
Printクラスを拡張して、任意の回数Print.print()をコールするmultiPrintというメソッドを具備したMultiPrintクラスを作成しなさい。

これは一番最初に見た「機能の拡張」とまったく同一の改造です。ただし、全く同じ方法で解決しようとすると問題が発生します。当然、Printクラスを拡張するのですから、Printクラスを継承したMultiPrintクラスを作ることは間違いありません。そしてmultiPrint()メソッドを追加し拡張することも変わりません。しかし、そのようにして作成したMultiPrintクラスはそのまま使用できるでしょうか?答えは否です。なぜなら、これだけでは抽象化された3つのメソッドを実装できていないからです。「じゃあ実装すればいいじゃん」といって実装するとなんだか気持ちの悪いことになります。つまりこういうクラス構成になるのです。

・Print
・JojoPrint
・DioPrint
・MultiJojoPrint
・MultiDioPrint

見ての通り、「機能の実装」が冗長になっています。例えばJojoの実装は、JojoPrintにもMultiJojoPrintにもまったく同一のコードが現れることになります。いくらなんでもこれは気持ち悪いですね。つまりこれが2つのクラス階層が混在している場合に発生する問題なわけです。次はこれをどう解決するかです。

コンポジションによる処理の委譲

継承によるクラス階層が混在している際に発生した問題は確認しました。次はこれをどう解決するかです。今問題なのは、Print←JojoPrint、およびPrint←DioPrintが表す「機能の実装」を意味するクラス階層が独立していないことです。これが独立して存在していれば、機能の拡張と機能の実装のパターンを自由に組み合わせることができるはずです。これを実現するには「コンポジションによる処理の委譲」を使用します。コンポジションとは既存のクラスをあるクラスの構成要素とすることです。実際に見てきます。まずはコンポジションを使用して改造したPrintクラスです。
/**
 * ヘッダ、本文、フッタを出力するクラス。
 * コンポジションを利用して実装を独立させている。
 * @author ragtimer
 *
 */
public class Print {

	/**
	 * 実装を委譲する抽象クラス
	 */
	private final PrintImpl impl;

	/**
	 * コンストラクタ
	 * @param impl
	 */
	public Print(PrintImpl impl) {
		this.impl = impl;
	}

	/**
	 * ヘッダの出力。処理は委譲。
	 */
	private void printHeader(){
		impl.printHeader();
	}

	/**
	 * フッタの出力。処理は委譲
	 */
	private void printFooter(){
		impl.printFooter();
	}

	/**
	 * 本文の出力。処理は委譲
	 */
	private void printContents(){
		impl.printContents();
	}

	/**
	 * 出力
	 */
	public void print(){
		//ヘッダの出力
		printHeader();
		//本文の出力
		printContents();
		//フッターの出力
		printFooter();
	}
}
フィールドにPrintImplという変数が追加されています。さらにtemplate methodパターンで記述した際に抽象化されている3つメソッドが実装されていますが、中身はimplという変数の同一名のメソッドをただ呼び出しているだけです。これが「委譲」です。あとは一緒ですね。ではこのPrintImplとは何者でしょうか。
/**
 * 抽象化された「実装」
 * @author USER
 */
abstract public class PrintImpl {

	/**
	 * ヘッダーの出力
	 */
	protected abstract void printHeader();

	/**
	 * フッターの出力
	 */
	protected abstract void printFooter();

	/**
	 * コンテンツの出力
	 */
	protected abstract void printContents();

}
このPrintImplは「実装」を意味するクラス階層のみを独立させるために設計された抽象クラスです。メンバには抽象メソッドしか存在しません。では具体的な実装を見てみます。
/**
 * 機能の実装クラス。Jojo。
 * @author ragtimer
 *
 */
public class JojoPrintImpl extends PrintImpl {

	private final String PRE = "オラオラオラ!!";

	private final String contents;

	public JojoPrintImpl(String contents) {
		this.contents = contents;
	}

	@Override
	protected void printHeader() {
		for(int i = 0; i < contents.length() + PRE.length(); i++){
			System.out.print("▽");
		}
		System.out.println();
	}

	@Override
	protected void printFooter() {
		for(int i = 0; i < contents.length() + PRE.length(); i++){
			System.out.print("△");
		}
		System.out.println();
	}

	@Override
	protected void printContents() {
		System.out.println(PRE + contents);
	}

}
/**
 * 機能の実装クラス。Dio。
 * @author ragtimer
 *
 */
public class DioPrintImpl extends PrintImpl {

	private final String PRE = "無駄無駄無駄!!";

	private final String contents;

	public DioPrintImpl(String contents) {
		this.contents = contents;
	}

	@Override
	protected void printHeader() {
		for(int i = 0; i < contents.length() + PRE.length(); i++){
			System.out.print("▼");
		}
		System.out.println();
	}

	@Override
	protected void printFooter() {
		for(int i = 0; i < contents.length() + PRE.length(); i++){
			System.out.print("▲");
		}
		System.out.println();
	}

	@Override
	protected void printContents() {
		System.out.println(PRE + contents);
	}
}
内容そのものは先ほどと同じですね。このように、コンポジションと委譲を用いるの機能の実装を綺麗に独立させることができるわけです。この手法はデザインパターンで言うところの「Bridgeパターン」です。Printクラスがフィールドで保持しているPrintImplフィールドが2つのクラス階層をBridge(橋)で繋いでいるのが分かると思います。では今度は機能の拡張です。
/**
 * 拡張クラス。
 * @author ragtimer
 *
 */
public class MultiPrint extends Print {

	public MultiPrint(PrintImpl impl) {
		super(impl);
	}

	/**
	 * 指定された回数printを呼び出す
	 * @param num
	 */
	public void multiPrint(int num){
		for(int i = 0; i < num; i++){
			print();
		}
	}

}
ただ、multiPrint()という機能の追加するだけですね。実装は独立していますからこれだけで問題ありません。では最期にこれらを使用するクライアントを見てみます。
/**
 * @param args
 */
public static void main(String[] args) {

	Print print = new Print(new JojoPrintImpl("スタープラチナ"));
	print.print();

	MultiPrint mPrint = new MultiPrint(new DioPrintImpl("ザ・ワールド"));
	mPrint.multiPrint(3);
}
・実行結果
▽▽▽▽▽▽▽▽▽▽▽▽▽▽▽
オラオラオラ!!スタープラチナ
△△△△△△△△△△△△△△△
▼▼▼▼▼▼▼▼▼▼▼▼▼▼
無駄無駄無駄!!ザ・ワールド
▲▲▲▲▲▲▲▲▲▲▲▲▲▲
▼▼▼▼▼▼▼▼▼▼▼▼▼▼
無駄無駄無駄!!ザ・ワールド
▲▲▲▲▲▲▲▲▲▲▲▲▲▲
▼▼▼▼▼▼▼▼▼▼▼▼▼▼
無駄無駄無駄!!ザ・ワールド
▲▲▲▲▲▲▲▲▲▲▲▲▲▲

実装(Jojo/Dio)と機能(Print/MultiPrint)が自由に組み合わせ可能であることに注目してください。これで先ほどの冗長性は完全に取り除かれました。

以上、継承を概念的に捉えてみる試みでした。継承の理解の一助となれば幸いです。なお、この記事は結城浩さんの「デザインパターン入門」を大いに参考にしています。
posted by sandman at 00:37| Comment(1) | Java | このブログの読者になる | 更新情報をチェックする

2012年04月11日

java.util.concurrent 〜Executor その3〜


さて、今回は複数の非同期処理を行う場合です。RunnableとThreadを用いて3本以上のスレッドの待ち合わせをするのは割と面倒だったりするのですが、ExecutorServiceを使用すると簡単です。また、ExecutorServiceはスレッドプールの機能も持ち合わせているので好きなようにリソースを最適化することができます。

では例によってサンプルソースを見ます。今回は非同期処理として複数の子スレッドでHTTP送信を行い、メインスレッドでは非同期処理が全て終わり次第結果を出力するようにします。HTTP送信など、コストが高い処理を順不同で大量に行う必要があり、その結果をマージしたい場合を想定します。

メインメソッド
public static void main(String[] args) {

	ExecutorService executor = null;
	try{
		//固定(3本)のスレッドプールを有するExecutorServiceを生成する
		executor = Executors.newFixedThreadPool(3);

		//タスクの生成しリストに詰める
		HttpGetTask task1 = new HttpGetTask("http://localhost:8080/DummyServer/test1.html");
		HttpGetTask task2 = new HttpGetTask("http://localhost:8080/DummyServer/test2.html");
		HttpGetTask task3 = new HttpGetTask("http://localhost:8080/DummyServer/test3.html");
		List<Callable<String>> tasks = new ArrayList<Callable<String>>();
		Collections.addAll(tasks, task1,task2,task3);

		List<Future<String>> resultList = null;
		try {
			//全てのタスクを実行
			resultList = executor.invokeAll(tasks);
		} catch (InterruptedException e) {
			e.printStackTrace();
			return;
		}

		//結果を表示
		for(Future<String> result : resultList){
			try {
				System.out.println(result.get());
			} catch (InterruptedException e) {
				e.printStackTrace();
			} catch (ExecutionException e) {
				e.printStackTrace();
			}
		}

	}finally{
		//使い終わったらshutdownしておく
		if(executor != null){
			executor.shutdown();
		}
	}
}

非同期タスククラス
/**
 * HTTP GETを行う非同期処理。
 * @author ragtimer
 *
 */
public class HttpGetTask implements Callable<String> {

	/**
	 * 接続先URL
	 */
	private String url;

	/**
	 * コンストラクタ
	 */
	public HttpGetTask(String url){
		this.url = url;
	}


	@Override
	public String call() throws Exception {
		System.out.println("HttpGetTask start...");

		//url宛てにHTTPGETを送るだけ
		HttpClient httpclient = new DefaultHttpClient();
		HttpGet httpGet = null;
		String result = null;
		try {
			httpGet = new HttpGet(url);
			HttpResponse response = httpclient.execute(httpGet);
			HttpEntity httpEntity = response.getEntity();
			result = EntityUtils.toString(httpEntity);
		} finally {
            if (httpGet != null) {
                httpGet.abort();
            }
        }
		System.out.println("HttpGetTask end...");

		return result;
	}

}


ではソースを詳しく見ていきます。

・ExecutorServiceを作成
executor = Executors.newFixedThreadPool(3);
Executors.newFixedThreadPool(int)を使用します。引数はプールするスレッド数です。これでこのExecutorServiceでスレッドプールが勝手に適用されます。仮にスレッドが枯渇した場合はスレッドが空くまで待機してくれます。

・非同期処理を複数作成
HttpGetTask task1 = new HttpGetTask("http://localhost:8080/DummyServer/test1.html");
HttpGetTask task2 = new HttpGetTask("http://localhost:8080/DummyServer/test2.html");
HttpGetTask task3 = new HttpGetTask("http://localhost:8080/DummyServer/test3.html");
List<Callable<String>> tasks = new ArrayList<Callable<String>>();
Collections.addAll(tasks, task1,task2,task3);
Callableを実装したHttpGetTaskを複数生成し、Listに詰めています。HttpGetTaskは生成時に渡されたurlにHTTP GETを送り、ボディを返却するだけの処理です。なお、HTTP送信にはapacheのHTTPClientを使用してます。対向のサーバローカルに適当に立てたTomcatです。

・非同期処理の実行
resultList = executor.invokeAll(tasks);
非同期処理を詰めたリストを引数にExecutorService.invokeAll(Collection>)を呼び出します。引数はFutureオブジェクトのリストで返却されます。

・結果の表示
for(Future<String> result : resultList){
	try {
		System.out.println(result.get());
	} catch (InterruptedException e) {
		e.printStackTrace();
	} catch (ExecutionException e) {
		e.printStackTrace();
	}
}
Futureをループで回しながら結果を取得していきます。たったこれだけで待ち合わせができるのは素晴らしいですね。

次回はスケジュール処理をやります。
posted by sandman at 23:20| Comment(0) | Java | このブログの読者になる | 更新情報をチェックする

広告


この広告は60日以上更新がないブログに表示がされております。

以下のいずれかの方法で非表示にすることが可能です。

・記事の投稿、編集をおこなう
・マイブログの【設定】 > 【広告設定】 より、「60日間更新が無い場合」 の 「広告を表示しない」にチェックを入れて保存する。


×

この広告は1年以上新しい記事の投稿がないブログに表示されております。