もっとポリモーフィズム

前回までのプログラムは分割した文字列をコンソールに一行ずつ表示していました。
しかし、実際に役に立つアプリケーションを作成する時にコンソールに出せば終わりというケースはごくまれです。というわけでまたクライアントから仕様追加がなされます。

  • 分割した結果をWriterに出力できるようにする。形式は一行ずつbタグで囲むこと。

この仕様に対応するにはどうすれば良いでしょうか。また、今回の仕様に対応するだけだといつ次の変更を受けるかわかりません。元のプログラムをその場だけに対応して書くのは得策では無いでしょう。

差し替え可能に

SplitStringOutputterの派生クラスであるCharSplitStringOutputterとStringSplitStringOutputterとにそれぞれ出力形式を示すものを外から渡せるようにするのはどうでしょうか。下記のようになります。

package ms2310.oot.section2.sample0;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;

public class SplitSample {

  public static void main(String[] args) {

    try {
      {
        //コンソールに出力するサンプル
        SplitHandler handler = new ConsoleLineOutputter();
        {
          //一文字のデリミタ
          String input = "こんな,文字列を,,分解します";
          char delim = ',';

          SplitStringOutputter outputter = new CharSplitStringOutputter(
              handler, input, delim);
          outputSplitString(outputter);
        }

        {
          //複数文字のデリミタ
          String input = "こんな::文字列を::::分解します";
          String delim = "::";

          SplitStringOutputter outputter = new StringSplitStringOutputter(
              handler, input, delim);
          outputSplitString(outputter);
        }
      }

      {
        //Writerに出力するサンプル
        StringWriter writer = new StringWriter();
        SplitHandler handler = new TaggedWriterOutputter(writer);
        {
          String input = "こんな::文字列を::::分解します";
          String delim = "::";

          SplitStringOutputter outputter = new StringSplitStringOutputter(
              handler, input, delim);
          outputSplitString(outputter);
        }

        System.out.println("# Writerの内容です");
        System.out.print(writer.toString());
      }
    } catch (Exception e) {
      // エラー処理を省略しています
      e.printStackTrace();
    }

/* 出力
# 分解開始
こんな
文字列を

分解します
# 分解終了
# 分解開始
こんな
文字列を

分解します
# 分解終了
# 分解開始
# 分解終了
# Writerの内容です
<b>こんな</b>
<b>文字列を</b>
<b></b>
<b>分解します</b>
 */
  }

  static interface SplitStringOutputter {
    void output() throws IOException;
  }

  static interface SplitHandler {
    void handleSplit(String str);
  }

  static class ConsoleLineOutputter implements SplitHandler {

    public void handleSplit(String str) {
      System.out.println(str);
    }
  }

  static class TaggedWriterOutputter implements SplitHandler {

    private Writer writer;

    public TaggedWriterOutputter(Writer writer) {
      this.writer = writer;
    }

    public void handleSplit(String str) {
      try {
        writer.write("<b>");
        writer.write(str);
        writer.write("</b>");
        writer.write('\n');
      } catch (IOException e) {
        // エラー処理を省略しています。
      }
    }
  }

  static class CharSplitStringOutputter implements SplitStringOutputter {
    private char delim;

    private String input;

    private SplitHandler handler;

    CharSplitStringOutputter(SplitHandler handler, String input, char delim) {
      this.delim = delim;
      this.input = input;
      this.handler = handler;
    }

    public void output() throws IOException {
      StringReader reader = new StringReader(input);
      int c;
      StringBuffer buf = new StringBuffer();
      for (;;) {
        c = reader.read();
        if (c == delim) {
          handler.handleSplit(buf.toString());
          buf = new StringBuffer();
        } else if (c == -1) {
          handler.handleSplit(buf.toString());
          break;
        } else {
          buf.append((char) c);
        }
      }
    }
  }

  static class StringSplitStringOutputter implements SplitStringOutputter {
    private String delim;

    private String input;

    private SplitHandler handler;

    StringSplitStringOutputter(SplitHandler handler, String input, String delim) {
      this.delim = delim;
      this.input = input;
      this.handler = handler;
    }

    public void output() throws IOException {
      BufferedReader reader = new BufferedReader(new StringReader(input));
      int c;
      StringBuffer buf = new StringBuffer();

      char[] delimBuf = new char[delim.length()];
      for (;;) {
        reader.mark(delim.length());
        c = reader.read(delimBuf);

        if (c != delim.length()) {
          handler
              .handleSplit(buf.append(new String(delimBuf, 0, c)).toString());
          break;
        } else if (delim.equals(new String(delimBuf))) {
          handler.handleSplit(buf.toString());
          buf = new StringBuffer();
        } else {
          reader.reset();
          c = reader.read();
          buf.append((char) c);
        }
      }
    }
  }

  private static void outputSplitString(SplitStringOutputter outputter)
      throws IOException {

    System.out.println("# 分解開始");
    outputter.output();
    System.out.println("# 分解終了");
  }
}

元々の仕様はFileへの出力等を目指したものです。『Writerに書ける⇒FileWriterに書ける』というポリモーフィズムの性質が適用されます。今回の例ではStringWriterに書くことにしました。
新たに登場したSplitHandlerは文字列を分解する度に呼ばれるハンドラメソッドhandleSplitを持っています。文字を分解した後それをコンソールに出すのか、それともWriterに書くのか、装飾の文字列(bタグ等)を付けるのか等、分解後の様々な処理を担当します。これを実装したのがConsoleLineOutputterとTaggedWriterOutputterです。

差し替え可能の利点

新たなインターフェースを作成した理由がわからなければ、自分で同様の問題を解決することはできません。「教科書の例題は読めるけれど、自分では解けない」子ではつまらないですね。
では、今回のケースでの利点は何でしょうか。

  • 出力形式の新たな変更が起きた場合(例えばbタグではなくdivタグに)SplitHandlerの派生クラス以外変更する必要が無い→バグを埋め込む可能性が減る。
  • 出力形式以外の汎用的な部分を使いまわすことが出来る。

時間の節約ができるというのが根源的なものでしょうか。バグが減るということは結局デバッグする時間が短縮できることになります。これからはたとえ「分割した文字列をメールで送る」というような仕様変更があってもSplitHandlerの派生クラスでメールを送信する機能を作れば良いことになります。


ここでは示せなかったのですが、下記のようなこともあります。

  • テスト用の実装と、本番用の実装を準備するようなことが可能になる*1

*1:ユニットテストの自動化の話を書く機会があればこの件も書きたいです