Prototype パターン [Java]


GoFデザインパターンについて学んでます.

増補改訂版Java言語で学ぶデザインパターン入門

増補改訂版Java言語で学ぶデザインパターン入門

  • 作者:結城 浩
  • 発売日: 2004/06/19
  • メディア: 大型本

今回は「Prototype」パターンについて学びます.

Prototype パターン

クラスからインスタンスを生成せず(newせず)に,インスタンスをコピー(複製)して新しいインスンタンスを作るパターンです.

どういう場合に当てはまるか?

  • 種類が多すぎてクラスにまとめられない場合

扱うオブジェクトの種類が多すぎて,1つ1つを別のクラスにしていたら,
ソースファイルを多数作成する必要が生じる場合.

生成させたいインスタンスが複雑な過程を経て作られるものであり,
クラスから作り上げることがとても難しい場合.

インスタンスを生成するときのフレームワークを特定のクラスに依存しないように作りたい場合.
この場合,クラス名を指定してインスタンスを作るのではなく,
前もって「ひな型」となるインスタンスを登録し,
その登録したインスタンスをコピーすることでインスタンスを生成する.


Javaでは複製を作る操作をcloneと呼ぶ.git cloneとかで馴染みがあると思う.

サンプルプログラム

文字列を枠線で囲って表示したり,下線を引いて表示したりするサンプルプログラムになります.

UML

f:id:xrdnk:20200328235927p:plain

ProductインタフェースとManagerクラスはframeworkパッケージに属し,
インスタンスを複製する仕事を行います.
ManagerクラスはcreateClone()を呼び出しますが,具体的にどのクラスのインスタンス
複製するかはわからない.Productインタフェースを実装しているクラスならば,
そのインスタンスを複製することができます.

ProductがPrototype役,MessageBox,UnderlinePenクラスがConcreteなPrototype役になります.

Product インタフェース

package Prototype.framework;

// 複製可能とするためにはCloneable
public interface Product extends Cloneable {
    public abstract void use(String s);

    public abstract Product createClone();
}

java.lang.Cloneableインタフェースを継承しています.
これにより,clone()を使って自動的に複製を行うことができます.

Manager クラス

createCloneを使ってインスタンスを複製するクラスです.

package Prototype.framework;

import java.util.HashMap;

public class Manager {
    // インスタンスに対応するキーを格納するKeyValueを定義
    private HashMap<String, Product> showCase = new HashMap<>();
    // 
    public void register(String name, Product photo) {
        showCase.put(name, photo);
    }

    public Product create(String photoName) {
        Product p = showCase.get(photoName);
        return p.createClone();
    }
}

ソース中にクラスの名前を書くとそのクラスと密接な関係ができます.(密結合)
このクラスでは,具体的な個々のクラス名を使わずにProductインタフェースを使っています.(疎結合

MessageBox クラス

文字列を飾り枠のように囲むように出来るクラスです.

package Prototype;

import Prototype.framework.*;

public class MessageBox implements Product {

    private char decochar;

    public MessageBox(char decochar) {
        this.decochar = decochar;
    }

    @Override
    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(decochar + " " + s + " " + decochar);
        for (int i = 0; i < length + 4; i++) {
            System.out.print(decochar);
        }
        System.out.println("");
    }

    @Override
    public Product createClone() {
        Product p = null;
        try{
            p = (Product) clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return p;
    }
}

UnderlinePen クラス

文字列を下線で引くようにできるクラスです.

package Prototype;

import Prototype.framework.*;

public class UnderlinePen implements Product {

    private char ulchar;

    public UnderlinePen(char ulchar) {
        this.ulchar = ulchar;
    }

    @Override
    public void use(String s) {
        int length = s.getBytes().length;
        System.out.println("\"" + s + "\"");
        System.out.println("");
        for (int i = 0; i < length; i++) {
            System.out.print(ulchar);
        }
        System.out.println("");
    }

    @Override
    public Product createClone() {
        Product p = null;
        try {
            p = (Product) clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return p;
    }
}

Mainクラスと結果

package Prototype;

import Prototype.framework.*;

public class Main {
    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");
        p1.use("Hello, world");
        Product p2 = manager.create("warning box");
        p2.use("Hello, world");
        Product p3 = manager.create("slash box");
        p3.use("Hello, world");
    }
}

結果

"Hello, world"

~~~~~~~~~~~~
****************
* Hello, world *
****************
////////////////
/ Hello, world /
////////////////

反芻タイム

クラス名は束縛?

OOPの目的の一つが「部品としての再利用」であることを思い出す必要がある.

ソース中に利用するクラスの名前を書いておくことが,常に悪いことではない.
しかし,ソースの中に利用するクラスの名前が書かれていると,そのクラスと
切り離して再利用することができなくなってしまう.
結合度については考慮する必要がある.

Javaのclone

Javaではインスタンスのコピーを行う機構として,clone()が用意されている.
clone()を実行する場合,コピー対象となるクラスはjava.lang.Cloneableインタフェースを
実装する必要がある.
もし実装していないクラスのインスタンスがclone()を呼び出すと,例外が発生する.

実際clone()はjava.lang.Objectクラスで定義されていて,Cloneableインタフェースではない.
そして,Cloneableインタフェースにはメソッドが一つも宣言されていない.
単に,「cloneによってコピーすることができる」印として使われている.
このような印をつけるインタフェースのことをマーカーインタフェースという.

shallow copy

clone()によって行われるのは,フィールドの内容をそのままコピーするという動作.
つまり,フィールドの先にあるインスタンスの中身まで考慮してない.

例として,フィールドの先に配列があったとして,clone()を使ってコピーした場合,
その配列への参照がコピーされるだけであって,配列の要素1つ1つコピーされるわけではない.
このようなfield-for-fieldコピーのことをshallow copyという.

kurochan-note.hatenablog.jp