Java Decoratorパターンが何かに似ていると引っかかった
はじめに
今回はGoFデザインパターンの一つである "Decoratorパターン" について考察します。
なぜ Decorator であるかの理由は、最近たまたま出てきて引っ掛かりを覚えたからです。
ごちゃごちゃした内容になります。
2016/07/29大部分修正
下書きで保存しておいたつもりが公開してました、ごめんなさい。
修正個所がわからなくなりますが、多すぎて読みづらくなるので消して書き直しまくります。
普通にDecoratorパターン
クラス図を書くほどでもないのでソースで提示します。
Component.java
public interface Component { int operation(); }
ConcreteComponent.java
public class ConcreteComponent implements Component { public int operation() { return 10; } }
Decorator.java
public abstract class Decorator implements Component { protected Component parent; public Decorator(Component parent) { this.parent = parent; } public abstract int operation(); }
ConcreteDecorator.java
public class ConcreteDecorator extends Decorator { public ConcreteDecorator(Component parent) { super(parent); } public int operation() { return super.parent.operation() * 2; } }
振る舞いは適当に書きましたが、こんな関係でしたね。
ConcreteComponent と ConcreteDecorator が共に Componentインタフェース を実装している為、外から見た機能は同じように扱え、 ConcreteComponent に行き着くまで再帰的に ConcreteDecorator の装飾処理を行うことが特徴でした。
何かに似ている?
上記の実装で利用するときのソースはこうなります。
ConcreteDecorator で三回装飾します。
Component component = new ConcreteDecorator( new ConcreteDecorator( new ConcreteDecorator( new ConcreteComponent() ) ) ); System.out.println(component.operation());
ここからは、部分部分でのシグネチャに注目します。
シグネチャの示したかは以下の書式で示し、カリー化されているものとします。
# 何のシグネチャかに関する説明 第一引数の型 -> 第二引数の型 -> ... -> 第m引数の型 -> 戻り値の型 # 引数が無い場合 戻り値の型 # 明示的に引数がないことを示す場合 () -> 戻り値の型 # 戻り値が無い(void)の場合 引数の型 -> () # 外側から見えない、状態などから値を得ている場合 引数の型 -> { 状態の型1 -> 状態の型2 -> ... -> 状態の型n } -> 戻り値の型
まず、メソッド呼び出しとメソッド内の式について考えます。
# メソッド int operation() のシグネチャ () -> int # operation内の式 super.parent.operation() * 2 のシグネチャ # "* 2" を値に対する操作であると考えて int -> (int -> int) -> int
比較すると、どこからともなく int 型の値が一つと、これを操作する関数が現れたことがわかります。
渡されたわけではない為、文脈と関係のない "状態" とでも呼ぶべき場所から値を得たことになります。
では、この状態を作っているのはどこであるかを考えてみます。
それはズバリ、インスタンス化している場所ですね。
ConcreteComponent は内部に int 型の値を保持しており、Decorator を継承するクラスは Component のインスタンスと int から int への関数を保持しています。
つまり、operation のみに着目してインスタンス化~メソッド呼び出しまでを一つの系と考えると、
# 装飾なし # new ConcreteComponent().operation() () -> { int } -> int # 装飾が一回 # new ConcreteDecorator( # new ConcreteComponent() # ).operation() () -> { int -> (int -> int) } -> int # 装飾が二回 # new ConcreteDecorator( # new ConcreteDecorator( # new Concrete() # ) # ).operation() () -> { int -> (int -> int) -> (int -> int) } -> int # 装飾が三回 # new ConcreteDecorator( # new ConcreteDecorator( # new ConcreteDecorator( # new Concrete() # ) # ) # ).operation() () -> { int -> (int -> int) -> (int -> int) -> (int -> int) } -> int
という経路を辿っていることがわかります。
ここで、装飾する度に増えていく
(int -> int) -> (int -> int)
とはいったい何でしょう?
シグネチャに対応する部分をJavaのラムダ式で考えると
Function<int, int> operation1; Function<int, int> operation2; // ConcreteComponent に近いものから順番に適用される Function<int, int> f = num -> operation2(operation1(num)); // 書き換えて Function<int, int> f_ = operation1.andThen(operation2);
となります。
つまり、関数の合成を行っていることとなります。
何かに似ていると感じたのは、合成関数だったのです。
Decoratorパターンを合成関数で表現
それでは始めに実装した Decorator パターンでの operation と同じ挙動を合成関数で表現してみます。
void run() { Supplier<int> concreteComponent = () -> 10; Function<int, int> concreteDecorator = num -> num + 2; Supplier<int> decorated = () -> concreteDecorator .andThen(concreteDecorator) .andThen(concreteDecorator) .apply(concreteComponent.get()); /* // Supplier に 以下のような andThen メソッドが定義されていれば interface Supplier<T> { <R> Supplier<R> andThen(Function<T, R> f) { return () -> f.apply(get()); } } // このように書ける Supplier<int> decorated = concreteComponent .andThen(concreteDecorator) .andThen(concreteDecorator) .andThen(concreteDecorator); */ System.out.println(decorated.get()); }
Supplier にも andThen の定義があれば綺麗になります。
concreteDecorator の定義を変えてどんどん合成していけば型を変えずに元の処理をどんどん装飾していけますね!
おわりに
今回は Decorator パターンに関してのみ考えましたが、再帰的な構造とポリモーフィズムによる終端の組み合わせを利用したパターンは多分ですがこれと同じ論法で合成関数との類似性を示すことができると思います。(例えば Composite パターンとか)
オブジェクト指向に関数型言語の考え方が当たり前のように利用されている今だからこそ、改めて新しい視点からデザインパターンを見つめることも有用だと感じました。
なお、ここで書いたソースのほとんどはベタで書きなぐったものであり、コンパイルしておらず、動作の保証はできません。
次のエントリも Decorator パターンについて書く予定です。