株式会社クイックのWebサービス開発blog

HAPPYなエンジニア&デザイナーのブログです

PHPでStrategyパターンを考えてみよう

甘い食べ物は好きですが、マカロンさんとはいい関係を築けていません。

11月中旬にチームにジョインしたフルーツパーラーと申します。
アプリケーションエンジニアをしています。

最近触った事や、気になる事、深掘りしたい事を書いていこうと思います。
記念すべき初回のテーマは!!!

ジャーーーン!!

デザインパターン

硬い。。。硬いですね。初回にしては。 いや、初回だから。ですね。

という事で、 先人の知恵を元に、いかにもありそうな実装に落として、理解を深めようと思います。

デザインパターン関連の書籍はJavaで書かれたサンプルが多く、PHPで書かれたサンプルソースはとても少ない印象です。 PHPデザインパターンを利用したコードをさくっと手元で試したい。という方に向けてコードを書いて行きます。 こういう風にかける、というアイディアもコメントで頂けますと幸いです。

デザインパターンとは

デザインパターンとは、GoFGang of Four)が提唱した23個のパターンを指します。 1995年に書かれた本(通称GoF本)は、言語自体が進化した今となっては古いという声もありますが、 設計や実装の知恵として非常に参考になると思います。

各パターンに名前が決まっている事から、 ○○パターンで実装を考えているという言葉だけで実装する処理をイメージ出来るメリットがあります。

以下 wikipediaデザインパターン」より引用

それぞれのパターンは、プログラマの間で何度も繰り返し考え出されてきた。

したがって、それは最善の解決策ではないかもしれないが、
その種の問題に対するトレードオフを考慮した、典型的な解決策ではある。

さらに、コストがかかるかもしれない問題解決を実際に行う前の先行調査として、
大変役に立つ。

パターンに名前が付いていることが重要である。
なぜなら、名前が付いていることで問題や解決策を記述したり、
会話の中で取り上げたりすることができるようになるからである。

今回試すパターン

今回はポリモーフィズムで処理を行うStrategyパターンで進めます。

※ソースはページ下部にすべて記載しています。

Strategy パターンとは

Strategyとは「戦略」を意味します。

書籍やネットでは、条件によって、XML/JSONで出力切り替え、またはXML/CSVのインポートという内容をよく見ます。 ちょっと違う方向で進め、こういうケースでStrategyパターンが利用出来るか?を実証していきます。

◯◯を□□する。という処理があった場合に、◯◯の部分をStrategyとして実装しておくことで、 ◯◯の切り替えを簡単に行えるようにします。

Strategy パターンのメリット

Strategyパターンでは、処理の分離を行う事で、新たな処理を追加しても、既存の処理に影響なく実装を行えます。

1つの処理内に複数の処理が存在しない事から、if/switchの条件分岐が少なくなり、コードの可読性が向上します。 複数の関数を動的に切り替える。という風に考えるとイメージしやすいです。

1つの送信処理の中で、メール送信、SMS送信(他社APIを利用)、Push通知送信が存在した場合に、 新たに何らかの送信処理を追加すると、条件分岐が増え、修正作業が難しくなる事は容易に想像がつきます。

今回の実装

例として、1ユーザに、メールを送信する、SMSを送信する、Push通知を送信するという流れです。

実際に送信する部分は省略していますが、validationをどこにいれるか、例外処理をどこに持つか。 という事を考えると楽しくなりますので、お時間があれば手元でいろいろ試して見て下さい。

登場クラスの役割

  • User

    • メール送信ユーザの情報を保持
  • MessageStrategy

    • 各送信種類の親クラス
  • MessageContext

    • 各送信種類を内部で保持し、クライアント側(クラス利用者)から呼び出される
    • Strategyパターンの肝となるクラス
  • ConcreteMailMessage

    • メール送信処理を行う
  • ConcreteSMSMessage

    • SMS送信処理を行う
  • ConcretePushNotificationMessage

    • Push通知送信を行う

処理の説明

// 送信対象のテストUserインスタンス
$user = new User('Pさん', 'test@example.com', '090xxxxyyyy');
  • 作成した$userをMail送信クラスに注入します
// Mail送信
$strategy = new ConcreteMailMessage($user);
  • 作成したMail送信処理を、コンテキストクラスに注入
// Contextクラスにstrategyクラスを注入
$contextMessage = new MessageContext($strategy);
  • 送信処理
// 送信処理 注入されたオブジェクトのsend()メソッドをcall
$contextMessage->send();
  • 出力
// mail->to(Pさん) と出力される

SMS / Push通知に切り替える場合は

  • SMSを送信したい場合は、SMSに切り替えるだけ
// SMS送信
$strategy = new ConcreteSMSMessage($user);
  • Push通知を送信したい場合は、Push通知に切り替えるだけ
// Push通知
$strategy = new ConcretePushNotificationMessage($user);

簡単に切り替えられます。

まとめ

シンプルな条件分岐なので冗長に見えるかもしれませんが、 簡単に切り替えられた事が分かると思います。

各処理が別々で機能していますので、追加で新しい種類の送信処理を実装しても 既存の送信処理に影響はありません。

補足説明

実は、直接具象クラス->send()を実行する事も出来ます。 しかし、メール、SMS、Push通知のすべてがsend()メソッドを持っているかどうかは 実際に内部を見るまでわかりません。

// コンテキストクラスを利用しなくてもsend()する事は出来るが。
// インスタンスがsend()メソッドを持っている保証はない。クラス内を見るまで判断出来ない
$strategy->send();
// mail->to(Pさん) と出力される

コンテキストクラス(MessageContext)のコンストラクタで タイプヒンティングを行いインターフェースを統一しています。 それによって、send()メソッドが保証されます。

まとめのまとめ

簡単に書きたいと思って説明を書き始めましたが、硬いですね。
うまくお伝え出来ない部分も多々ありますので、下にソース全文を貼ります。
お手元で遊んで見て下さい。

それでは〜!

  • 実際に試したい方は以下ソースをご利用下さい。
<?php

/**
 * 送信対象のUserクラス
 */
class User {
    private $name;
    private $mail;
    private $tel;

    /**
     * 仮に利用するpropertyを定義
     * User constructor.
     * @param $name
     * @param $mail
     * @param $tel
     */
    public function __construct($name, $mail, $tel) {
        $this->name = $name;
        $this->mail = $mail;
        $this->tel  = $tel;
    }

    /**
     * 仮に名前だけ出力するためにgetter設置
     * @return mixed
     */
    public function getName() {
        return $this->name;
    }

}

/**
 * 抽象クラス
 * Class MessageStrategy
 */
abstract class MessageStrategy {
    protected $user;
    public abstract function send();
    public abstract function __construct(User $user);
}

/**
 * コンテキストクラス
 * Class MessageContext
 */
class MessageContext {
    private $strategy;

    /**
     * MessageContext constructor.
     * @param MessageStrategy $message
     */
    public function __construct(MessageStrategy $message) {
        $this->strategy = $message;

    }

    public function send() {
        $this->strategy->send();
    }
}

/**
 * 具象クラス(Mail)
 * Class ConcreteMailMessage
 */
class ConcreteMailMessage extends MessageStrategy {
    public function __construct(User $user) {
        $this->user = $user;
        // mailに関する前処理
        // ...
    }

    public function send() {
        echo 'mail->to('. $this->user->getName(). ')'. PHP_EOL;
    }
}

/**
 * 具象クラス(SMS)
 * Class ConcreteSMSMessage
 */
class ConcreteSMSMessage extends MessageStrategy {
    public function __construct(User $user) {
        $this->user = $user;
        // smsに関する前処理
        // ...
    }

    public function send() {
        echo 'sms->to('. $this->user->getName(). ')'. PHP_EOL;
    }
}

/**
 * 具象クラス(PushNotification)
 * Class ConcretePushNotificationMessage
 */
class ConcretePushNotificationMessage extends MessageStrategy {
    public function __construct(User $user) {
        $this->user = $user;
        // Push通知に関する前処理
        // ...
    }

    public function send() {
        echo 'push->to('.$this->user->getName(). ')'. PHP_EOL;
    }
}

// ------------------------------
// 実際に利用するイメージです
// ------------------------------

// 送信対象のテストUserインスタンス
$user = new User('Pさん', 'test@example.com', '090xxxxyyyy');

// Mail送信
$strategy = new ConcreteMailMessage($user);

// $strategyにどのクラスを代入するかでMail / SMS / PushNotification とsend()の振る舞いを変える
// SMS送信
// $strategy = new ConcreteSMSMessage($user);

// Push通知
// $strategy = new ConcretePushNotificationMessage($user);

// Contextクラスにstrategyクラスを注入
$contextMessage = new MessageContext($strategy);

// 送信処理 注入されたオブジェクトのsend()メソッドをcall
$contextMessage->send();

// コンテキストクラスを利用しなくてもsend()する事は出来るが。
// インスタンスがsend()メソッドを持っている保証はない。クラス内を見るまで判断出来ない
$strategy->send();

// mail->to(Pさん) と出力される