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

第16回インターフェイスの使い

導入

同じ機能を持つクラスで、中身が異なるクラスを作る必要がある場合があります。その例が前回の内容でした。単純に複数の独立したクラスを作る方法もありますが、そのコードを継続的にメンテナンスしていく必要がある場合や、もっと大きなソフトウェアを作る場合には、それら複数のクラスが間違いなく同じように取り扱えるよう管理する手間が生まれます。仕様書をきちんと作成し、プログラマがその仕様書を守っていれば良いのですが、人間それほど「几帳面」な生き物ではありません。そこで考えられたのが今回学習するインターフェイスという仕組みです。インターフェイスにはさらに便利な使い方がありますが、今回は最も基本的な部分に絞って学習します。

展開

工夫の必要が生まれる

前回の学習の流れを次に示します。

  1. 設定情報が記載されたテキストファイルを読み込むクラスを作ろう。
  2. クラス1ができた。
  3. 別の方法でテキストファイルを読み込みたい。では別のクラスを作ろう。
  4. クラス2ができた。

クラス1とクラス2は同じ結果を得られるのですが、中身は別の仕組みを持っています。これら2つのクラスを使うクラス3からは、どちらのクラスも同じように使えるように作りました。

小さなコードでしたので困りませんでしたが、もっと大きなコードを書くときには、矛盾無くクラスを書くことに努力が必要になります。そこで、矛盾無くクラスを書くための工夫をする必要が生まれました。

インターフェイスを利用する

前回は「設定情報が記載されたテキストファイルを読み込む」という目的のもと、実装の異なる2つのクラスを作りました。今後も実装方法を変更する必要が生じる見立てがあるとか、異なる記載方法の設定ファイルを読み込む必要があるなど、実際のソフトウェア作成においては、プログラマは柔軟な対応を必要とします。

今回の場合はメソッドを呼ぶ方法と得られる情報が同じでしたから、設定情報を読み込む働きをする複数のクラスの「窓口」は共通です。この共通した窓口部分を切り出した機能を持つコードをインターフェイスと呼びます。インターフェイスは窓口となるメソッドと、定数の宣言部分のみを持ち、実装コードを持ちません。インターフェイスに記述されたメソッドを、プログラマが作成するクラスに組み込みコードを記述することをインターフェイスを実装すると呼びます。

似たような働きをするプログラミング言語の機能に抽象クラスがあります。抽象クラスはインターフェイスと同じ機能に加えて、必要ならば実装のあるメソッドを持てます。出生順からすると抽象クラスがお兄さんなので、学習の手順としては抽象クラスを先に学ぶべきなのかもしれません。しかしながら、この連載は「実践的」であることを旨としていますので飛ばしてしまいます。インターフェイスの機能は、次の理由から実用的なのです。

  • 継承はトラブルメーカーなので、あまり使いたくない。
  • 抽象クラスは複数継承できない。インターフェイスなら複数実装できる。
  • 窓口を共通にする仕事のためにはインターフェイスで十分。

「複数継承」とは、正式な用語では多重継承と呼びます。多重継承とは複数のクラスを一度に継承することで、便利なのですが問題を生じやすい技術です。Java言語ではその問題を回避するために、あえて多重継承を許可していません。

先々も便利に使えるインターフェイスの最も基本的な使い方、窓口を共通にする機能をこれから使ってみましょう。

共通部分をインターフェイスに切り出す

詳しいインターフェイスの記述方法は参考文献等を読んでもらうとして、ここでは実際に切り出して作成したインターフェイスのコードを掲載します。

interface ConfigInfo {
  // フィールド
  String CONFIG_FILENAME = "CONFIG.TXT";
  // 抽象メソッド
  String getVersion();
}

現実的・実用的にはフィールドCONFIG_FILENAMEはインターフェイスに記載すべきではないと思いますが、記述例として使用します。そしてメソッドはメソッド名のみを記載します。これによってバージョン番号の文字列を取得するためのメソッド名はgetVersionであるとはっきりしました。このメソッドは必ず実装しないとコンパイルできませんから、プログラマにこのインタフェイスによる規約を守らせることができます。ConfigInfoインターフェイスを実装するクラスにloadVersionStringというメソッドを別に作ることはできますが、getVersionメソッドは必ず実装しなければなりません。共通の情報を取得するためのメソッド名がクラスによって異なるのは、それらのクラスを使うプログラマにとって悪夢でしかありません。インターフェイスに宣言されたメソッドを使いましょう。

インターフェイスConfigInfoを実装したクラスのsketch AppInfoLoader3.pdeと、テストコードを次に示します。テスト用のコードがこれまでのものと全く同じであることを読み取ってください。

なお、テストコードに使用されるAppInfoLoader.pdeAppInfoLoader2.pdeは前回第15回で作成したファイルです。同じスケッチフォルダにコピーを置いてください。

AppInfoLoader3.pde。インターフェイスConfigInfoを実装したクラス
public class AppInfoLoader3 implements ConfigInfo{
  public static final String CLASS_NAME = "AppInfoLoader3";
  
  private String version;
  
  public AppInfoLoader3(){
    String lines[] = loadStrings(CONFIG_FILENAME);
    if (0 < lines.length){
      version = "No Version Info(Config file contains " + lines.length + " lines but...)";
      for(String v : lines){
        String[] tokens = split(v, ",");
        if(tokens[0].equals("VERSION")){
          version = tokens[1];
        }
      }
    } else {
      version = "No Info";
    }
  }
  
  public String getVersion(){
    return version;
  }
}
TestAppInfoLoader.pde
//Test AppInfoLoader class
private final String TARGET_CLASS_NAME  = "AppInfoLoader";
private final String TARGET_CLASS_NAME2 = "AppInfoLoader2";
private final String TARGET_CLASS_NAME3 = "AppInfoLoader3";
private final String SOME_TROUBLE       = "Some trouble happend.";
private final String TARGET_CLASS_VERSION = "1.0";

void setup(){
  test();
}

void test(){
  noLoop();
  println("Test Start.");
  println("...AppInfoLoader begins.");
  assert AppInfoLoader.CLASS_NAME.equals(TARGET_CLASS_NAME)
         : TARGET_CLASS_NAME + " : " + SOME_TROUBLE;  
  AppInfoLoader ail = new AppInfoLoader();
  assert ail.CLASS_NAME.equals(TARGET_CLASS_NAME)
         : TARGET_CLASS_NAME + " : " + SOME_TROUBLE;
  assert ail.getVersion().equals(TARGET_CLASS_VERSION)
         : TARGET_CLASS_NAME + " : " + SOME_TROUBLE
            + "(" + ail.getVersion() + ")";
  println("...AppInfoLoader finished.");

  println("...AppInfoLoader2 begins");
  assert AppInfoLoader2.CLASS_NAME.equals(TARGET_CLASS_NAME2)
         : TARGET_CLASS_NAME2 + " : " + SOME_TROUBLE;  
  AppInfoLoader2 ail2 = new AppInfoLoader2();
  assert ail2.CLASS_NAME.equals(TARGET_CLASS_NAME2)
         : TARGET_CLASS_NAME2 + " : " + SOME_TROUBLE;
  
  assert ail2.getVersion().equals(TARGET_CLASS_VERSION)
         : TARGET_CLASS_NAME2 + " : " +   SOME_TROUBLE
           + "(" + ail2.getVersion() + ")";
  println("...AppInfoLoader2 finished.");
  
  println("...AppInfoLoader3 begins");
  assert AppInfoLoader3.CLASS_NAME.equals(TARGET_CLASS_NAME3)
         : TARGET_CLASS_NAME3 + " : " + SOME_TROUBLE;  
  AppInfoLoader3 ail3 = new AppInfoLoader3();
  assert ail3.CLASS_NAME.equals(TARGET_CLASS_NAME3)
         : TARGET_CLASS_NAME3 + " : " + SOME_TROUBLE;
  
  assert ail3.getVersion().equals(TARGET_CLASS_VERSION)
         : TARGET_CLASS_NAME3 + " : " +   SOME_TROUBLE
           + "(" + ail3.getVersion() + ")";
  println("...AppInfoLoader3 finished.");
  
  println("All Test Done.");
  exit();
}

インターフェイスは実装を強制する道具

こうしてインターフェイスを使うのであれば、定数フィールドCLASS_NAMEも共通です。ただし値がクラスごとに異なりますので、メソッドに変更しましょう。そのほうが便利になります。インターフェイスにシグネチャgetClassNameを追加しましょう。そして具体的な値は実装時に与えるわけです。こうして、クラス名を取得するメソッドを実装する義務が生じました。今ここに、先ほどのAppInfoLoader3.pdeのタブを加えて実行しようとしても、AppInfoLoader3クラスはConfigInfoインターフェイスのgetClassNameメソッドを実装していませんからエラーを発生し実行できません。

インターフェイスにメソッド追加の変更が生じると、このインターフェイスを実装するすべてのクラスでそのメソッドを追加し実装しなければなりません。インターフェイスは滅多なことで変更すべきではないことが分かります。別の見方をすると、これは利点です。このインターフェイスを利用するすべてのクラスについて、もれなく変更しないと実行できませんから、うっかり修正忘れを防げます。ものは考えようで、良い方に考え、良い方に活用できるよう心がけましょう。

TestAppInfoLoader2.pde
//Test AppInfoLoader class
private final String TARGET_CLASS_NAME4 = "AppInfoLoader4";
private final String SOME_TROUBLE       = "Some trouble happend.";
private final String TARGET_CLASS_VERSION = "1.0";

void setup(){
  test();
}

void test(){
  noLoop();
  println("Test Start.");

  println("...AppInfoLoader4 begins");
  assert AppInfoLoader4.CLASS_NAME.equals(TARGET_CLASS_NAME4)
         : TARGET_CLASS_NAME4 + " : " + SOME_TROUBLE;  
  AppInfoLoader4 ail4 = new AppInfoLoader4();
  assert ail4.CLASS_NAME.equals(TARGET_CLASS_NAME4)
         : TARGET_CLASS_NAME4 + " : " + SOME_TROUBLE;
  assert ail4.getClassName().equals(TARGET_CLASS_NAME4)
         : TARGET_CLASS_NAME4 + " : " + SOME_TROUBLE;
  assert ail4.getVersion().equals(TARGET_CLASS_VERSION)
         : TARGET_CLASS_NAME4 + " : " +   SOME_TROUBLE
           + "(" + ail4.getVersion() + ")";
  println("...AppInfoLoader4 finished.");
  
  println("All Test Done.");
  exit();
}
ConfigInfo.pde
interface ConfigInfo {
  // フィールド
  String CONFIG_FILENAME = "CONFIG.TXT";
  // 抽象メソッド
  String getVersion();
  String getClassName();
}
AppInfoLoader4.pde
public class AppInfoLoader4 implements ConfigInfo{
  public static final String CLASS_NAME = "AppInfoLoader4";
  
  private String version;
  
  public AppInfoLoader4(){
    String lines[] = loadStrings(CONFIG_FILENAME);
    if (0 < lines.length){
      version = "No Version Info(Config file contains " + lines.length + " lines but...)";
      for(String v : lines){
        String[] tokens = split(v, ",");
        if(tokens[0].equals("VERSION")){
          version = tokens[1];
        }
      }
    } else {
      version = "No Info";
    }
  }
  
  public String getVersion(){
    return version;
  }
  public String getClassName(){
    return CLASS_NAME;
  }
}

インターフェイスは代理窓口

インターフェイスは、そのインターフェイスを実装したクラスのインスタンスへの参照を持つことができます。これを簡単に表現すれば、インターフェイスが代理窓口の役目を果たしてくれるということです。

次のコードを読んでください。インターフェイスConfigInfoを実装する、異なる二つのクラスAppInfoLoader4AppInfoLoader5のインタフェイスを ConfigInfoインターフェイスのインスタンスaに代入しています。ConfigInfoの実装部分のテストをするコードは同じですから、このようにインタフェイスを引数として渡すメソッドを書けば、同じコードで別のクラスのテストができてしまうというわけです。

TestAppInfoLoader3.pde
//Test AppInfoLoader class
private final String TARGET_CLASS_NAME4 = "AppInfoLoader4";
private final String TARGET_CLASS_NAME5 = "AppInfoLoader5";
private final String SOME_TROUBLE       = "Some trouble happend.";
private final String TARGET_CLASS_VERSION = "1.0";

void setup(){
  test();
}

void test(){
  noLoop();
  println("Test Start.");
  
  ConfigInfo a = new AppInfoLoader4();
  testAppInfoLoader(a,
                    TARGET_CLASS_NAME4,
                    TARGET_CLASS_VERSION);
  a = new AppInfoLoader5();
  testAppInfoLoader(a,
                    TARGET_CLASS_NAME5,
                    TARGET_CLASS_VERSION);
  
  println("All Test Done.");
  exit();
}

void testAppInfoLoader(ConfigInfo c,
                       String     name,
                       String     ver ){
  println("..." + c.getClassName() + " begins");
  assert c.getClassName().equals(name)
         : name + " : " + SOME_TROUBLE;
  assert c.getVersion().equals(ver)
         : name + " : " +   SOME_TROUBLE
           + "(" + c.getVersion() + ")";
  println("..." + c.getClassName() + " finished.");
}
AppInfoLoader4.pde
public class AppInfoLoader4 implements ConfigInfo{
  public static final String CLASS_NAME = "AppInfoLoader4";
  
  private String version;
  
  public AppInfoLoader4(){
    String lines[] = loadStrings(CONFIG_FILENAME);
    if (0 < lines.length){
      version = "No Version Info(Config file contains " + lines.length + " lines but...)";
      for(String v : lines){
        String[] tokens = split(v, ",");
        if(tokens[0].equals("VERSION")){
          version = tokens[1];
        }
      }
    } else {
      version = "No Info";
    }
  }
  
  public String getVersion(){
    return version;
  }
  public String getClassName(){
    return CLASS_NAME;
  }
}
AppInfoLoader5.pde
public class AppInfoLoader5 implements ConfigInfo{
  public static final String CLASS_NAME = "AppInfoLoader5";
  private static final String CONFIG_FILENAME = "CONFIG.TXT";
  
  private String version;
  
  public AppInfoLoader5(){
    try {
      BufferedReader br = createReader(CONFIG_FILENAME);
      version = "No Info";
      String line = br.readLine();
      while(line != null){
        String[] tokens = split(line, ",");
        if(tokens[0].equals("VERSION")){
          version = tokens[1];
        }
        line = br.readLine();
      }
    } catch(IOException e){
      e.printStackTrace();
    }
  }
  
  public String getVersion(){
    return version;
  }
  public String getClassName(){
    return CLASS_NAME;
  }
}

ConfigInfo.pde先ほどのものと同じです。

TestAppInfoLoader3.pdeのメソッドtestやメソッドtestAppInfoLoaderを読んで便利さを感じましたか? これは実際にデザインパターンを学んだときに真価を実感します。楽しみにしておいてください。

演習

演習1(難易度:easy)

設定ファイルCONFIG.TXTが存在しない場合は、バージョン番号0.0を返すようにAppInfoLoader5クラスを変更しましょう。新しいクラスのファイル名をAppInfoLoader6.pdeとしてください。また、テスト用のsketchファイル名はTestAppInfoLoader4.pdeとしましょう。sketchフォルダdataディレクトリにあるCONFIG.TXTのファイル名を_CONFIG.TXTなどとして、わざと例外を発生させましょう。

演習2(難易度:easy)

インターフェイスConfigInfoに設定ファイルを指定するメソッドを追加しましょう。メソッド名は動作を分かりやすく表すものにしてください。CONFIG.TXTばかりでなくSETTING.TXTなどというファイル名の設定ファイルも読み込めるようにします。これを実装するクラスをAppInfoLoader7とし、AppInfoLoader7.pdeというファイル名で保存しましょう。テスト用のsketchファイル名はTestAppInfoLoader5.pdeとしましょう。AppInfoLoader7のインスタンス生成時にはデフォルトでCONFIG.TXTを読み込みましょう。

まとめ

  • 実装を強制するためのインターフェイスの使い方を学習しました。
  • インスタンスを切り替えるためのインターフェイスの使い方を学習しました。

学習の確認

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

  1. 実装を強制するインターフェイスの役目が理解できましたか?
    1. 理解できた。気持ちよく納得した。
    2. 理解できた。しかし、今ひとつスッキリしない。
    3. 理解できない。
  2. インスタンスを切り替えるためのインターフェイスの役目が理解できましたか?
    1. 理解できた。気持ちよく納得した。
    2. 理解できた。しかし、今ひとつスッキリしない。
    3. 理解できない。

参考文献

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

演習解答

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

おすすめ記事

記事・ニュース一覧