Decoratorパターン

作成:2004年11月17日

吉田誠一のホームページ   >   ソフトウェア工学   >   技術コラム   >   デザインパターン

Decoratorパターンは、自由に組み合わせられる拡張機能を提供する時のパターンである。

目次

  1. 機能拡張におけるDecoratorパターンの利点
  2. Iteratorの装飾によるフィルタリング
  3. Template Methodパターンとの関係
  4. Decoratorパターンで遭遇する問題
    1. インターフェースを変更せざるを得ないケース
    2. 共通の枠組みで統合しきれないケース

機能拡張におけるDecoratorパターンの利点

クラスの持つ機能を拡張する方法としては、一般には継承か委譲が使われる。しかし、これらの方法では、拡張の順序によって、クラスどうしに不要な従属関係が生じてしまう、という問題がある。

例えば、ファイルの読み書きをするFileクラスがあるとしよう。ここで、

  • 圧縮されたファイルの読み書きもできるようにする。
  • 暗号化されたファイルの読み書きもできるようにする。

という、2つの機能を追加することにする。

圧縮されたファイルの読み書きをするクラスをCompressedFile、暗号化されたファイルの読み書きをするクラスをCipheredFileとする。

まず、Fileクラスを拡張して、圧縮機能を持ったCompressedFileクラスを作ったとしよう。問題は、CipheredFileクラスである。

Fileクラスを拡張してCipheredFileクラスを作ることもできる。しかし、そうすると、圧縮されて、かつ暗号化されたファイルは、読み書きできなくなってしまう。

かといって、CompressedFileクラスを更に拡張してCipheredFileクラスを作るのは、妙である。暗号化は、圧縮とは無関係の機能なのに、クラス構成上は、暗号化が圧縮に従属した形になってしまうためだ。

継承による拡張、委譲による拡張

Decoratorパターンの利点は、拡張の組み合わせや順序が自由に決められる点である。圧縮されたファイルの読み書きをする機能をCompressDecorator、暗号化されたファイルの読み書きをする機能をCipherDecoratorとする。すると、下記のように、いずれの場合でも対応できるようになる。

  1. 圧縮されたファイルの読み書きをする場合
    File file = new CompressDecorator(new File(foo));
                
  2. 暗号化されたファイルの読み書きをする場合
    File file = new CipherDecorator(new File(foo));
                
  3. 圧縮された後で暗号化されたファイルの読み書きをする場合
    File file = new CompressDecorator(new CipherDecorator(new File(foo)));
                
  4. 暗号化された後で圧縮されたファイルの読み書きをする場合
    File file = new CipherDecorator(new CompressDecorator(new File(foo)));
                

また、圧縮の機能と、暗号化の機能が、クラス構成上、対等な位置関係になる。

Iteratorの装飾によるフィルタリング

さまざまな条件を指定してデータを検索する時は、IteratorパターンとDecoratorパターンを組み合わせて使うと良い。

Iteratorパターンの考察では、図書館データベースで本の検索をした時に、結果をIteratorとして返す例を紹介した。

例えば、著者名が「吉田誠一」の本を探す場合を考える。図書館データベースをLibraryDatabaseクラス、検索条件をBookSearchConditionクラスとすると、著者名が「吉田誠一」の本を探す処理は、下記のようなコードになる。

LibraryDatabase database = new LibraryDatabase();

BookSearchCondition condition = new BookSearchCondition();
condition.setAuthorName("吉田誠一");

Iterator iterator = database.searchBook(condition);
        

だが、本の検索では、下記のように、さまざまな条件を指定する可能性がある。

  • 2000年以降に発行された本を探す。
  • タイトルに「銀河」が含まれた本を探す。

上記の方法では、検索条件を表すBookSearchConditionクラスに、これらの条件を指定するためのメソッドを追加する必要がある。

  • 2000年以降に発行された本を探す。
    • setPublishedDateメソッドを追加。
  • タイトルに「銀河」が含まれた本を探す。
    • setTitleWordメソッドを追加。

しかし、この方法では、BookSearchConditionクラスがたくさんのメソッドを持つことになってしまう。新たな検索方法を思い付くたびに、メソッドを追加し、検索処理を書き換える必要がある。

検索条件は、組み合わせによって更に膨れ上がる可能性がある。例えば、下記のような例である。

  • 著者名が「吉田誠一」または「古田誠一」で、タイトルに「銀河」または「天の川」が含まれた本を探す。

こうなると、膨大な検索条件ごとにメソッドを用意することは不可能となる。

このようなケースでは、Decoratorパターンを使って、Iteratorを装飾すると良い。検索条件に一致した本のみを返すような、フィルタリングの機能を持つIteratorを、数多く作るのだ。

例えば、著者名がで検索する場合は、下記のようなクラスを用意する。

public class AuthorFilter implements Iterator {
        private Iterator iterator;
        private String author;
        private Book next_book;

        public AuthorFilter ( Iterator iterator, String author ) {
                this.iterator = iterator;
                this.author = author;

                forward();
        }

        public boolean hasNext ( ) {
                return (next_book != null);
        }

        public Book next ( ) {
                Book book = next_book;
                forward();
                return book;
        }

        private void forward ( ) {
                while (iterator.hasNext()) {
                        Book book = iterator.next();
                        if (book.getAuthor().equals(author)) {
                                next_book = book;
                                return;
                        }
                }
                next_book = null;
        }
}
        

例えば、著者名が「吉田誠一」で、タイトルに「銀河」が含まれた本を探す場合は、下記のようなコードになる。

LibraryDatabase database = new LibraryDatabase();

// すべての本を返すiteratorを作る。
Iterator iterator = database.iterator();

// 著者名が「吉田誠一」の本だけを返すiteratorを作る。
iterator = new AuthorFilter(iterator, "吉田誠一");

// さらに、タイトルに「銀河」が含まれた本だけを返すiteratorを作る。
iterator = new TitleWordFilter(iterator, "銀河");
        

Decoratorパターンを使った検索では、アプリケーションの都合で検索条件が増えても、それに応じたDecoratorを作り、アプリケーションのコードを書き換えるだけで良い。

Template Methodパターンとの関係

Decoratorパターンは、機能の拡張を委譲で行い、機能を追加した後も元のクラスと同一視することで、自由に拡張できるようにした点が特徴である。

しかし、元になるクラスでTempalte Methodパターンが使われている時は、継承による機能の拡張が強制されてしまい、そのままではDecoratorパターンを採用できないことがある。

例えば、冒頭のファイルの読み書きをする例で、元になるFileクラスにTemplate Methodパターンが使われて、下記のような抽象メソッドが呼び出されるようになっていたとしよう。

public abstract class File {
        protected abstract byte[] convert ( byte[] buffer );
}
        

機能を追加する時は、このFileクラスを継承し、convertメソッドを実装することで、Fileクラスを拡張するようになっている。

圧縮したファイルの読み書きをするためには、Fileクラスの子クラスとしてCompressedFileクラスを作成し、convertメソッドに圧縮処理を記述する。暗号化したファイルの読み書きをするためには、Fileクラスの子クラスとしてCipheredFileクラスを作成し、convertメソッドに暗号化処理を記述する。

しかし、冒頭で述べたように、この方法では問題がある。

このようなケースでDecoratorパターンを使うためには、下記のような、中間的なクラスDecoratedFileを作ることになる。

public class DecoratedFile extends File {
        private Converter converter = null;

        public void setConverter ( Converter converter ) {
                this.converter = converter;
        }

        protected byte[] convert ( byte[] buffer ) {
                if (converter == null)
                        return buffer;

                return converter.convert(buffer);
        }
}

public interface Converter {
        public abstract byte[] convert ( byte[] buffer );
}

public abstract class ConverterDecorator implements Converter {
        protected Converter converter;

        protected ConverterDecorator ( Converter converter ) {
                this.converter = converter;
        }
}
        

圧縮や暗号化の機能は、ConverterDecoratorの子クラスとして作る。

中間的なクラスを使ったクラス構成

この時、変換処理を行うConverter系のクラスには、Decoratorパターンが使用されている。但し、ファイルを表すFile系のクラスは、Decoratorパターンの枠組みから外れることになる。

Decoratorパターンで遭遇する問題

Decoratorパターンは、Java言語のI/O処理で使われているため、Java言語を使っている人にはたいへん親しみがあるパターンである。しかし、実際に適用してみると、意外と多くの困難にぶつかることがある。ここでは、Decoratorパターンを使った時に遭遇しやすい問題を紹介する。

なお、下記の本(以下「デザインパターン入門」と記す)

Java言語で学ぶデザインパターン入門

結城浩著

ソフトバンクパブリッシング株式会社

で取り上げられている、文字列の周りに飾り枠をつけて表示する例を基に説明する。「デザインパターン入門」では、共通となるインターフェースは、下記のような抽象メソッドを持つDisplayクラスである。

int getColumns()

横の文字数を得る。

int getRows()

縦の行数を得る。

String getRowText(int row)

row番目の文字列を得る。

インターフェースを変更せざるを得ないケース

Decoratorパターンでは、すべてのクラスが同じインターフェースを持つ。だが、後日になって、具体的なクラスを1つ追加しようとした際に、メソッドに引数を増やす必要が生じたり、メソッドが投げる可能性のある例外が増えることがある。特に、メソッドが投げる例外が増えてしまうケースが多い。すると、これまでに作ったDecoratorも含めて、すべてのクラスを修正しなくてはならない。

「デザインパターン入門」の例に、StringDisplayの他に、指定したファイルの中身を表示するFileDisplayクラスを追加するとしよう。

FileDisplayクラスは、指定した番目の文字列を得ようとした際に、ファイルアクセスを行うので、処理中にIOExceptionが投げられる可能性がある。しかし、Displayクラスに定義されているgetRowTextメソッドは、キャッチしなければならない例外を持たない。Displayクラスを作った時には、具体例としてStringDisplayクラスしか想定されていなかったため、getRowTextメソッドが例外を投げる可能性があるとは、思い付かなかったのである。

FileDisplayクラスを追加するためには、StringDisplayや、各種Borderクラスのすべてで、getRowTextメソッドがIOExceptionを投げるように修正しなければならない。

Decoratorパターンは、機能を細分化し、たくさんのクラスに分割できる点が特徴であり、その分、クラスの数が多くなる。一般にインターフェースの変更は影響が大きいが、Decoratorパターンでは特に、修正範囲が広くなりやすい。

共通の枠組みで統合しきれないケース

さまざまな機能を表すクラス群のうち、ほとんどはDecoratorパターンで共通のインターフェースの下に統合されているのに、1つ2つだけ、例外的にその枠組に収まりきらないクラスができてしまうことがある。

「デザインパターン入門」の例に、Borderの1つとして、showメソッドが呼び出された回数を数えて、その回数を行頭に表示する、CallRepetitionBorderクラスを追加するとしよう。

例えば、

StringSisplay b1 = new StringDisplay("Hello, world.");
CallRepetitionBorder b2 = new CallRepetitionBorder(b1);

b2.show();
b2.show();
b2.show();
          

というコードを実行すると、下記のように表示される。

1: Hello, world.
2: Hello, world.
3: Hello, world.
          

この機能だけ考えれば、「デザインパターン入門」に載っているSideBorderやFullBorderと良く似ているので、それらと同じく、Borderクラスのサブクラスとして作りたい。ところが、Borderクラスのサブクラスでは、showメソッドが呼び出された回数を数えることはできない。そのため、CallRepetitionBorderクラスは、Displayインターフェースとほとんど同じメソッドを持つにも係わらず、Displayインターフェースで統合されたDecoratorパターンの枠組から外れた、まったく別のクラスとして作るしかないのである。

飾り枠をつけて表示する一連のクラスの構成を、クラス図としてまとめた時、CallRepetitionBorderクラスだけが浮いてしまうことになる。機能的にはSideBorderやFullBorderと同じ位置付けのものなので、クラス構成でも同じ位置付けに置くべきなのだが、そうできなくなってしまう。即ち、クラスの意味と構成が乖離してしまい、結果として紛らわしい設計となってしまう。

CallRepetitionBorderクラスをBorderクラスのサブクラスとして作成し、Decoratorパターンの枠組みに収めるためには、親のDisplayクラスに手を入れる必要がある。しかし、サブクラスを作るたびにこのようなことを繰り返していると、結局は、あらゆる機能がすべてDisplayクラスで実装されていた、という結果になりかねない。

Copyright(C) Seiichi Yoshida ( comet@aerith.net ). All rights reserved.