🐈

デコレータパターン解説してみたよ

2022/01/18に公開約4,100字

デコレータパターンが最近になってようやく自分の中で言語化できるようになってきたのでまとめてみた。

はじめにwikiでの説明

Decorator パターン(デコレータ・パターン)とは、GoF(Gang of Four; 4人のギャングたち)によって定>>義されたデザインパターンの1つである。 このパターンは、既存のオブジェクトに新しい機能や振る舞いを動的に追加することを可能にする。

抽象的すぎる。
なので、実際にコードを改修しながらデコレータパターンとは何か?どういったメリットがあるのかをまとめていきたい。

仕様

引数で与えられた文字列を出力するプログラムを書こうと思う。
ただしこの文字列には行番号やタイムスタンプ、語尾にクエスチョンマークをつけることもある。

最初のコード

zenn.java
import java.sql.Timestamp;

class Main{
    public static void main(String[] args){
        StringPrinter stringPrinter = new StringPrinter();
        System.out.println(stringPrinter.print("何でもない一行"));
        System.out.println(stringPrinter.printWithTimestamp("タイムスタンプ付きの一行"));
        System.out.println(stringPrinter.printWithNumbering("行番号付きの一行"));
        System.out.println(stringPrinter.printWithQuestionMark("クエスチョンマーク付きの一行"));
    }
}

class StringPrinter{
    int lineNumber;
    String print(String str){
        return str;
    }
    
    String printWithTimestamp(String str){
        Timestamp timestamp = new Timestamp(System.currentTimeMillis());
        return timestamp + ": " + str;
    }

    String printWithNumbering(String str){
        return lineNumber++ + ": " + str;
    }

    String printWithQuestionMark(String str){
        return str + "?";
    }
}

問題点

複数の加工(例えばナンバリングとタイムスタンプを掛け合わせるなど)を施したいときにいちいちメソッドを追加しないといけなかったり、組み合わせが無数にあるので設計時に全てを予測するのが不可能と言った点がある。

改善方法

ここでデコレータパターンを使う。
まずはコードを見て頂きたい。

zenn.java
import java.sql.Timestamp;

class Main{
    public static void main(String[] args){
        Printer printer = new SimplePrinter();
        System.out.println(printer.print("何でもない一行"));

        Printer printer2 = new QuestionMarkPrinter(new NumberingPrinter(new TimestampPrinter(new SimplePrinter())));
        System.out.println(printer2.print("タイムスタンプ、行番号、クエスチョンマーク付きの一行"));

        Printer printer3 = new QuestionMarkPrinter(new TimestampPrinter(new SimplePrinter()));
        System.out.println(printer3.print("タイムスタンプ、クエスチョンマーク付きの一行"));

        Printer printer4 = new TimestampPrinter(new QuestionMarkPrinter());
        System.out.println(printer3.print("行番号、タイムスタンプ付きの一行"));
    }
}

interface Printer{
    public String print(String str);
}

class SimplePrinter implements Printer{
    Printer printer;

    SimplePrinter(){}
    SimplePrinter(Printer printer){
        this.printer = printer;
    }

    public String print(String str){
        return str;
    }
}

class TimestampPrinter implements Printer{
    Printer printer;

    TimestampPrinter(){}
    TimestampPrinter(Printer printer){
        this.printer = printer;
    }

    public String print(String str){
        Timestamp timestamp = new Timestamp(System.currentTimeMillis());
        return String.format("%s: %s", timestamp, printer.print(str)); 
    }
}

class NumberingPrinter implements Printer{
    Printer printer;
    int lineNumber;

    NumberingPrinter(){}
    NumberingPrinter(Printer printer){
        this.printer = printer;
    }

    public String print(String str){
        return String.format("%s: %s", lineNumber++, printer.print(str)); 
    }
}

class QuestionMarkPrinter implements Printer{
    Printer printer;

    QuestionMarkPrinter(){}
    QuestionMarkPrinter(Printer printer){
        this.printer = printer;
    }

    public String print(String str){
        return String.format("%s??", printer.print(str)); 
    }
}

全てのクラスはPrinterインターフェースを実装し、メンバ変数として他のクラスのインスタンスを持つことが出来る。
実際に使う時はクライアント側で初期化の際に使いたいオプションを組み合わせれば良い。

最初の一つのクラスに全てを含めている場合と比べて以下の点が良いように思う。

  • 責務がはっきりしている。
    • 最初のクラスは様々な文字を出すことを責務としていて少しばかり抽象的だった。
    • しかしデコレータパターンを使っている方はタイムスタンプはこれ、行番号はこっちと責務が明確になった。
  • 無数の組み合わせに対応できる。
    • 使用するPrinterを変えるだけで幾多の組み合わせにも対応できる。
  • 仕様が追加された時にも強い。
    • 新たに与えられた文字列を2回出したいなどといった追加要望にもそれに特化したPrinterクラスを作れば良いだけなので既存のクラスを修正する必要がなくなった。

参考にさせてもらった書籍

こちらの書籍に載っていることをjavaに書き換え説明した。
デザインパターンについてだいぶ理解できるようになってきたが、多くのパターンで共通しているのはとにかく委譲を意識して使えと言うことではないだろうか。
今回のデコレータパターンにしてもそうだ。
メンバ変数としてprinterを持っているがprint()メソッドが内部で何をしてるかは知ろうとしていない。
とにかくString型の結果をよこせ。と言っているだけだ。
こうすることで他クラスの余計な責務を背負うことなく自分の仕事に集中することが出来るのだと思う。

Discussion

ログインするとコメントできます