115
147

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

PHPで考えるデザインパターン

Last updated at Posted at 2018-09-23

PHPによるデザインパターンなるものを勉強したので、自分の整理のため要約してみました。
こちらのブログでは絶版になってしまった書籍「PHPによるデザインパターン入門」の著者の方が、校正前の原稿テキストを公開して下さっています…!ありがとうございます!!

デザインパターンとは?

オブジェクト指向ソフトウェアを設計する際に繰り返し現れる経験的な要素を抽出したもので、効率の良いプログラミングをするためのテンプレートのこと。

要するに、こういうシステムならこういうパターンでプログラミングをすると効率いいよね
みたいなパターン(形)を集約したものです。

ここでは、全部で23個あるパターンの中、

  • Template
  • Singleton
  • Factory
  • Façade
  • Strategy

を見ていきます。

Template

Template Methodパターンは、親クラスで処理の大きな枠組み(テンプレート)を定義し、具体的な処理内容をサブクラスで決定させるためのパターンです。

1つのオペレーションにアルゴリズムのスケルトンを定義しておき、その中のいくつかのステップについては、サブクラスでの定義に任せることにする。Template Methodパターンでは、アルゴリズムの構造を変えずに、アルゴリズム中のあるステップをサブクラスで定義する。

「処理の一部分をサブクラスで実装する」というのがポイント。

「飲み物を用意する」といった大枠の定義があり、
その大枠に沿って具体的な手順でもって紅茶を作ったり、コーヒーを作ったりするイメージです。

スクリーンショット 2018-09-23 14.55.48.png

渡されたデータを成形して表示する簡単なアプリケーションを実装するとします。
そこで、まずこの処理の大枠となるクラスを用意します。

AbstractDisplay.class.php
<?php
/**
 * AbstractClassクラスに相当する
 */
abstract class AbstractDisplay
{
    /**
     * 表示するデータ
     */
    private $data;

    /**
     * コンストラクタ
     * @param mixed 表示するデータ
     */
    public function __construct($data)
    {
        if (!is_array($data)) {
            $data = array($data);
        }
        $this->data = $data;
    }

    /**
     * template methodに相当する
     */
    public function display()
    {
        $this->displayHeader();
        $this->displayBody();
        $this->displayFooter();
    }

    /**
     * データを取得する
     */
    public function getData()
    {
        return $this->data;
    }

    /**
     * ヘッダを表示する
     * サブクラスに実装を任せる抽象メソッド
     */
    protected abstract function displayHeader();

    /**
     * ボディ(クライアントから渡された内容)を表示する
     * サブクラスに実装を任せる抽象メソッド
     */
    protected abstract function displayBody();

    /**
     * フッタを表示する
     * サブクラスに実装を任せる抽象メソッド
     */
    protected abstract function displayFooter();

}

コンストラクタの引数として表示するデータを受け取り、displayメソッドでそのデータを成形してヘッダ、ボディ、フッタの順に表示するようになっています。

このAbstractDisplayクラスで定義した処理を具体的に実装したのがListDisplayクラス、TableDisplayクラスです。

ListDisplay.class.php
<?php
require_once 'AbstractDisplay.class.php';

/**
 * ConcreteClassクラスに相当する
 */
class ListDisplay extends AbstractDisplay
{
    /**
     * ヘッダを表示する
     */
    protected function displayHeader()
    {
        echo '<dl>';
    }

    /**
     * ボディ(クライアントから渡された内容)を表示する
     */
    protected function displayBody()
    {
        foreach ($this->getData() as $key => $value) {
            echo '<dt>Item ' . $key . '</dt>';
            echo '<dd>' . $value . '</dd>';
        }
    }

    /**
     * フッタを表示する
     */
    protected function displayFooter()
    {
        echo '</dl>';
    }
}

ListDisplayクラスではデータをリストにして表示します。

TableDisplay.class.php
<?php
require_once 'AbstractDisplay.class.php';

/**
 * ConcreteClassクラスに相当する
 */
class TableDisplay extends AbstractDisplay
{

    /**
     * ヘッダを表示する
     */
    protected function displayHeader()
    {
        echo '<table border="1" cellpadding="2" cellspacing="2">';
    }

    /**
     * ボディ(クライアントから渡された内容)を表示する
     */
    protected function displayBody()
    {
        foreach ($this->getData() as $key => $value) {
            echo '<tr>';
            echo '<th>' . $key . '</th>';
            echo '<td>' . $value . '</td>';
            echo '</tr>';
        }
    }

    /**
     * フッタを表示する
     */
    protected function displayFooter()
    {
        echo '</table>';
    }
}

TableDisplayクラスではデータをテーブルにして表示します。

これらのクラスを利用すると、以下のようになります。

template_method_client.php
<?php
require_once 'ListDisplay.class.php';
require_once 'TableDisplay.class.php';

$data = array('Design Pattern',
              'Gang of Four',
              'Template Method Sample1',
              'Template Method Sample2');

$display1 = new ListDisplay($data);
$display2 = new TableDisplay($data);

$display1->display();
echo '<hr>';
$display2->display();

親クラスであるAbstractDisplayクラスで設定したdisplayメソッドは、子クラスでそれぞれ実装した具体的な処理を実行します。

構成

  • AbstractClassクラス(親)
    大きな枠組みを定義し、子クラスに共通する抽象メソッドを持っている。例の中のAbstractDisplayクラスに当たる

  • ConcreteClassクラス(子)
    AbstractClassクラスを継承した子クラス。親クラスで定義されたメソッドの具体的な処理内容をここで決定する。例の中のListDisplayクラス、TableDisplayクラスに当たる

20141204145824.png

メリット・デメリット

メリット

  • メソッドを共通化してるので、修正するときは親のみ修正すれば済む
  • 子クラスの設計がシンプルになる
  • 子クラス独自の処理を各々追加できる

デメリット

  • 子クラスの数が増える
  • 親と子の関係が密接なので、子を実装する際に親の方もいちいち確認しないといけない

Singlton

Singltonパターンは、生成するインスタンスの数を制限します。

あるクラスに対してインスタンスが1つしか存在しないことを保証し、それにアクセスするためのグローバルな方法を提供する。

システムの設定を表現するクラスや、システム全体で一度読み込んだデータをキャッシュしておくクラスなどでは、インスタンスは一つしか生成させたくないはずです。
そんなとき、開発者が注意深くインスタンスを一つしか生成しないようにすることもできなくはないですが、それは保証されたものではありませんし、何らかのミスが発生してしまうことは大いに考えられます。

Singletonパターンを利用すれば、開発者が意識することなく、インスタンスの生成数を制限することができます。

SingletonSample.class.php
<?php
class SingletonSample
{

    /**
     * メンバー変数
     */
    private $id;

    /**
     * 唯一のインスタンスを保持する変数
     */
    private static $instance;

    /**
     * コンストラクタ
     * IDとして、生成日時のハッシュ値を作成
     */
    private function __construct()
    {
        $this->id = md5(date('r') . mt_rand());
    }

    /**
     * 唯一のインスタンスを返すためのメソッド
     * @return SingletonSampleインスタンス
     */
    public static function getInstance() {
        if (!isset(self::$instance)) {
            self::$instance = new SingletonSample();
            echo 'a SingletonSample instance was created !';
        }

        return self::$instance;
    }

    /**
     * IDを返す
     * @return インスタンスのID
     */
    public function getID()
    {
        return $this->id;
    }

    /**
     * このインスタンスの複製を許可しないようにする
     * @throws RuntimeException
     */
    public final function __clone() {
        throw new RuntimeException ('Clone is not allowed against ' . get_class($this));
    }
}

コンストラクトはprivateになっているため、このクラス内でしかインスタンス化することはできず、getInstanceメソッドを使うことでインスタンス化することが出来ます。
getInstanceメソッドを呼び出すことでインスタンスが生成され返されるようになっていますが、インスタンスは最初に呼び出された時のみ初期化されるようになっている為、二度目以降に呼び出されても一番最初に生成されたインスタンスを返します。
よって何度メソッドを呼び出しても全く同じインスタンスが返され、インスタンスが一意であることを実現しています。

構成

singleton.png

  • Singletonクラス
    他のクラスから直接インスタンスを生成できないが、ただひとつのインスタンスを返すstaticメソッドが用意される。例の中のSingletonSampleクラスに当たる

メリット・デメリット

メリット

  • クライアントからインスタンスへのアクセスを制御する
  • getInstanceメソッド内の処理を変更するだけで、全てのインスタンスの数を決定することができる

デメリット

  • 単体テストがしにくくなる
  • 依存関係を見えにくくし、コードが読みづらくなる
  • プログラムの再利用性が低下する。一度しか使わないからとシングルトンで作ってしまうと複数のユーザーから利用されるような場合に対応できなくなる
  • スケーラビリティが低下する
    などなど

Singetonパターンについては色々と議論があるようですが、ぱっと見デメリットが目立つようです。。
かの有名なプログラマが知るべき97のことにもデメリットについて記載があります。

便利だけどあまり使わないほうがいいのかも?

Factory

Factory、すなわち「クラスのインスタンス製造工場」。

オブジェクトを生成するときのインタフェースだけを規定して、実際にどのクラスをインスタンス化するかはサブクラス が決めるようにする。Factory Methodパターンは、インスタンス化をサブクラスに任せる。

Factoryクラスでは、オブジェクトを生成する(工場)クラスと生成したオブジェクトを使用するクラスを分けます。
オブジェクトの使用者はこの工場にオブジェクトの生成を依頼すると、
オブジェクトの生成手順や種類を意識する必要はなく、望むオブジェクトを使用できる状態で手に入れることができます。

発注者が、「高い音の出る金管楽器と、低い音の出る金管楽器を作って!」と工場に発注すると、
工場の中で高い音の出る金管楽器はトランペット、低い音の出る金管楽器はチューバと判断し生成をして
発注者に納品する、といったイメージです。
発注者は、発注後に工場の中でどんな楽器が作られるのか、どのように作られるのかはわからなくても望む楽器を手にすることができます。

スクリーンショット 2018-09-23 17.28.22.png

例えばcsvファイルを読み込んで結果をhtmlに出力する、という処理と
xmlファイルを読み込んで結果をhtmlに出力する、という処理を両方行いたいとき、

  • 外部ファイルを読み込む
  • htmlを出力する

という共通の処理を持っているにも関わらず、ファイル形式が異なるため同じ実装にはならないため
同じメソッドとしてまとめようとすると分岐が増え、複雑な実装になってしまいます。

この場合Factoryパターンを採用すると、
上記の共通の処理を持つインターフェースクラスをまず定義し、(Readerクラス)

Reader.class.php
<?php
/**
 * 読み込み機能を表すインターフェースクラスです
 */
interface Reader {
    public function read();
    public function display();
}

csvとxmlそれぞれのReaderインターフェースを実装したクラスを定義し、(CSVFileReaderクラス、XMLFileReaderクラス)

CSVFileReader.class.php
<?php
require_once("Reader.class.php");

/**
 * CSVファイルの読み込みを行なうクラスです
 */
class CSVFileReader implements Reader
{
    /**
     * 内容を表示するファイル名
     *
     * @access private
     */
    private $filename;

    /**
     * データを扱うハンドラ名
     *
     * @access private
     */
    private $handler;

    /**
     * コンストラクタ
     *
     * @param string ファイル名
     * @throws Exception
     */
    public function __construct($filename)
    {
        if (!is_readable($filename)) {
            throw new Exception('file "' . $filename . '" is not readable !');
        }
        $this->filename = $filename;
    }

    /**
     * 読み込みを行ないます
     */
    public function read()
    {
        $this->handler = fopen ($this->filename, "r");
    }

    /**
     * 表示を行ないます
     */
    public function display()
    {
        $column = 0;
        $tmp = "";
       while ($data = fgetcsv ($this->handler, 1000, ",")) {
            $num = count ($data);
            for ($c = 0; $c < $num; $c++) {
                if ($c == 0) {
                    if ($column != 0 && $data[$c] != $tmp) {
                        echo "</ul>";
                    }
                    if ($data[$c] != $tmp) {
                        echo "<b>" . $data[$c] . "</b>";
                        echo "<ul>";
                        $tmp = $data[$c];
                    }
                }else {
                    echo "<li>";
                    echo $data[$c];
                    echo "</li>";
                }
            }
            $column++;
        }
        echo "</ul>";
        fclose ($this->handler);
    }
}
XMLFileReader.class.php
<?php
require_once("Reader.class.php");

/**
 * XMLファイルの読み込みを行なうクラスです
 */
class XMLFileReader implements Reader
{
    /**
     * 内容を表示するファイル名
     *
     * @access private
     */
    private $filename;

    /**
     * データを扱うハンドラ名
     *
     * @access private
     */
    private $handler;

    /**
     * コンストラクタ
     *
     * @param string ファイル名
     * @throws Exception
     */
    public function __construct($filename)
    {
        if (!is_readable($filename)) {
            throw new Exception('file "' . $filename . '" is not readable !');
        }
        $this->filename = $filename;
    }

    /**
     * 読み込みを行ないます
     */
    public function read()
    {
        $this->handler = simplexml_load_file($this->filename);
    }

    /**
     * 文字コードの変換を行います
     */
    private function convert($str)
    {
        return mb_convert_encoding($str, mb_internal_encoding(), "auto");
    }

    /**
     * 表示を行ないます
     */
    public function display()
    {
        foreach ($this->handler->artist as $artist) {
            echo "<b>" . $this->convert($artist['name']) . "</b>";
            echo "<ul>";
            foreach ($artist->music as $music) {
                echo "<li>";
                echo $this->convert($music['name']);
                echo "</li>";
            }
            echo "</ul>";
        }
    }

}

指定されたファイルによりCSVFileReaderクラスとXMLFileReaderクラスのどちらを利用するか判別し生成するクラスを定義し(ReaderFactoryクラス)

ReaderFactory.class.php
<?php
require_once('Reader.class.php');
require_once('CSVFileReader.class.php');
require_once('XMLFileReader.class.php');

/**
 * Readerクラスのインスタンス生成を行なうクラスです
 */
class ReaderFactory
{
    /**
     * Readerクラスのインスタンスを生成します
     */
    public function create($filename)
    {
        $reader = $this->createReader($filename);
        return $reader;
    }

    /**
     * Readerクラスのサブクラスを条件判定し、生成します
     */
    private function createReader($filename)
    {
        $poscsv = stripos($filename, '.csv');
        $posxml = stripos($filename, '.xml');

        if ($poscsv !== false) {
            $r = new CSVFileReader($filename);
            return $r;
        } elseif ($posxml !== false) {
            return new XMLFileReader($filename);
        } else {
            die('This filename is not supported : ' . $filename);
        }
    }
}

ReaderFactoryクラスを利用するという流れになり、利用側のコードがスッキリします!
ここでReaderFactoryクラスはcreateメソッドに渡されたファイル形式により、生成するインスタンスを判別し生成します。

factory_client.php
<?php
require_once('ReaderFactory.class.php');
?>
<html lang="ja">
<head>
<title>Result</title>
</head>
<body>
<?php
    /**
     * 外部からの入力ファイルです
     */
    $filename = 'Music.xml'; // csvファイルでもOK

    $factory = new ReaderFactory();
    $data = $factory->create($filename);
    $data->read();
    $data->display();
?>
</body>
</html>

Factory Methodパターンは、継承を利用しており、まさにTemplate Methodパターンの代表的な適用と言えます。

構成

20141208182623.png

  • Productクラス
    オブジェクト生成メソッド(工場)で生成されるオブジェクト(製品)のAPIを定義するクラス。

  • ConcreteProductクラス
    Productクラスのサブクラスで、Productクラスで定義されたAPIを実装したクラス

  • Creatorクラス
    オブジェクト生成メソッド(工場)を提供するクラス

  • ConcreteCreatorクラス
    Creatorクラスを継承したサブクラス。ConcreteProductクラスのインスタンスを返す

メリット・デメリット

メリット

  • オブジェクトの生成処理と使用処理を分離できる
  • オブジェクトの利用側とオブジェクトのクラスの結びつきを低くする

デメリット

  • 新しいオブジェクトが増えていくと面倒

Façade

Façadeはフランス語で「窓口」の意味。

サブシステム内に存在する複数のインターフェースに1つの統一インターフェースを与える。Façadeパターンはサブシステムの利用を容易にするための高レベルインターフェースを定義する。

複雑なシステムになればなるほど、クラスの数も増え、クラス同士の関係も複雑になっていきます。
一つの処理をするにも、複雑なクラス群の関係を理解し適切な用途でクラスを利用する必要があります。
そういう場合、Façadeパターンを利用して、
この複雑なクラス群を隠蔽し利用する窓口となるAPIを用意すれば、クラス群の関係をいちいち意識する事なく、そのAPIを利用するだけで処理を実行することができます。

銀行でお金を預けたり引き出したりしたい時、様々な複雑な処理が必要になってきます。
そんな時、その複雑な処理を把握している窓口の人に相談すれば、その人がこちらが要求する処理を行ってくれる、といったようなイメージです。
スクリーンショット 2018-09-23 17.43.40.png

「商品を注文する」という処理をする際

まずDBや外部ファイルから商品の情報をDBに関する処理をまとめたクラス(ItemDao.class.php)(簡略化のためここではDBではなくテキストファイルから情報を取得する)を利用し取得し、その商品情報を持つインスタンスを商品クラス(Item.class.php)により生成し

ItemDao.class.php
<?php
require_once 'OrderItem.class.php';

class ItemDao
{
    private static $instance;
    private $items;
    private function __construct()
    {
        $fp = fopen('item_data.txt', 'r');

        /**
         * ヘッダ行を抜く
         */
        $dummy = fgets($fp, 4096);

        $this->items = array();
        while ($buffer = fgets($fp, 4096)) {
            $item_id = trim(substr($buffer, 0, 10));
            $item_name = trim(substr($buffer, 10, 20));
            $item_price = trim(substr($buffer, 30));

            $item = new Item($item_id, $item_name, $item_price);
            $this->items[$item->getId()] = $item;
        }

        fclose($fp);
    }

    public static function getInstance() {
        if (!isset(self::$instance)) {
            self::$instance = new ItemDao();
        }
        return self::$instance;
    }

    public function findById($item_id)
    {
        if (array_key_exists($item_id, $this->items)) {
            return $this->items[$item_id];
        } else {
            return null;
        }
    }

    public function setAside(OrderItem $order_item)
    {
        echo $order_item->getItem()->getName() . 'の在庫引当をおこないました<br>';
    }

    /**
     * このインスタンスの複製を許可しないようにする
     * @throws RuntimeException
     */
    public final function __clone() {
        throw new RuntimeException ('Clone is not allowed against ' . get_class($this));
    }
}
Item.class.php
<?php
class Item
{
    private $id;
    private $name;
    private $price;
    public function __construct($id, $name, $price)
    {
        $this->id = $id;
        $this->name = $name;
        $this->price = $price;
    }
    public function getId()
    {
        return $this->id;
    }
    public function getName()
    {
        return $this->name;
    }
    public function getPrice()
    {
        return $this->price;
    }
}

その商品クラスを引数に、どの商品をいくつ注文するか、という情報を持つインスタンスを注文商品クラス(OrderItem.class.php)により生成し

OrderItem.class.php
<?php
require_once 'Item.class.php';

class OrderItem
{
    private $item;
    private $amount;
    public function __construct(Item $item, $amount)
    {
        $this->item = $item;
        $this->amount = $amount;
    }
    public function getItem()
    {
        return $this->item;
    }
    public function getAmount()
    {
        return $this->amount;
    }
}

注文商品クラスを格納するインスタンスを注文クラス(Order.class.php)により生成し

Order.class.php
<?php
require_once 'OrderItem.class.php';

class Order
{
    private $items;
    public function __construct()
    {
        $this->items = array();
    }
    public function addItem(OrderItem $order_item)
    {
        $this->items[$order_item->getItem()->getId()] = $order_item;
    }
    public function getItems()
    {
        return $this->items;
    }
}

ItemDaoクラスのsetAsideメソッドを利用して在庫引当処理をし、(ここでは簡略化のため、文言表示のみ)
データベースにアクセスし、注文情報を登録するクラス(OrderDaoクラス)を利用し、注文が完了するという流れになります。

OrderDao.class.php
<?php
require_once 'Order.class.php';

class OrderDao
{
    public static function createOrder(Order $order) {
        echo '以下の内容で注文データを作成しました';

        echo '<table border="1">';
        echo '<tr>';
        echo '<th>商品番号</th>';
        echo '<th>商品名</th>';
        echo '<th>単価</th>';
        echo '<th>数量</th>';
        echo '<th>金額</th>';
        echo '</tr>';

        foreach ($order->getItems() as $order_item) {
            echo '<tr>';
            echo '<td>' . $order_item->getItem()->getId() . '</td>';
            echo '<td>' . $order_item->getItem()->getName() . '</td>';
            echo '<td>' . $order_item->getItem()->getPrice() . '</td>';
            echo '<td>' . $order_item->getAmount() . '</td>';
            echo '<td>' . ($order_item->getItem()->getPrice() * $order_item->getAmount()) . '</td>';
            echo '</tr>';
        }
        echo '</table>';
    }
}

クライアント側でこれらを順番に実行するよう実装することもできますが、
クラス同士の複雑な関係を把握する必要がある他、コードが複雑になってしまい可読性が落ちてしまいます。

そこでFaçadeクラスに相当するクラス(OrderManager.class.php)を用意し

OrderManager.class.php
<?php
require_once 'Order.class.php';
require_once 'ItemDao.class.php';
require_once 'OrderDao.class.php';

class OrderManager
{
    public static function order(Order $order) {
        $item_dao = ItemDao::getInstance();
        foreach ($order->getItems() as $order_item) {
            $item_dao->setAside($order_item);
        }

        OrderDao::createOrder($order);
    }
}

このクラスのorderメソッドに「どの商品をいくつ注文する」という情報が格納されたOrderインスタンスを引数として渡せば、
注文処理が実行されるようになります。

facade_client.php
<?php
require_once 'Order.class.php';
require_once 'OrderItem.class.php';
require_once 'ItemDao.class.php';
require_once 'OrderManager.class.php';

$order = new Order();
$item_dao = ItemDao::getInstance();

// IDが1のItemインスタンスを2個注文する情報をOrderインスタンスに格納
$order->addItem(new OrderItem($item_dao->findById(1), 2));
// IDが2のItemインスタンスを1個注文する情報をOrderインスタンスに格納
$order->addItem(new OrderItem($item_dao->findById(2), 1));
// IDが3のItemインスタンスを3個注文する情報をOrderインスタンスに格納
$order->addItem(new OrderItem($item_dao->findById(3), 3));

/**
 * 注文処理はこの1行だけ
 */
OrderManager::order($order);

複雑な処理がまとまっている窓口となるFaçadeクラスを利用するだけで、
クライアント側の実装はかなりスッキリします。

構成

facade.png

  • Façadeクラス
    サブシステム内のクラス同士の関係を把握し、クライアントからの要求をサブシステム内の適切なオブジェクトに渡す。サブシステムで提供される統一APIを持つクラス。例の中のOrderManagerクラスに当たる。

  • サブシステム内のクラス群
    サブシステムを構成するクラス群。Façadeクラスの存在は知らない。例の中のItemクラス、OrderItemクラス、Orderクラス、ItemDaoクラス、OrderDaoクラスに当たる。

  • Clientクラス
    サブシステムを利用するクラス。Façadeクラスを通じてサブシステムにアクセスする。例の中のfacade_client.phpに当たる。

メリット・デメリット

メリット

  • クライアント側からは窓口となるサブシステムの入り口しか見えなくなり、サブシステムの構成を隠しクライアント側が意識しなければならないクラスの数を抑えることができる

  • サブシステムとクライアント側の結びつきをゆるくすることで、独立性が高くなりコードに変更が必要な場合でもお互いに影響することなく変更をすることができる

デメリット

  • サブシステムの構成がわかりにくくなるため、テストがしづらい他、再利用性が下がる

変に使いまくればいい、というわけでもなさそうです。
テストをしっかり行いたい場合なんかは単純にクライアントサイドにつらつらとサブシステムの各処理を記載した方が良さそうですね。

Strategy

strategyという単語は「戦略」「作戦」「方針」「方策」などの意味があります。

アルゴリズムの集合を定義し、各アルゴリズムをカプセル化して、それらを交換可能にする。Strategyパターンを利用することで、アルゴリズムを、それを利用するクライアントからは独立に変更することができるようになる。

例えばデータ形式が異なる複数のデータを入出力したい時、
データの入力、出力といった大まかな処理は共通していても、実際に入出力する際の処理は異なります。
同じクラス内でifなどの分岐を利用して実装しようとすれば、コードが冗長になってしまい可読性が下がってしまいます。
そんな時に、入力、出力という大まかな処理を抽象メソッドなど実装したクラスを用意しておいて、それを継承した子クラスを作成し
各子クラス内でそれぞれのデータ形式にあった具体的な処理を実装しておきます。
その子クラスにアクセスできる共通APIを介し、利用したい子クラスを選択し実行するのがStrategyパターンです。

このように、Strategyパターンはアルゴリズムをクラスとして定義し、切り替えられるようにすることを目的としています。

コーヒーが飲みたくなって淹れようとしたとき、インスタントコーヒーを淹れるか、サイフォンで淹れるか、ドリップで淹れるかを自分で選択して決めた方法で淹れるといったイメージです。

スクリーンショット 2018-09-23 17.58.40.png

固定長データ(fixed_length_data.txt)

商品名 商品番号 価格 発売日
限定Tシャツ ABC0001 3800 20060311
ぬいぐるみ ABC0002 1500 20051201
クッキーセット ABC0003 800 20060520

タブ区切りデータ(tab_separated_data.txt)

商品番号 商品名 価格 発売日
ABC0001 限定Tシャツ 3800 2006/3/11
ABC0002 ぬいぐるみ 1500 2005/12/1
ABC0003 クッキーセット 800 2006/5/20

この2種類のデータを読み込み表示させる処理を行うとき、strategyパターンを利用するとより簡潔に実装することができます。

まずStrategyクラスに当たるReadItemDataStrategyというクラスを用意します。

ReadItemDataStrategy.class.php
<?php
/**
 * Strategyに相当する
 */
abstract class ReadItemDataStrategy
{

    private $filename;

    /**
     * コンストラクタ
     */
    public function __construct($filename)
    {
        $this->filename = $filename;
    }

    /**
     * データファイルを読み込み、オブジェクトの配列で返す
     * Contextに提供するメソッド
     * @param string データファイル名
     * @return データオブジェクトの配列
     */
    public function getData()
    {
        if (!is_readable($this->getFilename())) {
            throw new Exception('file [' . $this->getFilename() . '] is not readable !');
        }

        return $this->readData($this->getFilename());
    }

    /**
     * ファイル名を返す
     * @return ファイル名
     */
    public function getFilename()
    {
        return $this->filename;
    }

    /**
     * ConcreteStrategyクラスに実装させるメソッド
     * @param string データファイル名
     * @return データオブジェクトの配列
     */
    protected abstract function readData($filename);
}

インスタンス作成時に引数として渡されたデータを取得するgetFilenameメソッドと、
抽象メソッドであるreadDataメソッド、
getData()でgetFilename()で取得したデータをreadData()するgetDataメソッドが実装されています。

各ファイルごとにデータを読み込む処理は異なってくるので、readDataメソッドの具体的な処理はこのクラスを継承した各データ形式に向け作られたクラスごとに実装します。

固定長データを取り扱うクラスは以下の通りです。

ReadFixedLengthDataStrategy.class.php
<?php
require_once 'ReadItemDataStrategy.class.php';

/**
 * 固定長データを読み込む
 * ConcreteStrategyに相当する
 */
class ReadFixedLengthDataStrategy extends ReadItemDataStrategy
{

    /**
     * データファイルを読み込み、オブジェクトの配列で返す
     * @param string データファイル名
     * @return データオブジェクトの配列
     */
    protected function readData($filename)
    {
        $fp = fopen($filename, 'r');

        /**
         * ヘッダ行を抜く
         */
        $dummy = fgets($fp, 4096);

        /**
         * データの読み込み
         */
        $return_value = array();
        while ($buffer = fgets($fp, 4096)) {
            $item_name = trim(substr($buffer, 0, 20));
            $item_code = trim(substr($buffer, 20, 10));
            $price = (int)substr($buffer, 30, 8);
            $release_date = substr($buffer, 38);

            /**
             * 戻り値のオブジェクトの作成
             */
            $obj = new stdClass();
            $obj->item_name = $item_name;
            $obj->item_code = $item_code;
            $obj->price = $price;

            $obj->release_date = mktime(0, 0, 0,
                                        substr($release_date, 4, 2),
                                        substr($release_date, 6, 2),
                                        substr($release_date, 0, 4));

            $return_value[] = $obj;
        }

        fclose($fp);

        return $return_value;
    }
}

タブ区切りデータを取り扱うクラスは以下の通りです。

ReadTabSeparatedDataStrategy.class.php
<?php
require_once 'ReadItemDataStrategy.class.php';

/**
 * タブ区切りデータを読み込む
 * ConcreteStrategyに相当する
 */
class ReadTabSeparatedDataStrategy extends ReadItemDataStrategy
{

    /**
     * データファイルを読み込み、オブジェクトの配列で返す
     * @param string データファイル名
     * @return データオブジェクトの配列
     */
    protected function readData($filename)
    {
        $fp = fopen($filename, 'r');

        /**
         * ヘッダ行を抜く
         */
        $dummy = fgets($fp, 4096);

        /**
         * データの読み込み
         */
        $return_value = array();
        while ($buffer = fgets($fp, 4096)) {
            list($item_code, $item_name, $price, $release_date) = split("\t", $buffer);

            /**
             * 戻り値のオブジェクトの作成
             */
            $obj = new stdClass();
            $obj->item_name = $item_name;
            $obj->item_code = $item_code;
            $obj->price = $price;

            list($year, $mon, $day) = split('/', $release_date);
            $obj->release_date = mktime(0, 0, 0,
                                        $mon,
                                        $day,
                                        $year);

            $return_value[] = $obj;
        }

        fclose($fp);

        return $return_value;
    }
}

これらのクラスは、以下のクラスを介して利用されます。

ItemDataContext.class.php
<?php
/**
 * Contextに相当する
 */
class ItemDataContext
{

    private $strategy;

    /**
     * コンストラクタ
     * @param ReadItemDataStrategy ReadItemDataStrategyオブジェクト
     */
    public function __construct(ReadItemDataStrategy $strategy)
    {
        $this->strategy = $strategy;
    }

    /**
     * 商品情報をオブジェクトの配列で返す
     * @return データオブジェクトの配列
     */
    public function getItemData()
    {
        return $this->strategy->getData();
    }

}

このクラスからインスタンスを作成するときに固定長データのオブジェクトかタブ区切りのオブジェクトを渡すと
そのオブジェクトのgetDataメソッドをgetItemDataメソッドで利用できるようになります。

クライアントサイドで以下のようにしてこのItemDataContextクラスを利用します。

strategy_client.php
<?php
require_once 'ItemDataContext.class.php';
require_once 'ItemDataContextByName.class.php';
require_once 'ReadFixedLengthDataStrategy.class.php';
require_once 'ReadTabSeparatedDataStrategy.class.php';

function dumpData($data) {
    echo '<dl>';
    foreach ($data as $object) {
        echo '<dt>' . $object->item_name . '</dt>';
        echo '<dd>商品番号:' . $object->item_code . '</dd>';
        echo '<dd>\\' . number_format($object->price) . '-</dd>';
        echo '<dd>' . date('Y/m/d', $object->release_date) . '発売</dd>';
    }
    echo '</dl>';
}

/**
 * 固定長データを読み込む
 */
$strategy1 = new ReadFixedLengthDataStrategy('fixed_length_data.txt');
$context1 = new ItemDataContext($strategy1);
dumpData($context1->getItemData());

echo '<hr>';

/**
 * タブ区切りデータを読み込む
 */
$strategy2 = new ReadTabSeparatedDataStrategy('tab_separated_data.txt');
$context2 = new ItemDataContext($strategy2);
dumpData($context2->getItemData());

それぞれのデータ形式を取り扱うインスタンスを作成し、
ItemDataContextクラスにそのインスタンスを引数として渡し作成したインスタンスを利用し
getItemDataメソッドを利用することでそのデータ形式にあった読み込み処理を実行しています。

こうすることで、実際に生成したインスタンス以外全く同じ処理で別々の処理を実行することができます。

構成

strategy2.png

  • Strategyクラス
    ConcreteStrategyクラスのそれぞれの処理に共通のAPIを定義する。例の中のReadItemDataStrategyクラスに当たる。

  • ConcreteStrategyクラス
    Strategyクラスのサブクラスで、Strategyクラスで定義されたAPIを具体的に実装したクラス。例の中のReadFixedLengthDataStrategyクラス、ReadTabSeparatedDataStrategyクラスに当たる。

  • Contextクラス
    Strategy型のオブジェクトを内部に保持し、具体的な処理をそのオブジェクトに委譲する。例の中のItemDataContextクラスに当たる。

メリット・デメリット

メリット

  • それぞれの処理毎にクラスをまとめることができ、新しい処理を追加する際もそれ用のクラスを用意すれば済み、保守性が高まる

  • 異なる処理を選択するためにif文などで分岐する必要がなくなる

  • 使いたいConcreteStrategyクラスのインスタンスをContextオブジェクトに渡すだけで、異なる処理を動的に切り替えることができる

デメリット

  • 構造が複雑になりコード量が増えがち

参考

https://qiita.com/shoheiyokoyama/items/c2ce16b4f492cd014d50
https://qiita.com/mo12ino/items/abf2e31e34278ebea42c
https://blog.ytake.jp.net/entry/2015/12/16/011812
http://www.nulab.co.jp/designPatterns/designPatterns1/designPatterns1-1.html

115
147
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
115
147

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?