はむはむエンジニアぶろぐ

365日エンジニアリング

PHPでStrategyパターンを実装してみた

f:id:secret_hamuhamu:20150816222337j:plain
GoFのデザインパターンの一つStrategyパターンをPHPで実装してみた。
Strategyとは「戦略」のことで、戦略とはアルゴリズムのことを指しています。
Strategyパターンは、アルゴリズム(振る舞い)をカプセル化し振る舞いの差替えや追加、削除をしやすくするものです。


簡単に言うと、こんな感じのアルゴリズムを別のクラスに切り出そうと言うパターンですね。

if (/* 条件A */) {
    // アルゴリズム
    return '戦略A';
} elseif (/* 条件B */) {
    // アルゴリズム
    return '戦略B';
} elseif (/* 条件C */) {
    // アルゴリズム
    return '戦略C';
}

サンプルコード

ユーザの状態に応じて、支払額を決定するアルゴリズムを実装した。

  • グレードがゴールドなら支払額が、1割引であること
  • グレードがシルバーなら支払額が、0.5割引であること
  • グレードが一般かつ性別が女性なら支払額が、0.5割引であること

これらの戦略(アルゴリズム)は、月日とともに追加されたり変更、削除されるものとする。
その場合、ユーザの状態に応じた振り分けや支払額計算アルゴリズムが、一つに固まっていると変更時の影響度が高くなってしまいます。


Strategyパターンをあてて段階的にリファクタリングしていこうと思う。

サンプルコード1

まずは、Strategyパターンを当てる前。

Client

<?php
require_once 'User.php';
require_once 'Amount.php';
require_once 'PaymentCalculator1.php';

$calculator = new PaymentCalculator1();

$user   = new User('ゴールド', '女性');
$amount = new Amount(1000);

$calcedAmount = $calculator->calc($user, $amount);

// 900
echo $calcedAmount->getValue();
echo "\r\n";


$user   = new User('シルバー', '女性');
$amount = new Amount(1000);

$calcedAmount = $calculator->calc($user, $amount);

// 950
echo $calcedAmount->getValue();
echo "\r\n";


$user   = new User('一般', '女性');
$amount = new Amount(1000);

$calcedAmount = $calculator->calc($user, $amount);

// 950
echo $calcedAmount->getValue();
echo "\r\n";


$user   = new User('一般', '男性');
$amount = new Amount(1000);

$calcedAmount = $calculator->calc($user, $amount);

// 1000
echo $calcedAmount->getValue();
echo "\r\n";

User

<?php
class User
{
    private $grade;
    private $sex;

    public function __construct($grade, $sex)
    {
        $this->grade = $grade;
        $this->sex   = $sex;
    }

    public function getGrade()
    {
        return $this->grade;
    }

    public function getSex()
    {
        return $this->sex;
    }
}

Amount

<?php
class Amount
{
    private $value;
    public function __construct($value)
    {
        $this->value = $value;
    }

    public function getValue()
    {
        return $this->value;
    }
}

PaymentCalculator1

<?php
class PaymentCalculator1
{
    public function calc(User $user, Amount $amount)
    {
        $grade  = $user->getGrade();
        $sex    = $user->getSex();

        if ($grade === 'ゴールド') {
            return new Amount(floor($amount->getValue() * 0.90));
        }

        if ($grade === 'シルバー') {
            return new Amount(floor($amount->getValue() * 0.95));
        }

        if ($grade === '一般' && $sex === '女性') {
            return new Amount(floor($amount->getValue() * 0.95));
        }

        return new Amount($amount->getValue());
    }
}

PaymentCalculator1を見てもらえればわかると思うが、 ユーザの状態に応じた振り分け支払額計算アルゴリズム が固まっています。
もし、グレードがシルバーのユーザに対する支払額が0.7割引になった場合、アルゴリズムに修正を加えなくてはなりません。
そうなった場合、他のアルゴリズムにも影響を与えてしまう可能性があります。

安全にリファクタリングするためにStrategyパターンをあてます。


サンプルコード2

PaymentCalculator2

<?php
class PaymentCalculator2
{
    public function calc(User $user, Amount $amount)
    {
        $grade  = $user->getGrade();
        $sex    = $user->getSex();

        if ($grade === 'ゴールド') {
            return (new NineDiscountAmountStrategy())->calc($amount);
        }

        if ($grade === 'シルバー') {
            return (new NineFiveDiscountAmountStrategy())->calc($amount);
        }

        if ($grade === '一般' && $sex === '女性') {
            return (new NineFiveDiscountAmountStrategy())->calc($amount);
        }

        return (new NormalAmountStrategy())->calc($amount);
    }
}

支払額計算アルゴリズムを各Strategyクラスに委譲しました。
支払額計算アルゴリズムに変更が生じた際は、Strategyに修正を加える。
ユーザの状態に応じた振り分け支払額アルゴリズム を分離することができた。

Strategy

<?php

interface StrategyCalcInterface {
    public function calc(Amount $amount);
}

class NineDiscountAmountStrategy implements StrategyCalcInterface
{
    public function calc(Amount $amount)
    {
        return new Amount(floor($amount->getValue() * 0.90));
    }
}

class NineFiveDiscountAmountStrategy implements StrategyCalcInterface
{
    public function calc(Amount $amount)
    {
        return new Amount(floor($amount->getValue() * 0.95));
    }
}

class NormalAmountStrategy implements StrategyCalcInterface
{
    public function calc(Amount $amount)
    {
        return new Amount($amount->getValue());
    }
}

各Strategyには interface を付与する。
Strategyパターンは、ポリモフィズムを活用するからである。


サンプルコード3

サンプルコード2の状態で、十分Strategyパターンと言えますが、 PaymentCalculator から ユーザの状態に応じた振り分け を分離することができます。
Factoryパターンを使うやり方です。

PaymentCalculator3

<?php
class PaymentCalculator3
{
    public function calc(User $user, Amount $amount)
    {
        $strategy = UserAmountStrategy::create($user);

        return $strategy->calc($amount);
    }
}

class UserAmountStrategy
{
    public static function create(User $user)
    {
        $grade  = $user->getGrade();
        $sex    = $user->getSex();

        if ($grade === 'ゴールド') {
            return new NineDiscountAmountStrategy();
        }

        if ($grade === 'シルバー') {
            return new NineFiveDiscountAmountStrategy();
        }

        if ($grade === '一般' && $sex === '女性') {
            return new NineFiveDiscountAmountStrategy();
        }

        return new NormalAmountStrategy();
    }
}

PaymentCalculatorが随分スッキリした。
Strategyの選択に $amount は関与していなかったので、さらに凝集度を上げることができた。

さらに、リファクタリングするのであれば、ユーザの状態をUserクラスにを持たせるなどがあります。
$user->isGradeGold() で、ユーザのグレードがゴールドであるかというアルゴリズムをUserクラスにカプセル化する。

まとめ

Strategyパターンで、アルゴリズムをカプセル化することで、振る舞いの差替えや追加、削除をしやすくした。
関心事が固まるので修正しやすい。 しかし、クラスが多くなり複雑さも増すので、早い段階から無理に適用しないこと。

こちら今回のサンプルコードです。
StrategySample

おすすめの本

オブジェクト指向における再利用のためのデザインパターン

オブジェクト指向における再利用のためのデザインパターン

  • 作者: エリックガンマ,ラルフジョンソン,リチャードヘルム,ジョンブリシディース,Erich Gamma,Ralph Johnson,Richard Helm,John Vlissides,本位田真一,吉田和樹
  • 出版社/メーカー: ソフトバンククリエイティブ
  • 発売日: 1999/10
  • メディア: 単行本
  • 購入: 21人 クリック: 711回
  • この商品を含むブログ (210件) を見る

Head Firstデザインパターン ―頭とからだで覚えるデザインパターンの基本

Head Firstデザインパターン ―頭とからだで覚えるデザインパターンの基本

  • 作者: Eric Freeman,Elisabeth Freeman,Kathy Sierra,Bert Bates,佐藤直生,木下哲也,有限会社福龍興業
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2005/12/02
  • メディア: 大型本
  • 購入: 14人 クリック: 362回
  • この商品を含むブログ (98件) を見る

新装版 リファクタリング―既存のコードを安全に改善する― (OBJECT TECHNOLOGY SERIES)

新装版 リファクタリング―既存のコードを安全に改善する― (OBJECT TECHNOLOGY SERIES)

レガシーコード改善ガイド (Object Oriented SELECTION)

レガシーコード改善ガイド (Object Oriented SELECTION)

  • 作者: マイケル・C・フェザーズ,ウルシステムズ株式会社,平澤章,越智典子,稲葉信之,田村友彦,小堀真義
  • 出版社/メーカー: 翔泳社
  • 発売日: 2009/07/14
  • メディア: 大型本
  • 購入: 45人 クリック: 673回
  • この商品を含むブログ (152件) を見る