Processingで学ぶ 実践的プログラミング専門課程

第17回継承と委譲

導入

オブジェクト指向のプログラミング言語の機能の中でも目玉と呼べるものが「継承」です。しかしながら継承によって発生する可能性があるデメリットが大きいため、Java言語やProcessingでは使用方法に制限が加えられています。前回は、継承で行うことができる機能を部分的に実現する方法であるインターフェイスを学びました。今回は、継承を用いずに継承で行うことができる機能を実現するもう1つの工夫、委譲(いじょう)を紹介します。

展開

必要な機能を持っているクラスを利用する仕組みが継承と委譲

継承や委譲を使う基本的なストーリーは次の2通りが考えられます。

  • あるクラスAを作成した後に、そのクラスAの機能と同じ機能を持ち、さらに別の機能を持つクラスBを作成したくなったとします。このような場合に継承や委譲を用います。

  • あるいは、あるクラスAとBが同じ機能を持つ良く似たコードを持っているとします。この良く似た部分のコードを切り出してクラスCにまとめるリファクタリングを行います。そしてクラスCを継承、あるいは委譲するクラスA'とB'を作成します。

どちらも複数のクラスの間に、包含関係や依存関係を上手に設定することを目的としています。上手に設定することでコードの見通しが良くなり、その結果つくりやすく、メンテナンスのしやすいコードになるのです。

単純に継承したクラスと、著作者名情報を取得するメソッドを追加したクラスをつくる

これから紹介するサンプルコードは、先ほど紹介したストーリーが両方適用されたものです。 連載第16回ではインターフェイスを活用して設定ファイルの情報を読み込むクラスをつくりました。 連載第16回演習2の解答例で掲載したコードを、インターフェイスを使わず継承を利用して再作成してみます。解答例のクラスから設定情報を読み込むコードをクラスに切り出しConfigInfoクラスとします。そしてこれを継承したAppInfoLoader8クラスを作成します。さらに新しいクラスAppInfoLoader9には著作者情報を持たせ、それを取得するメソッドを付加します。これらをテストするスケッチをTestAppInfoLoader6.pdeとします。

以下のファイルはTestAppInfoLoader6フォルダに納めてください。CONFIG.TXTSETTING.TXTはさらにその中のdataフォルダに納めてください。

「インターフェイスの実装」「クラスの継承」の最も大きな違いでありメリットは、クラスの継承の場合はスーパークラス側で実装済みのメソッドを利用できることです。AppInfoLoader8クラスはコンストラクタとgetClassNameメソッドしか持ちません。それにもかかわらず、スーパークラスであるConfigInfoクラスのメソッドを呼び出して使うことができます。目的の仕事をするメソッドをスーパークラスが持っていれば、サブクラスは特にコードを追加すること無く利用できます。これは便利な仕組みです。

さらに追加の機能が欲しくなった場合、新しいメソッドをサブクラスに書き込みます。クラスAppInfoLoader9では、クラスConfigInfoを継承した後に著作者名を取得するための新しいメソッドgetAuthorNameを追加しています。

プロトタイピング中など、コードが発展途上にある場合や、そもそもどんなことができるか不明瞭な場合は、このように継承したクラスを発展させていき、やがて重要なメソッドが明らかになればスーパークラスのConfigInfoクラスへ吸収し、ConfigInfoクラスを継承するクラスを再構成するのです。

緻密な設計を必要とするクリティカルなシステムのコードではこんな行き当たりばったりな方法論は通用しませんが、私たちが今利用している言語はProcessingです。アートやプロトタイピング、そして本来は教育に適した言語です。思う存分行き当たりばったりにコーディングしましょう。やがて勘がつかめれば、洗練された設計を行うスキルを効率良く身につけられるはずです。今、Processingを使う私たちは、それとは別の「作品作り」に注力しましょう。

継承した後、今あるメソッドをオーバーライドする

クラスConfigInfoとクラスAppInfoLoader8AppInfoLoader9を比較すると、同じ名前のメソッドgetClassNameが定義されています。

クラスConfigInfogetClassNameメソッド
  private   final String CLASS_NAME      = "ConfigInfo";
  
  public String getClassName(){
    return CLASS_NAME;  
  }
クラスAppInfoLoader8getClassNameメソッド
  public static final String CLASS_NAME      = "AppInfoLoader8";

// 中略

  public String getClassName(){
    return CLASS_NAME;
  }

このように、サブクラスでスーパークラスと同じメソッドを再定義することをオーバーライドと呼びます。 クラスConfigInfoを単体で使用する際には、getClassNameメソッドでクラス名を取得すると、クラスConfigInfoで定義されている定数CLASS_NAMEの値ConfigInfoが取得されます。

クラスAppInfoLoader8を使用する際には、オーバーライドしたgetClassNameメソッドでクラス名を取得すると、クラスAppInfoLoader8で定義されている定数CLASS_NAMEの値AppInfoLoader8が取得されます。 2つのクラスを使うコードの側から見ると、至って自然な挙動です。メソッドを「上書き」したわけですから、サブクラスのインスタンスを利用しているのに、スーパークラス側のメソッドが優先されては困ります。クラスAppInfoLoader8のインスタンスのスーパークラスのクラス名が必要ならば、クラスAppInfoLoader8内でsuper.getClassName()と呼べば、スーパークラスのクラス名が得られます。

継承した後、今あるメソッドをオーバーロードする。すなわち引数が異なるメソッドを作成する

クラスConfigInfoにはloadConfigInfo()メソッドがあります。これは設定ファイル名を引数として受け取るように宣言されていますから、設定ファイル名を指定しなければ使うことができません。しかし、場合によってはファイル名を指定しないで呼び出し、その際にデフォルトの設定ファイルを読み込むということも可能にしたいかもしれません。その際には、引数を持たないメソッドを定義できると便利です。そこで、サブクラス側で引数を持たないloadConfigInfoメソッドを定義します。次のsketch TestAppInfoLoader7を見てください。

以下のファイルはTestAppInfoLoader7フォルダに納めてください。CONFIG.TXTはさらにその中のdataフォルダに納めてください。

次のコードのように、スーパークラスにあるメソッドと同じメソッド名でありながら、引数が異なるメソッドを定義することをオーバーロードと呼びます。

クラスAppInfoLoader10でオーバーロードしたloadConfigInfoメソッド。
  public boolean loadConfigInfo(){
    return super.loadConfigInfo(CONFIG_FILENAME);
  }

そして、このオーバーロードによって、呼び出し側では引数を持たないメソッドとして呼び出しが可能になりました。

TestAppInfoLoader7.pde 設定ファイル名を指定せずに設定情報を読み込むように使ったloadConfigInfoメソッド。
  a.loadConfigInfo();

こうなると、スーパークラスに定義されたメソッドと引数において一致しないメソッドを呼ぶわけですから、クラスAppInfoLoader10への参照をクラスConfigInfoに代入して、AppInfoLoadr10でオーバーロードしたメソッドを呼んでも使用できません。このためクラスTestAppInfoLoader内のtestAppInfoLoaderメソッドの引数を変更しなければなりませんでした。継承を利用していて、メソッドの追加を行った場合の弊害です。この弊害を防ぐためには、メソッドの追加が必要ないように緻密な設計を行っておくか、そもそも継承を利用しないでおくかとなります。

継承を使わず委譲で実現する

これまでで継承と、それに付随するオーバーライドやオーバーロードを学習しました。同様な仕事をもっと泥臭く実現する方法があります。次のコードを読んでください。AppInfoLoader11は継承を使わずにこれまでと同等の機能を持たせています。

以下のファイルはTestAppInfoLoader8フォルダに納めてください。CONFIG.TXTSETTING.TXTはさらにその中のdataフォルダに納めてください。

クラスAppInfoLoader11の全体。
public class AppInfoLoader11{
  public static final String CLASS_NAME = "AppInfoLoader11";
  public static final String AUTHOR     = "Atsushi Hirata";
  
  private static final String CONFIG_FILENAME = "CONFIG.TXT";

  private ConfigInfo c = new ConfigInfo();
  private String version = "0.0";
  
  public AppInfoLoader11(){
    c.loadConfigInfo(CONFIG_FILENAME);
    version = c.getVersion();
  }
  
  public String getClassName(){
    return CLASS_NAME;
  }
  
  public String getAuthorName(){
    return AUTHOR;
  }
  
  public String getVersion(){
    return version;
  }
  
  public boolean loadConfigInfo(){
    boolean result = c.loadConfigInfo(CONFIG_FILENAME);
    version = c.getVersion();
    return result;
  }
  
  public boolean loadConfigInfo(String filename){
    boolean result = c.loadConfigInfo(filename);
    version = c.getVersion();
    return result;
  }
}

クラスConfigInfoのコードはそのままです。ただし、クラスAppInfoLoader11はそれを継承していません。クラス定義の先頭が

public class AppInfoLoader11 extends ConfigInfo

ではなく、単に

public class AppInfoLoader11

となっています。せっかくある継承の仕組みですが、それを使わない選択肢もあるということです。では、どのようにして同じような機能を実装しているかと見ると、クラスAppInfoLoader11の内部にクラスConfigInfoのインスタンスを持たせています。必要に応じてAppInfoLoader11ConfigInfoに仕事をたらい回しし、結果をさも自分が行った仕事のように処理します。 このように、自分ではない別のクラスのインスタンスに仕事を「譲って」⁠委せる」ことを委譲と呼びます。継承をサポートしないプログラミング言語で使われていた方法に付けた名前で、泥臭いのですがその分自由が利きます。継承を使わなくても、多重継承できなくても、必要なクラスのインスタンスを必要な数だけ持てば良いのです。オーバーロードもオーバーライドもプログラマが自分で手配して組み上げるのです。先祖帰りのようで抵抗があるでしょうか。しかし無理な継承関係を作り込むぐらいならば余程分かりやすいコードが書けます。特にProcessingのように小規模なソフトウエア向きの言語では、継承関係を検討しながら緻密に組み上げるよりは適宜委譲しながら「とりあえず動く」コードを割り切って手早く組み上げるほうが良いでしょう。

欠点と言えば、継承を用いる場合に比較して、委譲するためのコードをタイプする量が増えること。このため委譲の使用に関して意見の分かれるところではあるのですが、大変有効な工夫です。

Java言語やProcessingではインターフェイスの仕組みがあり、インターフェイスは1つのクラスで複数実装できますから、統一した実装を強制する手段としてインターフェイスを用い、中身では適切なメソッドを持つクラスに委譲する方法が用いられます。

現実的な採用順

プロトタイピングに始まって完成に至るまでのプログラミングの過程において、当初から緻密な設計が可能であれば別ですが、そもそも何をどうつくって良いか分からないような場合が多いものです。

そのような場合には、最初からクラス構成をきっちり設計してからコーディングに入ることができません。まずは「こんな感じかな」というクラスを書き、コードが大きくなってきたら機能や領域ごとのクラスに切り分けていくことになるでしょう。そこでは委譲が用いられます。やがてさらに対象が明らかになるにしたがって、必要なインターフェイスが見いだされるでしょう。いよいよ本格的に問題が理解され、組むべきコードの構成が分かったら、必要に応じてスーパークラス、サブクラスと言う継承関係を設計し、そのようなコードに組み替えて行く。このような採用順が現実的な小規模プログラミングの過程です。

演習

演習1(難易度:easy)

高校生の成績を保持するクラスを作成しましょう。氏名、性別、国語、数学、英語のtテストの点を保持し、取得するメソッド、平均点を取得するメソッドを備えるクラスBaseをまず作成しましょう。その後に、理系を選択した生徒のためにBaseを継承したクラスScience文系を選択した生徒のためにクラスHumanitiesを作成してください。Scienceは物理、化学のテストの点を保持、取得するメソッドを持ち、Humanitiesは古文、社会のテストの点を保持、取得するメソッドを持たせてください。

演習2(難易度:easy)

演習1の問題において、クラスHumanitiesは継承を用いず委譲を用いて書き直しましょう。

まとめ

  • 継承、オーバーライド、オーバーロードという仕組みや、委譲という工夫を学びました。

学習の確認

それぞれの項目で、Aを選択できなければ、本文や演習にもう一度取り組みましょう。

  1. 継承、オーバーライド、オーバーロードが理解できましたか?

    1. 理解できた。気持ちよく納得した。
    2. 理解できた。しかし、今ひとつスッキリしない。
    3. 理解できない。

  2. 委譲の意味と役割が理解できましたか?
    1. 理解できた。気持ちよく納得した。
    2. 理解できた。しかし、今ひとつスッキリしない。
    3. 理解できない。

参考文献

  • 『Java言語プログラミングレッスン(下⁠⁠結城浩 著、ソフトバンクくりエィティブ株式会社
    • Java言語のオブジェクト指向的特徴を大変分かりやすく解説した入門書中の名著。上下巻ともにJava言語入門者は必携。

演習解答

  1. 以下のファイルをsketchフォルダTestResultsに納めます。
  2. 以下のファイルをsketchフォルダTestResults2に納めます。

おすすめ記事

記事・ニュース一覧