PHPでPrototype(プロトタイプ)を学ぶ【デザインパターン】

PHP応用 デザインパターン PHP
※当サイトはアフィリエイト広告を掲載しています。

デザインパターンは、コードの保守や拡張性を高めるための定番として、全23の手法があります。

その中の一つがPrototype Pattern(プロトタイプパターン)です。※以後、PrototypeまたはPrototypeパターンと表記します。

概要は「既存のオブジェクトをコピーして新しいオブジェクトを生成する」ことです。

本ページではPHPを利用して、Prototypeの基本的な概念~実装方法についてわかりやすく解説します。

Lara
Lara

私もデザインパターンは一つずつ学びながらまとめています。間違いなどがあればご指摘ください!

Prototypeパターンとは?

概要

最初にも触れたとおり、既存のオブジェクトをコピーして新しいオブジェクトを生成すること。それを戦略的に行うことで、効率良くプログラミングするパターンです。

「Prototype」という言葉は、英語で「原型」を意味します。原型から、新しいインスタンスを作り出し上手く使うパターン……というわけです。

このパターンは、以下の様な場合に有効です。

  • newによるオブジェクトの作成コストが高い場合
  • システム内で多数のインスタンスが共通の状態を共有する場合
  • オブジェクトの種類に応じて動的に新しいオブジェクトを生成する必要がある場合
Lara
Lara

これから具体的に解説していきますので、何となくの理解で大丈夫です。

PHPでのPrototypeパターンの基本

Prototypeパターンを理解するためのコード例

PHPでは、cloneキーワードを使用してオブジェクトを複製することができます。以下は、Prototypeパターンを理解するための簡素なコード例です。

このコード自体はPrototypeとは言えず、あくまで理解するための最初のステップとしてお考えください。

<?php
class Human {
    public $name = "名無し";

    public function __clone() {
        // 必要に応じてディープコピーの処理を追加
    }
}

$original = new Human();

$man = clone $original;
$man->name = "タロウ";

echo $original->name;
// 出力: 名無し

echo $man->name;
// 出力: タロウ

この例では、Humanクラスのインスタンスを作成し、そのインスタンスをcloneキーワードを使って複製しています。

当然$man$originalを複製しているので、$originalの値を複製しつつ$manを利用することが可能です。

特別目立った部分はないので、「ただクローンしただけじゃん」と思われるかもしれません。実はその通り。

先ほども書いた通り、Prototypeパターンとはとても言えないシロモノです。

あくまで、これから学ぶための基礎としてのコードと捉えてください。

実際には、事前にきちんと初期値一式を用意したオブジェクトを用意し、そこからクローンして活用していきます。

Lara
Lara

これから実例を挙げていきますので、まだ何となくの理解でもサクっと進んでください。

cloneキーワードの役割

PHPでのオブジェクトのコピーはcloneキーワードを使うので、説明において避けて通れません。

cloneは、既存のオブジェクトをシャローコピーするためのものです。

シャローコピーとは、オブジェクトの最上位のコピーだけを作成することを意味します。

ここでは詳細は置いておきますが、Prototypeをきちんと理解するにはcloneの知識を身につけておく必要があります。理解を深めたい方は、以下の記事もご覧ください。

Prototypeの簡単な活用例から、理解を深める

オブジェクトをクローン・コピーしただけで、メリットがありそうになり

正直、私も最初はメリットになりそうなイメージがわきませんでした。そんな時は、より実用的なコードを見ながら理解するのが一番です。

いきなり難しいと混乱するので、短い例からいきます。なのでまだまだPrototypeを名乗るには簡素すぎますが、エッセンスを学ぶためには役立つと思います。

設定の読み込みの例

アプリケーションの設定を扱う際に、特定のユーザーやシチュエーションに基づいて設定をカスタマイズしたい場合があります。

この時、デフォルトの設定オブジェクトをPrototypeとして使い、それを基にカスタマイズされた設定オブジェクトを複製してみましょう。

<?php
class AppConfig {
    public $displayMode = "light";
    public $language = "en";
    public $externalSettings = [];

    public function __construct() {
        // 仮定:この処理が非常に時間がかかるとします。
        $this->externalSettings = $this->loadHeavyExternalFile();
    }

    private function loadHeavyExternalFile() {
        // 5秒待機して「重い処理」をシミュレートします。
        sleep(5); 
        return [
            'setting1' => 'value1',
            'setting2' => 'value2',
            // ... その他の設定項目
        ];
    }

    public function __clone() {
        // 必要に応じてクローン時の処理を追加
    }
}

$defaultConfig = new AppConfig();

$userConfig = clone $defaultConfig;
$userConfig->displayMode = "dark";

まずは$defaultConfigを取得し、その上で$userConfigとして上書きしていくことで、ユーザー設定が行えます。

また、ここではAppConfignewするのに時間がかかる場合を想定しているので、できるだけnewしたくありません。

Prototypeパターンを利用することで、もしユーザーが増えた場合でも逐一newせず、複製して使い回すことができます。

Lara
Lara

ようやくメリットとして見えてきましたね。

ゲームのキャラクター複製の例

PHPでゲーム開発はしないと思いますが、Prototypeパターンの説明例としては理解しやすいかもしれません。

例えば、RPGゲームで同じ系統のモンスターを複数出現させたい場合。モンスターのオリジナルのインスタンスをPrototypeとして使い、新しいモンスターを効率的に複製することができそうです。

<?php
// メタルスライムのクラス
class MetalSlime {
    public $hp;
    public $attackPower;
    public $defensePower = 255;
    public $speed = 255;

    public function __construct($hp, $attackPower) {
        $this->hp = $hp;
        $this->attackPower = $attackPower;
    }
    
    public function __clone() {
        // ここで特別な複製処理が必要なら追加
    }
}

// メタルスライム(弱い&早い&固い)
$metalSlime = new MetalSlime(10, 20);
$hagureMetal = clone $metalSlime;
$metalKing = clone $metalSlime;

// はぐれメタル(メタルスライムより気持ち強い&早い&固い)
$hagureMetal->hp = 15;

// メタルキング(強い&早い&固い)
$metalKing->hp = 30;
$metalKing->attackPower = 100;


echo $hagureMetal->hp; // 出力: 15
echo $hagureMetal->attackPower; // 出力: 20
echo $hagureMetal->defensePower; // 出力: 255
echo $hagureMetal->speed; // 出力: 255

echo $metalKing->hp; // 出力: 30
echo $metalKing->attackPower; // 出力: 100
echo $metalKing->defensePower; // 出力: 255
echo $metalKing->speed; // 出力: 255

この方法で、同じ属性を持つモンスターを迅速に複製することができます。

上記例では、メタルスライムは素早さ: $speed$defensePowerは共通(つまり素早く守備力が高い)で、他の要素で差別化しています。

さらにいろいろな要素があれば、よりPrototypeが活きてくるシーンも想像できますね。

文書生成アプリケーションの例

もう一歩ステップアップした例として、企業向けの文書生成アプリケーションを考えてみます。

以下のような条件としましょう。

  • ユーザーは、複数タイプの文書(例:レポート、メモ、契約書など)を作成できる
  • アプリケーションは、それぞれの文書タイプに対して基本的なテンプレート(Prototype)を持っている
  • ユーザーが新しい文書を作成するとき、選択した文書のPrototypeを基に新しい文書を生成する

少し長くなりますが、以下がコードです。

<?php

interface Document {
    public function clone(): Document;
    public function setContent($content);
    public function display();
}

class Report implements Document {
    private $content = "Default Report Content";

    public function clone(): Document {
        return clone $this;
    }

    public function setContent($content) {
        $this->content = $content;
    }

    public function display() {
        echo "Report: {$this->content}\n";
    }
}

class Memo implements Document {
    private $content = "Default Memo Content";

    public function clone(): Document {
        return clone $this;
    }

    public function setContent($content) {
        $this->content = $content;
    }

    public function display() {
        echo "Memo: {$this->content}\n";
    }
}

class DocumentFactory {
    private $prototypes = [];

    public function __construct() {
        // デフォルトのプロトタイプを登録
        $this->prototypes['report'] = new Report();
        $this->prototypes['memo'] = new Memo();
    }

    public function createDocument($type) {
        if (!isset($this->prototypes[$type])) {
            throw new Exception("Document type not found.");
        }
        return $this->prototypes[$type]->clone();
    }
}

$factory = new DocumentFactory();

$report = $factory->createDocument('report');
$report->setContent("2023年年間売上報告書");
$report->display();  // Report: 2023年年間売上報告書

$memo = $factory->createDocument('memo');
$memo->setContent("月曜日のチームミーティング");
$memo->display();  // Memo: 月曜日のチームミーティング

この例では、DocumentFactoryは異なるタイプの文書のPrototypeを管理しています。

ユーザーが新しい文書を作成する際に、文書タイプに該当するPrototypeが複製され、新しい文書オブジェクトが生成されます。

まさにPrototype=原型(ひな形)から、文書を新規作成している例となりますね。私はこのコードで、妙に納得した感がありました。

Prototypeの疑問

クラスで良いじゃん!?と思った方へ

コード例を見て、「Prototypeを使わずにクラスで解決することもあるのでは!?」と思う方もいるかもしれません。

確かに、例えばゲームキャラクターを単純に複製するだけであれば、クラスや継承を利用することで、同様の結果を得ることができます。確かにそれらがよい時もあるでしょう。

なんにでも言えることですが、使い所は状況次第です。

以下にPrototypeパターンが役立つかもしれない状況を挙げますので、参考にしてください。

  • 初期化コストの節約をしたい場合
    一度初期化されたオブジェクトを複製することで、初期化のコストを節約できることがあります。
  • 状態を保持したい場合
    ある特定の状態で初期化されたオブジェクトを複数作りたいとき、その状態を保持したオブジェクトをPrototypeとすることで、効率的に作成できることがあります。
  • オブジェクトの種類が多い場合
    異なる属性や振る舞いをさせたい時、それらを都度クラスのインスタンスとして新しく生成するよりも、既存のPrototypeを基にして複製する方が効率的なことがあります。
  • 動的な変更が必要な場合
    動的に属性の変更が頻繁に行われる場合、Prototypeを変更して新しいオブジェクトを複製するほうが効率的なことがあります。
Lara
Lara

システムは俯瞰して考え、その時々で最適な実装を考えることになります。

Prototype=クローン!?

注意して書いたつもりですが、ただcloneしただけでPrototypeと思ってしまわれた方がいたら私の説明不足かもしれません。

もちろん、違います。

cloneは単なるコピーする手段です。そのcloneを戦略的にうまく使用した設計思想がPrototypeパターンと言えます。

cloneは諸刃の剣的な一面もあります。きちんとした設計にしないと、逆にすごくイマイチな実装になってしまいかねないので、気をつけましょう。

Prototypeパターンの注意点

ディープコピーとシャローコピーの違いを理解する

Prototypeでは、__cloneマジックメソッドを使用して、複製時に特定の操作や設定を行うことが可能です。

さらに言うと__cloneマジックメソッドでは、ディープコピーに関する処理を行うこともめずらしくありません。そのため、本格的にPrototypeを利用するのであれば、コピーの違いを理解しておく必要があるでしょう。

上の方でも挙げましたが、シャローコピー・ディープコピーの違いは是非学んでおきたいところです。

使い所に注意

全てのシチュエーションでPrototypeパターンが適しているわけではありません。

私はPHPの実務を20年以上こなしていますが、ぶっちゃけcloneを使った記憶が無いです(汗)。

もちろん私の力不足により、適したタイミングでcloneやPrototypeパターンを使うという発想が持てなかったことに起因することもあります。

ですから、本当に適したところでPrototypeを使う必要がある……ということに注意が必要です。

逆にそうでないところでは、無理して使う必要は全くありません。例えばオブジェクトを複製することで、意図しない挙動を引き起こす可能性もありえるからです。

デザインパターンは、使用シチュエーションを正しく判断する(これが難しいのですが)ことが重要です。

Lara
Lara

私も使いこなせておらず、今のところは知識だけに留まっていますが、適した場所では使っていきたいです。

まとめ

以上、Prototypeパターンについて説明しました。

本文でも書いたように、私はcloneを使ったことはほぼ無いと思いますが、学ぶ中で、「あれ、上手く使えばこんなこともできるんだ」という新しい気づきがありました。

しかしデザインパターン全般に言えることですが、すぐに実践で使えるかと言えば違います。

頭の隅にいれておき、適した場所で引き出せるようになれると良いなと思います。デザインパターンの引き出しを多くしておけば、きっとエンジニアの総合力に繋がるはずです。

コメント

タイトルとURLをコピーしました