Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

※また、コメント欄にレビューをいただきました @htsign さんありがとうございます。
レビューを元に修正しました内容を反映させましたので、その点も踏まえて書いていこうと思います。

Prototypeとは

インスタンスを作成する際はクラスを元にするが、既存のインスタンスをコピーして別のインスタンスとして複製するパターン。

下記のような場合にメリットがあるとのこと。

  1. 種類が多すぎてクラスにまとめられない場合
  2. クラスからのインスタンス生成が難しい場合
  3. フレームワークと生成するインスタンスを分けたい場合

また、サンプルコードでは原型となるインスタンスを元に新しいインスタンスを作成するパターンを提供しています。

Productインターフェース

Cloneableを継承しているインターフェースでProductクラスを継承しているサブクラスのインスタンスがコピー対象のクラスとなります。

KotlinではCloneableがProductインターフェースで実装されている場合はjava.lang.NoClassDefFoundErrorになります。どうやらインターフェースでは実装できないらしく、Cloneableを継承するのはクラスでなければならないとの事です。
また、Cloneableを継承した抽象クラスのサブクラスの場合は実装できることを確認しました。

参考:
Kotlinでデザインパターン Prototype編
06设计模式-kotlin-复制Prototype

Product.java
interface Product extends Cloneable {
    public abstract void use(String s);
    public abstract Product createClone();
}
Product.kt
interface Product {
    fun use(s: String)
    fun createClone(): Product
}

Product抽象クラスの場合

Product.kt
abstract class Product(): Cloneable {
    abstract fun use(s: String)
    abstract fun createClone(): Product
}

Managerクラス

createCloneを使ってインスタンスを複製するクラスで、登録しておけば好きなタイミングで複製が可能なクラスになってます。

HashMapはshowcase.get()でもshowcase[]でも取得可能。
kotlinだとval a: String = nullではコンパイルエラーとなり、デフォルトでNullを許容していないので?指定をするとNullが扱えます。また、?指定したpがNullならcreateCloneは実行されずNullが返ります。

参考:
【Kotlin】【Java】Kotlin Javaの比較メモ
【Null安全】Kotlin Java比較メモ 正しいNull安全の使い方
『増補改訂版Java言語で学ぶデザインパターン入門』をKotlinでやってみる(Prototype編)

@htsign さんレビューをいただきありがとうございます。
元々Productのサブクラスでtry文のブロックの外にProduct型の変数pを宣言したくて
Nullを代入し、?指定にしたのですが、lateinitを使用して初期化を遅延する処理に修正し、Nullを扱わないように修正しました。
なのでProductインターフェースのcreateClone()も戻り値をProduct?からProductに戻してあります。

Manager.java
class Manager {
    private HashMap showcase = new HashMap();
    public void register(String name, Product proto) {
        showcase.put(name, proto);
    }
    public Product create(String protoname) {
        Product p = (Product)showcase.get(protoname);
        return p.createClone();
    }
}
Manager.kt
class Manager {
    private var showcase: MutableMap<String, Product> = mutableMapOf()
    fun register(name: String, proto: Product){
        showcase.put(name, proto)
    }
    fun create(protoname: String): Product?{
        val p = showcase[protoname] as Product
        return p?.createClone()
    }
}
修正Manager.kt
class Manager {
    private var showcase: MutableMap<String, Product> = mutableMapOf()
    fun register(name: String, proto: Product){
        showcase.put(name, proto)
    }
    fun create(protoname: String): Product{
        val p = showcase[protoname] as Product
        return p.createClone()
    }
}

MessageBoxクラス

文字列を枠線で囲って表示するクラスです。
先ほど言及しましたCloneableはProductのサブクラスにて実装する必要があります。
また、cloneメソッドは自分のクラス(およびサブクラス)からしか呼び出せないので、他のクラス(Manager)からの要請で複製を行う場合は、createClone()のような別メソッドでcloneをくるんで呼び出す必要があるとのことです。

MessageBox.java
class MessageBox implements Product {
    private char decochar;
    public MessageBox(char decochar) {
        this.decochar = decochar;
    }
    public void use(String s) {
        int length = s.getBytes().length;
        for (int i = 0; i < length + 4; i++) {
            System.out.print(decochar);
        }
        System.out.println("");
        System.out.println(String.format("%s %s %s", decochar, s, decochar));
        for (int i = 0; i < length + 4; i++) {
            System.out.print(decochar);
        }
        System.out.println("");
    }
    public Product createClone(){
        Product p = null;
        try {
            p = (Product)clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return p;
    }
}
MessageBox.kt
class MessageBox(private val d: Char): Product, Cloneable{
    override fun use(s: String){
        val length = s.toByteArray().size
        for (i: Int in 1..length + 4) print(d)
        println("")
        println("%s %s %s".format(d, s, d))
        for (i: Int in 1..length + 4) print(d)
        println("")
    }
    override fun createClone(): Product?{
        var p: Product? = null
        try {
            p = clone() as Product
        }catch (e:CloneNotSupportedException){
            e.printStackTrace()
        }
        return p
    }
}
修正MessageBox.kt
class MessageBox(private val d: Char): Product, Cloneable{
    override fun use(s: String){
        val length = s.toByteArray().size
        for (i: Int in 1..length + 4) print(d)
        println("")
        println("%s %s %s".format(d, s, d))
        for (i: Int in 1..length + 4) print(d)
        println("")
    }
    override fun createClone(): Product{
        lateinit var p: Product
        try {
            p = clone() as Product
        }catch (e:CloneNotSupportedException){
            e.printStackTrace()
        }
        return p
    }
}

UnderlinePenクラス

文字列に下線を引いて表示するクラスです。

UnderlinePen.java
class UnderlinePen implements Product {
    private char ulchar;
    public UnderlinePen(char ulchar) {
        this.ulchar = ulchar;
    }
    public void use(String s) {
        int length = s.getBytes().length;
        System.out.println(String.format("\"%s\"", s));
        System.out.print(" ");
        for (int i = 0; i < length; i++) {
            System.out.print(ulchar);
        }
        System.out.println("");
    }
    public Product createClone() {
        Product p = null;
        try {
            p = (Product)clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return p;
    }
}
UnderlinePen.kt
class UnderLinePen(ulchar: Char): Product, Cloneable{
    private val u = ulchar
    override fun use(s: String) {
        val length = s.toByteArray().size
        println("\"%s\"".format(s))
        print(" ")
        for (i: Int in 1..length) print(u)
        println("")
    }

    override fun createClone(): Product? {
        var p: Product? = null
        try {
            p = clone() as Product
        }catch (e: CloneNotSupportedException){
            e.printStackTrace()
        }
        return p
    }
}
修正UnderlinePen.kt
class UnderLinePen(ulchar: Char): Product, Cloneable{
    private val u = ulchar
    override fun use(s: String) {
        val length = s.toByteArray().size
        println("\"%s\"".format(s))
        print(" ")
        for (i: Int in 1..length) print(u)
        println("")
    }

    override fun createClone(): Product {
        lateinit var p: Product
        try {
            p = clone() as Product
        }catch (e: CloneNotSupportedException){
            e.printStackTrace()
        }
        return p
    }
}

Mainクラス

PrototypeSample.java
public class PrototypeSample {
    public static void main(String[] args) {
        Manager manager = new Manager();
        UnderlinePen upen = new UnderlinePen('~');
        MessageBox mbox = new MessageBox('*');
        MessageBox sbox = new MessageBox('/');
        manager.register("strong message", upen);
        manager.register("warning box", mbox);
        manager.register("slash box", sbox);

        Product p1 = manager.create("strong message");
        Product p2 = manager.create("warning box");
        Product p3 = manager.create("slash box");
        p1.use("Hello, World.");
        p2.use("Hello, World.");
        p3.use("Hello, World.");
    }
}
PrototypeSample.kt
fun main(args: Array<String>) {
    val manager = Manager()
    val upen = UnderLinePen('~')
    val mbox = MessageBox('*')
    val sbox = MessageBox('/')
    manager.register("strong message", upen)
    manager.register("warning box", mbox)
    manager.register("slash box", sbox)

    val p1 = manager.create("strong message")
    val p2 = manager.create("warning box")
    val p3 = manager.create("slash box")
    p1?.use("Hellow, World.")
    p2?.use("Hellow, World.")
    p3?.use("Hellow, World.")
}
修正PrototypeSample.kt
fun main(args: Array<String>) {
    val manager = Manager()
    val upen = UnderLinePen('~')
    val mbox = MessageBox('*')
    val sbox = MessageBox('/')
    manager.register("strong message", upen)
    manager.register("warning box", mbox)
    manager.register("slash box", sbox)

    val p1 = manager.create("strong message")
    val p2 = manager.create("warning box")
    val p3 = manager.create("slash box")
    p1.use("Hellow, World.")
    p2.use("Hellow, World.")
    p3.use("Hellow, World.")
}
実行結果
   "Hello, World."
    ~~~~~~~~~~~~~
   *****************
   * Hello, World. *
   *****************
   /////////////////
   / Hello, World. /
   /////////////////

クラス図

image.png

所感

  • cloneについてそもそもなぜ必要なのかを併せて調べてみたのですが(参考:Java の clone() メソッドについて)継承されたサブクラスなどでインスタンスを作成する際に意図していない動作をするので、よりそのままインスタンスをコピーするようにcloneメソッドは設計されたことを学んだ。
  • Null安全やHashMapの初期化、String#getBytestoByteArray()で代替することを学んだ
  • また、cloneableインターフェースがkotlinとjavaで仕様が違い、kotlinだとインターフェースに実装できないことを学んだ。
  • cloneはshallow copy(フィールド先の参照先の値ではなく参照値がコピーされる)なので、インスタンスのフィールドを書き換えた場合、意図せずclone元のフィールドまで書き換える恐れがあるのでdeep copy(フィールド先の参照先の値までコピー)する際はオーバーライドして処理を足す必要があることを学んだ。
  • また、Clonenableインターフェースはメソッドなどの宣言がされてず、あくまで印のようなインターフェースのことをmarker interfaceと呼ぶことを学んだ。

参考

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

JavaのString#getBytesはKotlinでどう書く?
Kotlinでデザインパターン Prototype編
06设计模式-kotlin-复制Prototype
【Kotlin】【Java】Kotlin Javaの比較メモ
【Null安全】Kotlin Java比較メモ 正しいNull安全の使い方
『増補改訂版Java言語で学ぶデザインパターン入門』をKotlinでやってみる(Prototype編)

また、clone()については下記ブログが詳細に説明してくださっていて大変勉強になりました。
Java の clone() メソッドについて

Takunawa
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away