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 | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
そか、れかひょんもいたのか 角川文庫版の米倉斉加年の絵が表紙のやつね
Posted by 大島優子のオっパ at 2013年07月07日 16:28
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。
×

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