ObjectSquare [2007 年 4 月号]

[技術講座]


事例で学ぶデザインパターン

第3回 Template Methodパターン
サブクラス化による重複実装の排除と機能のプラグイン

(株)オージス総研
福田 直樹


Template Method パターンはサブクラス化を行って機能を拡張する際に用いられるデザインパターンであり、ソースコードの洗練を行う際にも有効なテクニックです。アプリケーション開発のためのフレームワークを活用する際にも暗黙的にTemplate Method パターンを活用して機能をプラグインするような適用例もあります。

※雑誌『Java WORLD』2006 年 6月号に掲載した記事のオリジナル原稿を Java WORLD 編集部の了解を得て掲載しています。


前回のおさらい

前回は、最初のデザインパターンとしてIteratorパターンを取り上げました。Iteratorパターンは、要素を集約するオブジェクトに対してデータ構造に依存しないアクセス方法を提供するものでした。Iteratorパターンのメリットや登場するクラス要素についての意味合いについてはご理解いただけたでしょうか。1つ1つをじっくり見ていくと、デザインパターンと言っても特別に難しいことをやっているわけではないものです。デザインパターンを初めて適用したA君はCさんの助けもあり、デザインパターンのイメージが沸いてきたようです。今回もA君と一緒にデザインパターンの適用を考えてみてください。

保存形式の異なるファイル出力

今回は、「受講情報照会機能」で取得したデータをファイルに出力する機能を検討します。前回は、受講情報を取り出す部分に焦点を当てて検討を進めましたが、今回はそれをいくつかの保存形式で出力するような部分に焦点を当てます。出力したデータは、Excelや他のメール配信ツールに取り込んだり、ブラウザで多くの社員が参照できるようにすることを目的としている機能のようです。

既存のソースコード

Cさん「今回は、受講情報をファイルに出力する機能を検討してみよう。A君の方でここの部分のソースコードは見てくれたのかい?」
A君「はい、前回Iteratorパターンを適用したため、その部分を修正したものがあります。Aggregateインタフェースを追加したので、以前のものよりもソースコードがすっきりして、洗練されている感じがしますね」
Cさん「なるほど。じゃ、この部分の実装内容も把握できてるわけだね」
A君「そうですね。で、実はここのファイル出力の機能については、ついこの間機能追加の対応をしたんです。以前はCSVの形式だけでファイル保存していたのですが、ブラウザで簡単に見れるようにしたいということでHTMLの形式でも保存できるように対応しました。その部分のソースコードは結構覚えてます。その時は、CSVで出力する部分のソースコードがあったので、それを参考に作成したんですよ。CSVで出力するクラスとHTMLで出力するクラスをちゃんと分けてクラスを定義したんですが」
Cさん「じゃ、早速そのソースコードを見てみようか」

A君が先日実装したというソースコードはリスト1、2のようなものでした。CSVFileSaverは照会した受講情報をCSVファイルで出力するためのクラスです。csvSaveメソッドでは、引数にAggregateインタフェースを取り、IteratorによってParticipationオブジェクトを取得し、その内容をカンマ区切りフォーマットでファイルに出力しています。HTMLFileSaverクラスのhtmlSaveメソッドの方は、同じくAggregateインタフェースを引数に取り、HTMLのタグを挿入する形でファイルに出力しているようです。

リスト1:既存のソースコード(CSVFileSaver)
// HTML 形式でファイルを保存するクラス
public class CSVFileSaver {

    // 受講情報として出力する項目を定数で定義する
    private static final int FIELD_ID = 0;
    private static final int FIELD_DATE = 1;
    private static final int FIELD_SEMINARNAME = 2;
    private static final int FIELD_COMPANY = 3;
    private static final int FIELD_DIVISION = 4;
    private static final int FIELD_NAME = 5;
    private static final int FIELD_EMAIL = 6;
    private static final int SIZE = 7;
    private static final String DELIMITER = ",";
    private static final String DEFAULT_FILENAME = "participation_list.csv";

    // 渡された Aggregate から受講情報を CSV 形式で保存する
    public void csvSave(Aggregate aggregate) throws Exception {
        
        PrintWriter out = null;
        try {

            // ファイル名を指定してのオープン。(処理は簡略化している)
            String fileName = System.getProperty("csv.filename", DEFAULT_FILENAME);
            if(fileName == null) {
                fileName = DEFAULT_FILENAME;
            }
            
            Writer writer = new FileWriter(fileName);
            out = new PrintWriter(new BufferedWriter(writer));

            StringBuffer buf = new StringBuffer();
            
            // 受講情報のデータを変換する
            Iterator iterator = aggregate.iterator();
            while (iterator.hasNext()) {
                
                Participation participation = (Participation) iterator.next();

                List values = Arrays.asList(new String[SIZE]);

                values.set(FIELD_ID, participation.getId());
                values.set(FIELD_SEMINARNAME, participation.getSeminar().getName());
                values.set(FIELD_DATE, participation.getDate());
                values.set(FIELD_COMPANY, participation.getCustomer().getCompanyName());
                values.set(FIELD_DIVISION, participation.getCustomer().getDivisionName());
                values.set(FIELD_NAME, participation.getCustomer().getName());
                values.set(FIELD_EMAIL, participation.getCustomer().getEMail());

                Iterator j = values.iterator();
                if (j.hasNext()) {
                    buf.append(j.next());
                    while (j.hasNext()) {
                        buf.append(DELIMITER);
                        buf.append(j.next());
                    }
                }
                buf.append("\n");

            }

            out.println(buf.toString());
            out.flush();

        } finally {
            if (out != null)
                out.close();
        }

    }

   …略…

}
リスト2:既存のソースコード(HTMLFileSaver)
// HTML 形式でファイルを保存するクラス
public class HTMLFileSaver {

    // 受講情報として出力する項目を定数で定義する
    private static final int FIELD_ID = 0;
    private static final int FIELD_DATE = 1;
    private static final int FIELD_SEMINARNAME = 2;
    private static final int FIELD_COMPANY = 3;
    private static final int FIELD_DIVISION = 4;
    private static final int FIELD_NAME = 5;
    private static final int FIELD_EMAIL = 6;
    private static final int SIZE = 7;
    private static final String SUFFIX = ".html";

    // 渡された Aggregate から受講情報を HTML形式で保存する
    public void htmlSave(Aggregate aggregate) throws Exception {
        
        PrintWriter out = null;
        try {

            // ファイル名を指定してのオープン。(処理は簡略化している)
            DateFormat dFormat = new SimpleDateFormat("yyyyMMddHHmmss");
            String formatDate = dFormat.format(new Date());
            Writer writer = new FileWriter(formatDate + SUFFIX);
            out = new PrintWriter(new BufferedWriter(writer));
            
            StringBuffer buf = new StringBuffer();
            buf.append("<html>");
            buf.append("<head><title> 受講者リスト(HTML版)</title></head>");
            buf.append("<body><table border=\"2\">" + "\n");
            buf.append(aggregate.size() + "件の受講情報があります" + "\n");

            // 受講情報のデータを変換する
            Iterator iterator = aggregate.iterator();
            while (iterator.hasNext()) {
                
                Participation participation = (Participation) iterator.next();

                List values = Arrays.asList(new String[SIZE]);

                values.set(FIELD_ID, participation.getId());
                values.set(FIELD_SEMINARNAME, participation.getSeminar().getName());
                values.set(FIELD_DATE, participation.getDate());
                values.set(FIELD_COMPANY, participation.getCustomer().getCompanyName());
                values.set(FIELD_DIVISION, participation.getCustomer().getDivisionName());
                values.set(FIELD_NAME, participation.getCustomer().getName());
                values.set(FIELD_EMAIL, participation.getCustomer().getEMail());

                Iterator j = values.iterator();
                int counter = 0;
                while (j.hasNext()) {
                    if (counter == 0) {
                        buf.append("<tr>");
                    }
                    buf.append("<td>");
                    buf.append(j.next());
                    buf.append("</td>");
                    counter++;
                }
                buf.append("</tr>" + "\n");

            }

            buf.append("</table></body>" + "\n");
            
            out.println(buf.toString());
            out.flush();

        } finally {
            if (out != null)
                out.close();
        }

    }

   …略…

}
Cさん「うーん、HTMLFileSaverクラスはどうやって作成したんだい?」
A君「はい、その時は何しろ時間がなくて、考える時間がなかったんですよね。HTML形式でファイル保存すればいいということだったんで、とりあえずCSVFileSaverのソースコードをコピー&ペーストしてさくっと作ってしまいました。でも、よく見ると同じような処理をしてる部分がありますね。。ただ、HTML形式とCSV形式でファイル名の付け方や、1件1件の格納データ形式が当然違うわけで、うまいまとめ方がわからなかったんですよ」
Cさん「なるほど。でも、「とりあえず」動くものを、とか「コピー&ペースト」で対応する、というような言葉が出るときは、あまりよくない兆候であることが多いんだ。「とりあえず」作成したプログラムを洗練せずに正式リリースとして採用してしまうと、品質が非常に低いソフトウェアを持つことになる」
A君「そうですね。実はAggregateインタフェースの修正をしたときも思ったんですよね。同じ修正を2箇所しないといけなかったので」
Cさん「そうだね。重複した実装は統合して1つにまとめるようにするべきだね。まあ、これは処理をサブルーチンとしてまとめるのと同じ考え方なんだけど。あと、気になる点としては、csvSaveメソッド、htmlSaveメソッドは、少し処理が長くなってるから、細かく分割していくとやりたいことが見えやすくなってくるよ。メソッドを切り出してまとめた方がよさそうだ」
A君「ちょっと眺めてるだけだとどう切り出せばいいかわかりづらいですね」
Cさん「じゃ、実際に手を動かしながら修正していくことにしよう」

既存のソースコードの洗練

A君に考えてもらう前に、Cさんはまず現状の問題点を整理しました。

Cさん「まずは、現状のメソッドをもう少し細かく分割していこう。CSVFileSaverクラスとHTMLFileSaverクラスを見比べると処理の流れは同じような形でまとめられそうだ」
  1. ファイル名を指定する
  2. ファイルをオープンする
  3. Participationオブジェクトから出力する要素を取り出す
  4. 取り出した情報を加工する
  5. ファイルに出力する
A君「そうですね。両方のクラスとも処理の流れはこのようになってます。えーと、いくつかの処理では同じ実装になりそうなので、共通の実装部分を継承を使ってまとめられないですか?」
Cさん「いい目の付けどころだね。共通の実装にできそうな部分はそのように切り出しておいてもらえるかな」

A君は、Cさんのアドバイスを元に修正を行ったようです。

A君「きちんと考えて切り出すと、結構きれいにメソッドを抜き出せた気がします。各メソッドは以下のようなことを実現するものです。まずはメソッドを切り出しただけですけど」
Cさん「うまく分割されてる感じがするな。じゃ、もう一つ作業やってもらってからソースコードを確認することにしようかな」

Template Methodパターンの適用

A君「これをベースにこれらのスーパークラスを定義して共通部分を定義すればいいんですね。ソースコードの洗練を今までしているような感じですけど、今回はデザインパターンは使わないんですね?」
Cさん「いや、もうすでにデザインパターンを適用できる土台はできているよ。今回適用するデザインパターンはTemplate Methodパターンと言うんだ。このパターンは、継承の機能を利用して処理の重複を排除することができる。さらに、いくつかのテクニックを使って、スーパークラスとサブクラスの役割分担をして、処理を実現する構造を作成するんだよ。Template Methodパターンを適用するためには、あと以下のような点を行うことになる」

Cさん「Template Methodパターンを適用する状況としては、『全体の処理のステップは同じで個別の処理が異なる』という場合に、その異なる処理をサブクラスにまかせてしまうということがポイントになる。このパターンのイメージを表すとこのような感じかな(図1)それを実現するために抽象メソッドにすることもポイントだよ」

デザインパターン説明の流れ
図 1: Template Methodパターンの適用イメージ
A、Cはサブクラスで処理を提供する

A君は、上の対応を行い、リスト3、4、5のようなソースコードが出来上がりました。スーパークラスとしてFileSaverクラスを定義し、csvSaveメソッドとhtmlSaveメソッドを統合したsaveメソッドを定義しています。また、共通の実装であるtoListメソッドの実装を置いています。抽象メソッドとしてはfileNameメソッド、bodyStringメソッドが存在してます。CSVFileSaverクラス、HTMLFileSaverクラスはFileSaverクラスを継承し、抽象メソッドを実装するメソッドとしてfileNameメソッドとbodyStringメソッドで各サブクラス独自の実装を行っています。クラス図は、図2のようにまとめられます。

リスト3:Template Methodパターンを適用したソースコード(FileSaver)
// ファイル保存用抽象クラス
public abstract class FileSaver {

    private static final int FIELD_ID = 0;
    private static final int FIELD_DATE = 1;
    private static final int FIELD_SEMINARNAME = 2;
    private static final int FIELD_COMPANY = 3;
    private static final int FIELD_DIVISION = 4;
    private static final int FIELD_NAME = 5;
    private static final int FIELD_EMAIL = 6;
    private static final int SIZE = 7;

    // 渡された Aggregate から受講情報を保存する
    public final void save(Aggregate aggregate) throws Exception {

        PrintWriter out = null;
        try {
            // ファイル名を指定してのオープン。(処理は簡略化している)
            Writer writer = new FileWriter(fileName());
            out = new PrintWriter(new BufferedWriter(writer));

            StringBuffer buf = new StringBuffer();

            buf.append(headerString(aggregate));
            Iterator iterator = aggregate.iterator();
            while (iterator.hasNext()) {
                Participation participation = (Participation) iterator.next();
                List values = toList((Participation) participation);
                buf.append(bodyString(values));
            }
            buf.append(footerString(aggregate));

            out.println(buf.toString());
            out.flush();

        } finally {
            if (out != null)
                out.close();
        }

    }

    // Participationオブジェクトから必要な要素を取得する
    protected List toList(Participation participation) {

        List values = Arrays.asList(new String[SIZE]);

        values.set(FIELD_ID, participation.getId());
        values.set(FIELD_SEMINARNAME, participation.getSeminar().getName());
        values.set(FIELD_DATE, participation.getDate());
        values.set(FIELD_COMPANY, participation.getCustomer().getCompanyName());
        values.set(FIELD_DIVISION, participation.getCustomer().getDivisionName());
        values.set(FIELD_NAME, participation.getCustomer().getName());
        values.set(FIELD_EMAIL, participation.getCustomer().getEMail());

        return values;

    }
    
    // ファイル名を取得する(抽象メソッド)
    abstract protected String fileName();

    // 取り出すデータを構成する(抽象メソッド)
    abstract protected String bodyString(List values);

    // ヘッダ情報を取得する(フックメソッド)
    protected String headerString(Aggregate aggregate) {
        return "";
    }

    // フッタ情報を取得する(フックメソッド)
    protected String footerString(Aggregate aggregate) {
        return "";
    }

   …略…

}
リスト4:Template Methodパターンを適用したソースコード(CSVFileSaver)
public class CSVFileSaver extends FileSaver {

    private static final String DELIMITER = ",";

    private static final String DEFAULT_FILENAME = "participation_list.csv";

    // ファイル名を取得する
    protected String fileName() {

        String fileName = System.getProperty("csv.filename", DEFAULT_FILENAME);
        if (fileName == null) {
            fileName = DEFAULT_FILENAME;
        }
        return fileName;

    }

    // 出力する文字列を構成する
    protected String bodyString(List values) {

        StringBuffer buf = new StringBuffer();
        Iterator j = values.iterator();
        if (j.hasNext()) {
            buf.append(j.next());
            while (j.hasNext()) {
                buf.append(DELIMITER);
                buf.append(j.next());
            }
        }
        buf.append("\n");

        return buf.toString();

    }

   …略…

}
リスト5:Template Methodパターンを適用したソースコード(HTMLFileSaver)
public class HTMLFileSaver extends FileSaver {

    private static final String SUFFIX = ".html";

    // ファイル名を取得する
    protected String fileName() {

        DateFormat dFormat = new SimpleDateFormat("yyyyMMddHHmmss");
        String formatDate = dFormat.format(new Date());
        return formatDate + SUFFIX;

    }

    // ヘッダ情報を取得する
    protected String headerString(Aggregate aggregate) {

        StringBuffer buf = new StringBuffer();
        buf.append("<html>");
        buf.append("<head><title> 受講者リスト(HTML版)</title></head>");
        buf.append("<body><table border=\"2\">" + "\n");
        buf.append(aggregate.size() + "件の受講情報があります" + "\n");
        return buf.toString();

    }

    // 取り出すデータを構成する
    protected String bodyString(List values) {

        StringBuffer buf = new StringBuffer();

        Iterator j = values.iterator();
        int counter = 0;
        while (j.hasNext()) {
            if (counter == 0) {
                buf.append("<tr>");
            }
            buf.append("<td>");
            buf.append(j.next());
            buf.append("</td>");
            counter++;
        }
        buf.append("</tr>" + "\n");

        return buf.toString();

    }

    // フッタ情報を取得する
    protected String footerString(Aggregate aggregate) {

        StringBuffer buf = new StringBuffer();
        buf.append("</table></body>");
        return buf.toString();

    }

   …略…

}
デザインパターン説明の流れ
図 2: Template Methodパターン適用後のクラス図
(各クラスの属性は省略)
A君「なるほど。これで、各サブクラスでは、独自の実装に関係する部分のみを実装すればいい形になったというわけですね。また、FileSaverクラスに保存の処理の流れを一元化したために、重複実装部分がなくなりました。今度、新たな保存形式が追加されたとした場合には、FileSaverクラスのサブクラスを追加して、そこでは使用するファイル名の規則とデータの格納形式を定義すればいいわけですね」
Cさん「Template Methodパターンのメリットがわかってるじゃないか。あと、補足としては、FileSaverクラスに定義しているheaderStringメソッドとfooterStringメソッドだけど、これは抽象メソッドにしてないけど、これはなんでかわかるかい?」
A君「ここは抽象メソッドにしてもいいと思ったんですが。。」
Cさん「抽象メソッドにすると、サブクラス側では実装しないといけないから、サブクラスの責任が重くなってしまうよね。CSVFileSaverのようにヘッダやフッタの情報を出さなくていい場合にも実装をしないといけないのは面倒だ。だから、具象メソッドとして定義しておいて、必要があればサブクラスでオーバーライドしてもらう、という形で定義してるんだね」
A君「オプションとして定義したければする、というような位置付けなんですね」
Cさん「これは、Template Methodパターンを適用する時の注意点で、サブクラスで『必ず』オーバーライドしなければいけないメソッドは必要最小限にするべきなんだ。意味のあるまとまりでメソッドを定義することも重要なことなんだ。それと、あともう1点、FileSaverクラスのsaveメソッドはfinalで定義している。これは、サブクラスで処理の手順をオーバーライドして定義しないようにfinalとしてるんだ。サブクラスではスーパークラスの処理の手順を前提に実装してるのに、それを再定義できてしまうとまずいからね」

Template Methodパターンの基本構造

それでは、今回紹介したTemplate Methodパターンの基本構造を確認しておきましょう。

ご覧いただいたとおり、Template Methodパターンは、処理(アルゴリズム)のステップを定義しておき、必要に応じてサブクラスの方で各ステップの処理内容をプラグインして提供するデザインパターンです。処理のステップを定義したメソッドが「テンプレート・メソッド」となるわけです。

その基本構造は、図3のようになります。また、Template Methodパターンの構成要素(および、本稿で紹介したサンプル・アプリケーション内の各クラスとの対応)は表1のようになります。

デザインパターン説明の流れ
図 3: Template Methodパターンの基本クラス構造
表1:Template Methodパターンの構成要素
構成要素
役割
対応するサンプル・プログラムのクラス/インタフェース
AbstractClass
templateMethodを実装し、抽象メソッドやフックメソッドであるprimitiveOperationを定義した抽象クラス
FileSaver
ConcreateClass AbstractClassで定義された抽象メソッドやフックメソッドを実装する具象クラス
CSVFileSaver、HTMLFileSaver

Template Methodパターンを適用したメリットをまとめておきましょう。

Template Methodパターンは、その性質からアプリケーション開発のためのフレームワークでも活用されている例があります。フレームワークでは、処理のステップ(アルゴリズム)を定義しておき、そのフレームワークを使う側は、そのクラスを継承し、オーバーライドすべきメソッドを実装すればフレームワークで提供する機能が使えるということです。

A君は、Template Methodパターンを適用し、ソースコードがきれいに整理できたことにいいことをした喜びのようなものを感じているようです。重複した実装をしないことや、複雑さを軽減させるような意識は設計の品質を高め、デザインパターンを理解する上でも重要なことです。皆さんもこまめにソースコードの整理整頓を意識するようにしてみてください。

《参考文献》

  1. 「オブジェクト指向における再利用のためのデザインパターン」
    著者:Erich Gamma, Rechard Helm, Ralph Jonson, John Vlissides
    発行:ソフトバンククリエイティブ, 1999

© 2007 OGIS-RI Co., Ltd.
Prev Index Next
Prev. Index Next