mohuneko’s blog

かんばる駆け出しエンジニアのブログです

【Java】Decoratorパターン【デザインパターン】

駆け出しエンジニアがデザインパターンをもくもく勉強します

 こんな本で勉強しています🌟

目次

Decoratorパターンについて

Decoratorパターンとは

  • Decoratorパターンは、オブジェクトに対してデコレーション (飾り付け) を行うパターンです
  • スポンジに苺、チョコ、生クリーム。。とデコレーションするように、オブジェクトも機能を一つ一つ被せてデコレーションしていくイメージです

サンプルプログラム

  • Decoratorパターンを使って、文字列の周りに飾り枠をつけて表示する例を取り上げます
  • 各クラスの役割は以下のようになっています
名前 役割
Display 文字列表示用の抽象クラス
StringDisplay 1行だけの文字列表示用のクラス
Border 飾り枠を表す抽象クラス
SideBorder 左右に飾り枠をつけるクラス
FullBorder 上下左右に飾り枠をつけるクラス

f:id:mohuNeko:20201229222052p:plain

Displayクラス

  • 複数行の文字列を表示するための抽象クラスです
  • showメソッドでは、getRowsメソッドで行数を取得し、getRowTextメソッドで表示する文字列を取得し、forループを使って全行を表示します
  • showはgetRows、getRowTextメソッドの抽象メソッドを使ったTemplate Methodパターンになっています
public abstract class Display {
    public abstract int getColumns();               // 横の文字数を得る
    public abstract int getRows();                  // 縦の行数を得る
    public abstract String getRowText(int row);     // row番目の文字列を得る
    public void show() {                            // 全部表示する
        for (int i = 0; i < getRows(); i++) {
            System.out.println(getRowText(i));
        }
    }
}

StringDisplayクラス

  • StringDisplayクラスは、一行の文字列を表示するクラスです
  • StringDisplayクラスはDisplayクラスのサブクラスなので、Displayクラスで宣言している抽象メソッドを実装します
  • stringフィールドに表示する文字列を保持します
public class StringDisplay extends Display {
    private String string;      // 表示文字列
    public StringDisplay(String string) {      // 引数で表示文字列を指定
        this.string = string;
    }
    public int getColumns() {    // 文字数
        return string.getBytes().length;
    }
    public int getRows() {   // 行数は1
        return 1;
    }
    public String getRowText(int row) {   // rowが0のときのみstringを返す
        if (row == 0) {
            return string;
        } else {
            return null;
        }
    }
}

Borderクラス

  • 飾り枠を表すクラスです
  • Displayクラスを継承しているので、飾り枠は中身と同じメソッドを持つことになります
    • インターフェース的には、飾り枠と中身を同一視できることになります
  • "中身"はSrtingDisplayクラスなのか、Borderクラスのサブクラスのインスタンス(飾り枠)かは分かりません
    • Borderクラスのサブクラスのインスタンスも、さらにdisplayフィールドを持ちます
public abstract class Border extends Display {
    protected Display display;    // この飾り枠がくるんでいる"中身"を指す
    protected Border(Display display) { // インスタンス生成時に"中身"を引数で指定
        this.display = display;
    }
}

SideBorderクラス

  • Borderクラスのサブクラスの一つで、具体的な飾り枠です
  • SideBorderクラスはスーパークラスで宣言したメソッドを全て実装しているので、抽象クラスではありません
public class SideBorder extends Border {

    private char borderChar;    // 飾りとなる文字

    public SideBorder(Display display, char ch) {   // コンストラクタでDisplayと飾り文字を指定
        super(display);
        this.borderChar = ch;
    }
    public int getColumns() {  // 中身の両側+飾り文字分を加えた
        return 1 + display.getColumns() + 1;
    }
    public int getRows() {     // 行数は中身と同じ
        return display.getRows();
    }
    public String getRowText(int row) {  // 中身の指定行の両側+飾り文字
        return borderChar + display.getRowText(row) + borderChar;
    }
}

FullBorderクラス

  • Borderクラスのサブクラスの一つで、具体的な飾り枠です
public class FullBorder extends Border {
    public FullBorder(Display display) {
        super(display);
    }
    public int getColumns() {  // 中身の両側+左右の飾り文字分を加えた
        return 1 + display.getColumns() + 1;
    }
    public int getRows() {    //中身の行数+上下の飾り文字分を加えた
        return 1 + display.getRows() + 1;
    }
    public String getRowText(int row) {   // 指定した行の内容
        if (row == 0) {    // 上端の枠
            return "+" + makeLine('-', display.getColumns()) + "+";
        } else if (row == display.getRows() + 1) {     // 下端の枠
            return "+" + makeLine('-', display.getColumns()) + "+";
        } else {      // それ以外
            return "|" + display.getRowText(row - 1) + "|";
        }
    }
    private String makeLine(char ch, int count) {   // 文字chをcount個連続させた文字列を作る
        StringBuffer buf = new StringBuffer();
        for (int i = 0; i < count; i++) {
            buf.append(ch);
        }
        return buf.toString();
    }
}

Mainクラスで動作確認

public class Main {
    public static void main(String[] args) {
        Display b1 = new StringDisplay("Hello, world."); //飾りなし
        Display b2 = new SideBorder(b1, '#'); //#の文字で左右に飾り付け
        Display b3 = new FullBorder(b2); //全体の飾り枠をつけた
        b1.show();
        b2.show();
        b3.show();
        Display b4 =  //何重にも重ねて枠をつける
                    new SideBorder(
                        new FullBorder(
                            new FullBorder(
                                new SideBorder(
                                    new FullBorder(
                                        new StringDisplay("こんにちは。")
                                    ),
                                    '*'
                                )
                            )
                        ),
                        '/'
                    );
        b4.show();
    }
}

Decoratorパターンのメリット

Decoratorパターンでは、飾り枠(Border)も中身(StringDisplay)も共通のインターフェースを持っています。 インターフェースは共通ですが、包めば包むほど機能が追加されていきます。 その際に、包まれる方を修正する必要はありません。 包まれるものを変更することなく、機能の追加を行うことができます。

今日のポイント

  • Decoratorパターンは、オブジェクトに対してデコレーション (飾り付け) を行うパターンです

 本日もお疲れ様です😊