Decoratorパターン


 ★★☆

Decoratorパターン

動的に機能を拡張し、柔軟に組み合わせたりできる

Decorator とは、英語で「装飾者」を意味する言葉です。Decorator パターンでは、飾り枠と中身を同一視することで、より柔軟な機能拡張方法を提供します。

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

そんなときは Composite パターンが威力を発揮します。Composite パターンは、要素であるオブジェクトと、複数の要素からなる複合オブジェクトを区別なく扱えるという特徴を持ちます。この特徴を利用することで、構造を再帰的に組み立て、クライアントからの見た目をシンプルに保つことができます。


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

まず、Fileクラスを拡張して、圧縮機能を持ったクラスを作ったとしましょう。

次に、暗号化機能を持ったクラスを作ります。 Fileクラスを継承して作ることもできますが、そうすると、圧縮されて、かつ暗号化されたファイルは、読み書きできなくなってしまいます。

かといって、圧縮機能を持ったクラスを更に継承して作ると、今度は圧縮しないで暗号化することができなくなってしまいます。



Decoratorパターンの利点は、拡張の組み合わせや順序が自由に決められる点です。飾り枠を使って中身を包んでも、インタフェースはは少しも隠されません。クラスの持つメソッドは他のクラスから見ることができます。これをインタフェースが「透過的」であるといいます。すなわち、飾り枠をたくさん使って包んでも、インタフェースはまったく変更されないのです。ですから、 Decoratorパターンは、拡張の組み合わせや順序が自由に決められるのです。

インタフェースが「透過的」であるため、 Decoratorパターンでは、Compositeパターンに似た再帰的な構造が登場します。すなわち、「飾り枠」が保持している「中身」が、実際には別のものの「飾り枠」になったいるという構造です。 DecoratorパターンとCompositeパターンは、再帰的な構造を扱うという点では似ていますが、目的は異なります。 Decoratorパターンは、外枠を重ねるということで機能を追加していく点に主眼があるからです。



例題







元の文字列
AAAAA
BBB

文字列を修飾して表示するクラスを作成しなさい。

ひとつは SideBorder で、文字列の両側に、指定文字をつけます。もうひとつは FullBorder で、文字列の周りを罫線で囲みます。また、これらを組み合わせて何回も修飾できるようにします。


実行結果
SideBorder
(%で囲む)
%AAAAA%  
%BBB%





       組み合わせ
+-------+  
|%AAAAA%|
|%BBB%  |
+-------+

FullBorder
+-----+  
|AAAAA|
|BBB  |
+-----+










Decoratorパターンを使用しない例

AbstractDisplay.java
public abstract class AbstractDisplay { protected String[] str; protected int height; protected int width; public AbstractDisplay() {} public AbstractDisplay(String[] str) { height = str.length; this.str = new String[height]; for (int i = 0; i < height; i++) this.str[i] = str[i]; width = getCharCount(); } public abstract String getLine(int n); public abstract int getLineCount(); private int getCharCount() { int cnt = 0; for (int i = 0 ; i < str.length ; i++) { if (cnt < str[i].getBytes().length) cnt = str[i].getBytes().length; } return cnt; } public final void show() { for (int i = 0 ; i < getLineCount() ; i++) { System.out.println(getLine(i)); } } }
SideBorder.java
public class SideBorder extends AbstractDisplay { private char border; public SideBorder(String[] str, char border) { super(str); this.border = border; } public String getLine(int n) { return border + str[n] + border; } public int getLineCount() { return str.length; } }
FullBorder.java
public class FullBorder extends AbstractDisplay { public FullBorder(String[] str) { super(str); } public String getLine(int n) { if (n == 0 || n == getLineCount() - 1) return "+" + repeat("-", width) + "+"; else { String str = this.str[n - 1]; return "|" + str + repeat(" ", width - str.length()) + "|"; } } public int getLineCount() { return str.length + 2; } private String repeat(String str, int count) { StringBuilder b = new StringBuilder(str.length() * count); while(count-- > 0) b.append(str); return b.toString(); } }
Main.java
public class Main { public static void main(String[] args) { String[] str = {"AAAAA", "BBB"}; SideBorder s1 = new SideBorder(str, '%'); s1.show(); FullBorder s2 = new FullBorder(str); s2.show(); } }

Decoratorパターンを使用した例

AbstractDisplay.java
public abstract class AbstractDisplay { protected AbstractDisplay disp; protected int height; protected int width; protected AbstractDisplay() { } protected AbstractDisplay(AbstractDisplay disp) { this.disp = disp; } protected int getCharCount() { return width; } protected int getLineCount() { return height; } protected abstract String getLine(int n); public final void show() { for (int i = 0 ; i < getLineCount() ; i++) { System.out.println(getLine(i)); } } }
SimpleText.java
public class SimpleText extends AbstractDisplay { private String[] str; public SimpleText(String[] str) { int height = str.length; this.str = new String[height]; width = 0; for (int i = 0; i < height; i++) { this.str[i] = str[i]; if (width < str[i].length()) width = str[i].length(); } this.height = height; } protected String getLine(int n) { return str[n]; } }
SideBorder.java
public class SideBorder extends AbstractDisplay { private char border; public SideBorder(AbstractDisplay disp, char border) { super(disp); this.border = border; width = disp.getCharCount() + 2; height = disp.getLineCount(); } protected String getLine(int n) { return border + disp.getLine(n) + border; } }
FullBorder.java
public class FullBorder extends AbstractDisplay { public FullBorder(AbstractDisplay disp) { super(disp); width = disp.getCharCount(); height = disp.getLineCount() + 2; } protected String getLine(int n) { if (n == 0 || n == height - 1) return "+" + repeat("-", width) + "+"; else { String str = disp.getLine(n - 1); return "|" + str + repeat(" ", width - str.length()) + "|"; } } private String repeat(String str, int count) { StringBuilder b = new StringBuilder(str.length() * count); while(count-- > 0) b.append(str); return b.toString(); } }
Main.java
public class Main { public static void main(String[] args) { String[] str = new String[] { "AAAAA", "BBB" }; AbstractDisplay d1 = new SimpleText(str); AbstractDisplay d2 = new SideBorder(d1, '%'); d2.show(); AbstractDisplay d3 = new FullBorder(d2); d3.show(); } }

まずは、Decorator パターンを意識せずに、2種類のクラスを作成してみましょう。SideBorder は、与えられた文字列に指定の文字をつけます。FullBorder は、与えられた文字列の周りを罫線で囲みます。

ともに、引数としてString 型が与えられます。よって、SideBorder と FullBorder の組み合わせ、つまり SideBorder の結果を FullBorder に与えるとか、その逆ができません。

(SideBorder の結果は、Display 型です。)

Decorator パターンでは、 SideBorder も FullBorder もともに引数として AbstractDisplay 型を受け取ります。つまり、 SideBorder や FullBorder の結果をそのまま受け取ることができるのです。いったん、SimpleText によって String 型を AbstractDisplay 型にすれば、文字列の両側に ‘%’ をつけ、さらに罫線で囲って、また、全体の 両側に ‘$’ をつけるということができるようになるわけです。

Decorator パターンでは、委譲が使われています。「飾り枠」に対して行われた要求(メソッドの呼び出し)は、その「中身」に処理の実行が依頼されます。この例では、SideBorder や FullBorder の コンストラクタで disp.getCharCount() と disp.getLineCount() を呼び出し、中身の文字数や行数を求めています。(disp が中身のインスタンス)

Decorator パターンを使うと、多様な機能追加を行うことができます。具体的な「飾り枠」をたくさん用意しておけば、それらを自由に組み合わせて新しいオブジェクトを作ることができるからです。その際、個々の飾り枠は単純でもかまいません。組み合わせていけば複雑なものも作れるからです。ただし、このせいで、よく似ている小さなクラスがたくさん作られてしまうという欠点もあります。