daisukeの技術ブログ

AI、機械学習、最適化、Pythonなどについて、技術調査、技術書の理解した内容、ソフトウェア/ツール作成について書いていきます

Javaでデザインパターンを学ぶ:Prototypeパターン

Javaによるデザインパターンの9回目です!

今回も教材は、参考文献の「Java言語で学ぶデザインパターン入門第3版」のサンプルコードを使わせて頂きます。

今回は、Prototypeパターンです。

では、やっていきましょう!

参考文献

参考文献のサンプルプログラムのダウンロード

www.hyuki.com

はじめに

「Javaでデザインパターンを学ぶ」の記事一覧です。良かったら参考にしてください。

Javaでデザインパターンの記事一覧

先に、23種類のデザインパターンを示します。

参考サイト:デザインパターン (ソフトウェア) - Wikipedia

デザインパターン一覧
◆生成に関するパターン
 ・Abstract Factory:関連するインスタンスを状況に応じて、適切に生成する方法を提供する
 ・Builder:複合化されたインスタンスの生成過程を隠蔽する。
 ・Factory Method:生成されるインスタンスに依存しない、インスタンスの生成方法を提供する
 ・Prototype:同様のインスタンスを生成するために、原型のインスタンスを複製する
 ・Singleton:あるクラスについて、インスタンスが1つしか存在しないことを保証する
◆構造に関するパターン
 ・Adapter:元々関連性のない2つのクラスを接続するクラスを作る
 ・Bridge:クラスと呼び出し側の間の橋渡しをするクラスで、実装を隠蔽する
 ・Composite:再帰的な構造を表現する
 ・Decorator:あるインスタンスに対し、動的に付加機能を追加する
 ・Facade:複数のサブシステムの窓口となる共通のインタフェースを提供する
 ・Flyweight:多数のインスタンスを共有し、インスタンスの構築のための負荷を減らす
 ・Proxy:共通のインタフェースを持つインスタンスを内包し、利用者からのアクセスを代理する。Wrapperとも呼ばれる
◆振る舞いに関するパターン
 ・Chain of Responsibility:イベントの送受信を行う複数のオブジェクトを鎖状につなぎ、それらの間をイベントが渡されていくようにする
 ・Command:複数の異なる操作について、それぞれに対応するオブジェクトを用意し、オブジェクトを切り替えることで、操作の切り替えを実現する
 ・Interpreter:構文解析のために、文法規則を反映するクラス構造を作る
 ・Iterator:複数の要素を内包するオブジェクトのすべての要素に対して、順番にアクセスする方法を提供する
 ・Mediator:オブジェクト間の相互作用を仲介するオブジェクトを定義し、オブジェクト間の結合度を低くする
 ・Memento:データ構造に対する一連の操作のそれぞれを記録しておき、以前の状態の復帰または操作の再現が行えるようにする
 ・Observer:インスタンスの変化を他のインスタンスから監視できるようにする
 ・State:オブジェクトの状態を変化させることで、処理内容を変えられるようにする
 ・Strategy:データ構造に対して適用する一連のアルゴリズムをカプセル化し、アルゴリズムの切り替えを容易にする
 ・Template Method:アルゴリズムは抽象クラスで、処理内容はサブクラスで定義する
 ・Visitor:データ構造を保持するクラスと、それに対して処理を行うクラスを分離する

エンジニアグループのランキングに参加中です。

気楽にポチッとよろしくお願いいたします🙇

今回は、「Prototypeパターン」をやっていきます!

サンプルコードの理解

Prototypeパターンは、同様のインスタンスを生成するために、原型のインスタンスを複製する、とあります。一度作成したものをプロトタイプとして、もう一度同じものを作るよりも、プロトタイプをコピーする方が簡単だということを表現したパターンです。

それでは、早速サンプルコードを見ていくことにします。

今回のPlantUMLのクラス図

PlantUMLで作成したクラス図のコードと画像ファイルです。

Prototypeパターンを実現しているのは、Productインタフェースと、それを実装したUnderlinePenクラスとMessageBoxクラスです。

Productインタフェースの createCopy() は、自分自身(this)をコピーコンストラクタでコピーして返すメソッドで、これによって作成したプロトタイプをコピーするPrototypeパターンを実現しています。

Managerクラスは、Productインタフェースのインスタンスを登録、参照するための管理クラスです。

@startuml
package framework {
    interface Product {
        + {abstract} void use()
        + {abstract} Product createCopy()
    }
    
    class Manager {
        - Map<String, Product> showcase
        + void register()
        + Product create()
    }
    Manager -> Product : Uses >
}

class UnderlinePen {
    - char ulchar
    + UnderlinePen(char ulchar)
    + UnderlinePen(UnderlinePen prototype)
    + void use()
    + Product createCopy()
}
Product <|.. UnderlinePen

class MessageBox {
    - char decochar
    + MessageBox(char decochar)
    + MessageBox(MessageBox prototype)
    + void use()
    + Product createCopy()
}
Product <|.. MessageBox
@enduml

今回のクラス図
今回のクラス図

Mainクラス(使う側のクラス)

今回の実装のイメージが沸くように、先にMainクラスを見ていきます。また、実行結果も合わせて示します。

まず、準備として、Managerクラス(マネージャ)のインスタンスを生成しています。さらに、- を使ったUnderlinePenクラス(下線)、* を使ったMessageboxクラス(囲い枠)、/ を使ったMessageBoxクラス(囲い枠)のインスタンスを生成しています。

さらに登録として、マネージャに、作成した下線、アスタリスクの囲い枠、スラッシュの囲い枠を、それぞれ名前を付けてプロトタイプとして登録しています。

生成と使用として、マネージャに登録した下線を呼び出して(下線のプロトタイプをコピーして)、"Hello, world." を下線で装飾して出力します。同様に、マネージャに登録したアスタリスクの囲い枠をコピーして、"Hello, world." を装飾して出力し、スラッシュの囲い枠をコピーして、"Hello, world." を装飾して出力しています。

import framework.Manager;
import framework.Product;

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. という文字列に対して、- の下線と、* の囲い枠、/ の囲い枠で出力されていますね。

もう少し中身を見ていきます。

Productインタフェース

Productインタフェースは、frameworkパッケージで、2つの抽象メソッドを定義しています。

package framework;

public interface Product {
    public abstract void use(String s);
    public abstract Product createCopy();
}

UnderlinePenクラス

UnderlinePenクラスは、Productインタフェースを実装した下線を引く装飾のクラスです。

プライベートメンバ変数として、ulchar を持ちます。Mainクラスでは、- を登録していました。- を使った下線の装飾ということになります。他の文字を使った下線を引く装飾のインスタンスも作ることができます。

コンストラクタが2つあります(多重定義、オーバーロードと呼ばれます)。1つ目は普通のコンストラクタで、引数で与えられたchar型の文字でプライベートメンバ変数を初期化します。2つ目はコピーコンストラクタと呼ばれるコンストラクタで、自分自身と同じクラス型の引数を受け取って、全メンバ変数をコピーするコンストラクタです。これによって、Prototypeパターンを実現しています。

use() は、Productインタフェースで定義されていたメソッドで、引数で渡された文字列を対象として、下線で装飾するメソッドです。

createCopy() も、Productインタフェースで定義されていたメソッドで、コピーコンストラクタを呼び出して、自分自身のインスタンスを生成して返しています。

import framework.Product;

public class UnderlinePen implements Product {
    private char ulchar;

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

    // コピーコンストラクタ
    public UnderlinePen(UnderlinePen prototype) {
        this.ulchar = prototype.ulchar;
    }

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

    @Override
    public Product createCopy() {
        return new UnderlinePen(this);
    }
}

MessageBoxクラス

MessageBoxクラスは、UnderlinePenクラスと同様に、Productインタフェースを実装した囲い枠で装飾するクラスです。

プライベートメンバ変数として、decochar を持ちます。Mainクラスでは、* と、/ を登録していました。* と、/ を使った囲い枠の装飾ということになります。

UnderlinePenクラスと同様に、コンストラクタが2つあります。内容もUnderlinePenクラスと同じなので説明は省略します。

use()createCopy() も、UnderlinePenクラスとほとんど同じです。use() は、UnderlinePenクラスでは下線でしたが、MessageBoxクラスでは囲い枠なので、少し実装が増えていますね。

import framework.Product;

public class MessageBox implements Product {
    private char decochar;

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

    // コピーコンストラクタ
    public MessageBox(MessageBox prototype) {
        this.decochar = prototype.decochar;
    }

    @Override
    public void use(String s) {
        int decolen = 1 + s.length() + 1;
        for (int i = 0; i < decolen; i++) {
            System.out.print(decochar);
        }
        System.out.println();
        System.out.println(decochar + s + decochar);
        for (int i = 0; i < decolen; i++) {
            System.out.print(decochar);
        }
        System.out.println();
    }

    @Override
    public Product createCopy() {
        return new MessageBox(this);
    }
}

Managerクラス

Managerクラスは、Productインタフェースを実装したクラスの登録、複製を行うクラスです。

プライベートメンバ変数として、HashMapクラスの showcase を持ちます。HashMapクラスは、キーと値をセットにしたデータ構造です。今回は、名前をキーとして、Productインタフェースを実装したクラスのインスタンスを値として格納します。

register() は、引数で渡された名前とProductインタフェースを実装したクラスのインスタンスを、showcase に登録します。

create() は、引数で渡された名前を使って、showcase からProductインタフェースを実装したクラスのインスタンスを取り出し、Productクラスの createCopy() を呼び出して、インスタンスをコピーして返します。

package framework;

import java.util.HashMap;
import java.util.Map;

public class Manager {
    private Map<String,Product> showcase = new HashMap<>();

    public void register(String name, Product prototype) {
        showcase.put(name, prototype);
    }

    public Product create(String prototypeName) {
        Product p = showcase.get(prototypeName);
        return p.createCopy();
    }
}

Prototypeパターンのメリットと考察

Prototypeパターンは、インスタンスをコピーするところが特徴的です。

ペイントなどのソフトで、ユーザが絵を描いたとします。長い時間をかけて描いた絵がもう一つ必要になったとき、ユーザの操作をもう一度行うよりも、絵をコピーしますよね(当たり前ですね)。このコピーの部分がPrototypeパターンにあたります。

ペイントだとコピーするのが当たり前でしたが、ソフトウェアの処理の場合では、意外とコピーはされず、もう一度同じ処理を実行させている場合が結構あります。ペイントで絵を描くのは大変ですが、ソフトウエアの場合は、そのメソッドをもう1回呼ぶだけで実現できるからです。

メソッドをもう一度呼ぶ方が、そのメソッドの結果を複製するより簡単な場合が多いからです。しかし、結果を複製する方がソフトの処理時間は圧倒的に早くできる場合が多いです。

少し個人的な見解が入ってしまっていますが、Prototypeパターンは、複製するという選択肢を想起させてくれるパターンだと思います。

今回は以上です!

おわりに

今回はPrototypeパターンを学びました。

最後までお読みいただき、ありがとうございました。