書きたいことを書きたいだけ。

とある会社員の生活記録。

デザインパターン「Decorator」

ここに書いてある内容は個人のアウトプットを吐き出している場です。
違うことが多々あると思いますが、温かい目で見てください。

Decoratorパターン

どんな時に使うのか?(駄目パターン)

以下は駄目駄目パターンです。
こんな設計したら多分椅子が飛んできます。
例えばこんな設計をしようと思ったときです。
牛丼チェーン店のメニューを作ろうと思った時。
更に牛丼に対してトッピングを追加しようと考えた時

f:id:itisyuu:20190419001920p:plain

めちゃくちゃ適当ですけどクラス図で表したらこんな感じです。

実際の実装するソースコード

public abstract class Food {
    String foodName;
    int price;
    public void order(){
        System.out.println(foodName + ":¥" + price);
    }
    public void setCheese(){
        foodName = foodName + "+チーズ";
        price = price + 50;
    }
    public void setMasterd(){
        foodName = foodName+ "+マスタード";
        price = price + 30;
    }
    public void setMayonnaise(){
        foodName = foodName + "+マヨネーズ";
        price = price + 60;
    }

上のコードが抽象クラスのFoodです。
FoodにはfoodNameやpriceを持たせて、更にトッピングを追加するにはsetCheese()等を呼び出せば値段が上がります。
ついでに名前の部分も分かりやすいように追記します。

で以下が実際に継承したもの。

public class Gyudon extends Food{
    
    public Gyudon(){
        foodName = "普通の牛丼";
        price = 500;
    }
}

public class BigGyudon extends Food{
    public BigGyudon(){
        foodName = "大きい牛丼";
        price = 700;
    }   
}

ブログ書きながら思ったけど牛丼しか作らなかったら継承する意味なかった気がしますが、まぁ分かりやすいので気にせず行きましょう。

で、動作させるには

public class Main {
    public static void main(String[] args){
        Food bigGyudon = new BigGyudon();
        Food normalGyudon = new Gyudon();
        
        bigGyudon.setCheese();
        bigGyudon.order();
        
        normalGyudon.setCheese();
        normalGyudon.setMasterd();
        normalGyudon.setMayonnaise();
        normalGyudon.order();
        
    }
}

こんな感じでmain()を書いて、各bigGyudonとかをインスタンス化します。
ついでに好きなトッピングがあればset~~~()メソッドで追加します。
こいつを実行すると、

大きい牛丼+チーズ:¥750
普通の牛丼+チーズ+マスタード+マヨネーズ:¥640

てな感じにトッピングが追加された牛丼が完成されました。
現時点ではなんの問題も無いように動いているかも知れません。

問題が発生した。

①新しいフードメニュー(例えばカレー)が追加された。 →setMasterd()とか適切ではないメソッドを持ってしまっている。(ここではカレーにマスタードは絶対入れないものとして)

②牛丼の大きさによってトッピングの値段を変更したい。 →現在はsetCheese()が並でも大でも同じ値段になっている。対応策として抽象クラスのFoodのメソッドとして、setBigCheese()とか加えるとキリが無い。

等など、この設計は新しい設計が入ると途端に破綻します。

Decoratorパターンを使おう

こんなときに役に立つのがDecoratorパターンです。
大雑把にどんなパターンかと言うと、
①牛丼を作る。
②チーズで装飾する。
マスタードで装飾する。
④最後にトータルの値段を計算する。
てな感じに牛丼に対してどんどん装飾をしていって対応します。これがDecoratorパターンの由来です。

Decoratorパターンに作り変える

ここでトッピングの値段の計算方法ですが、抽象クラスで実装を行うのではなく、サブクラスに委譲したいと思います。
クラス図としては以下になります。

f:id:itisyuu:20190419012155p:plain

ついでに「Soba」や「Udon」を付け加えておきました。
またトッピングに対しては別のクラスを作っています。
トッピング(CheeseやMasterd等)はDecoratorを拡張します。
またDecoratorはFoodを拡張しています。

実際に実装してみる

public abstract class Food {
    String name;
    public String getName(){
        return name;
    }
    public abstract int calc();
}

抽象クラスであるFoodをこのように書き換えました。
どんどん行きましょう。

public class Gyudon extends Food{
    public Gyudon(){
        name = "普通の牛丼";
    }
    public int calc(){
        return 500;
    }
}

public class BigGyudon extends Food{
    public BigGyudon(){
        name = "大きい牛丼";
    }   
    public int calc(){
        return 700;
    }
}

GyudonとBigGyudonクラスです。
コンストラクタが呼ばれたらnameをそれぞれの名前に設定します。
calc()は値段を返すだけにしています。

Decorator部分

Decorator部分も実装していきましょう。

public abstract class Decorator extends Food{
    public abstract String getName();
}

デコレータの抽象クラスを実装したら次は具象クラスです。

public class Cheese extends Decorator{
    Food food;//装飾される物を保存するようのインスタンス変数
    public Cheese(Food food){//コンストラクタに装飾される物を渡します
        this.food = food;
    }
    public String getName(){
        return food.getName() +"+チーズ";
    }
    public int calc(){
        return food.calc()+50;
    }
}

コンストラクタには引数としてFoodクラスを受け取ります。
これは、装飾されるもの(ex.牛丼とか大きい牛丼)を引数とします。
その後、インスタンス変数に保存しておきます。
getName()ですが、先程インスタンス変数に保存した装飾されるもの(ex.牛丼とか大きい牛丼)の名前を取得して、更に飾り付けで「チーズ」を追加します。
calc()も同じく、「+50」というチーズの値段を飾り付けます。

後は他の装飾も同じように実装していきます。

public class Masterd extends Decorator{
    Food food;
    public Masterd(Food food){
        this.food = food;
    }
    public String getName(){
        return food.getName() + "+マスタード";
    }
    public int calc(){
        return food.calc() +30;
    }
}

public class Mayonnaise extends Decorator{
    Food food;
    public Mayonnaise(Food food){
        this.food = food;
    }
    public String getName(){
        return food.getName() + "+マヨネーズ";
    }
    public int calc(){
        return food.calc() + 60;
    }
}

実際に動かしてみる

public class Main {
    public static void main(String[] args){
        Food gyudon = new Gyudon();
        gyudon = new Cheese(gyudon);//牛丼をチーズで装飾
        gyudon = new Masterd(gyudon);//チーズ付き牛丼をマスタードで装飾
        System.out.println(gyudon.getName());
        System.out.println("¥"+ gyudon.calc());
    }
}

で 実行結果が

普通の牛丼+チーズ+マスタード
¥580

となります。
ソースコードの説明ですが、Gyudon()で新しいインスタンスを作成します。
そのインスタンスに対してCheeseクラスで装飾します。
更にそのインスタンスに対してMasterdクラスで装飾します。
つまり、これ書こうと思えば

Food gyudon = new Masterd(new Cheese(new Gyudon()));

ってな感じに書き換える事が出来ます。

どっかで見た形

Food gyudon = new Masterd(new Cheese(new Gyudon()));

なんかこんな形ってどこかで見たことありませんか?
これ、Javaの最初の方でやる文字入力の時とかによく出てくる形です。

file = "text.txt"
BufferedReader br = new BufferedReader(new FileReader(file));

これです、これ。
Javaの入出力関係のクラスって結構Decoratorパターンが使われています。

まとめ

このパターンは、既存のオブジェクトに新しい機能や振る舞いを動的に追加することを可能にする。

wikiからの引用です。
つまり、今回の場合はGyudon等といった既存のオブジェクトに対して色々なトッピングを追加できるということです。Eggというトッピングが追加されても、Gyudonクラスには何の変更も必要ありません。
反対にRamenといったフードが増えた所で、トッピング側には何の影響もありません。
このように新しい機能や振舞いを追加することが可能となります。