デザインパターンをkotlinで書いてみた Template編


オブジェクト指向で大切になるInterfaceの考え方やオブジェクトの再利用性を学ぶために「Java言語で学ぶデザインパターン入門」について学び、Javaとついでにkotlinで書いてみることにしました。
今回はTemplateについてまとめます。

Templateとは

スーパークラスで処理の枠組み(似たような共有の処理)を定め、サブクラスで具体的な内容を定めるようなデザインパターン。
下記は「文字や文字列を5回繰り返して表示する」という共通の処理を枠組みとしてサブクラスにそれぞれ具体的な処理として実装していきます。

AbstractDisplayクラス

型となる4つの抽象メソッドが定義されていて、open,print,closeの具体的な処理はサブクラスに実装させるようになっている。

AbstractDisplay.java
abstract class AbstractDisplay {
    public abstract void open();
    public abstract void print();
    public abstract void close();
    public final void display() {
        open();
        for(int i = 0; i < 5; i++) {
            print();
        }
        close();
    }
}
AbstractDisplay.kt
abstract class AbstractDisplay {
    abstract fun open(): Unit
    abstract fun print(): Unit
    abstract fun close(): Unit
    fun display(): Unit {
        open()
        for (i: Int in 1..5) print()
        close()
    }
}

CharDisplayクラス

テンプレートとなるAbstractDisplayを継承したサブクラスで、このクラスではコンストラクタで渡された一文字を整形して表示するような具体的な処理を実装していく。
Kotlinでインターフェースを継承する場合は()はいらなかったが、抽象クラスだと継承の際はAbstractDisplay() のように()をつける必要があり、これはインターフェースはコンストラクタがないが、抽象クラスはコンストラクタの実装が可能なのでデフォルトでつける。

CharDisplay.java
class CharDisplay extends AbstractDisplay {
    private char ch;
    public CharDisplay(char ch) {
        this.ch = ch;
    }
    @Override
    public void open() {
        System.out.print("<<");
    }
    @Override
    public void print() {
        System.out.print(ch);
    }
    @Override
    public void close() {
        System.out.println(">>");
    }
}
CharDisplay.kt
class CharDisplay(private val ch: Char): AbstractDisplay() {
    override fun open() = print("<<")
    override fun print() = print(ch)
    override fun close() = println(">>")
}

StringDisplayクラス

テンプレートとなるAbstractDisplayを継承したサブクラスで、このクラスではコンストラクタで渡された文字列を整形して表示するような具体的な処理を実装していく。
Kotlinでfor文で任意の数字を指定して回す場合はfor i..widthと指定すればいい。

StringDisplay.java
class StringDisplay extends AbstractDisplay {
    private String str;
    private int width;
    public StringDisplay(String str) {
        this.str = str;
        this.width = str.getBytes().length;
    }
    @Override
    public void open() {
        printLine();
    }
    @Override
    public void print() {
        System.out.println("|" + str + "|");
    }
    @Override
    public void close() {
        printLine();
    }
    private void printLine() {
        System.out.print("+");
        for(int i = 0; i < width; i++) {
            System.out.print("-");
        }
        System.out.println("+");
    }
}
StringDisplay.kt
class StringDisplay(private val str: String, private  val width: Int = str.length): AbstractDisplay() {
    override fun open()= printLine()
    override fun print() = println("|" + this.str + "|")
    override fun close() = printLine()
    private fun printLine() {
        print("+")
        for (i: Int in 1..width) print("-")
        println("+")
    }
}

Mainクラス

AbstractDisplayを継承したサブクラスなのでdisplayメソッドを呼び出すことができ、出力は実装した各々のサブクラスで定義した内容で出力される。

TemplateSample.java
public class TemplateSample {
    public static void main(String[] args) {
        AbstractDisplay d1 = new CharDisplay('H');
        AbstractDisplay d2 = new StringDisplay("Hello, World");
        AbstractDisplay d3 = new StringDisplay("Hello World!!");

        d1.display();
        d2.display();
        d3.display();
    }
}
Template.kt
fun main(args: Array<String>){
    val d1: AbstractDisplay = CharDisplay('H')
    val d2: AbstractDisplay = StringDisplay("Hello, World")
    val d3: AbstractDisplay = StringDisplay("Hello World!!")

    d1.display()
    d2.display()
    d3.display()
}
<<HHHHH>>
+------------+
|Hello, World|
|Hello, World|
|Hello, World|
|Hello, World|
|Hello, World|
+------------+
+-------------+
|Hello World!!|
|Hello World!!|
|Hello World!!|
|Hello World!!|
|Hello World!!|
+-------------+

クラス図

所感

  • ロジックを共通化できるメリットが二つあり、抽象クラスのテンプレートメソッドでアルゴリズムが定義されているので、サブクラスで記述をしなくてよくなる点と、もしテンプレートメソッドにバグがある場合はテンプレートメソッドのみの修正で済むので影響範囲が限定される点を学んだ。
  • 例えばdisplayテンプレートメソッド(executeメソッド)を個々のクラスでコピペで記述した場合だとすべての個々のクラスで変更が必要になると例では挙げられていた。
  • また、スーパークラス型の変数にサブクラスのインスタンスのどれを代入しても正しく動作するという原則をThe Liskov Substitution Principle(LSP)と呼ばれていることを学んだ。
  • Kotlinでfor文を記述する場合、for i..widthと指定したり、抽象クラスを継承する場合は()をつけ、Interfaceを継承する場合はつけないことを学んだ。

参考

下記を参考にさせて頂き、大変読みやすく、理解しやすかったです。

JavaプログラマのためのKotlin入門
逆引きKotlin