@blog.justoneplanet.info

日々勉強

PHPでStrategyパターン

アルゴリズムをクラスとして定義し、対象によって切り替える。

■実装

class ItemDataContext
{
    private $_strategy;

    public function __construct(ReadItemDataStrategy $strategy){
        $this->_strategy = $strategy;
    }

    public function getItemData(){
        return $this->strategy->getData();
    }
}
abstract class ReadItemDataStrategy
{
    private $_filename;
    public function __construct($filename)
    {
        $this->_filename = $filename;
    }

    public function getData()
    {
        if(!is_readable($filename = $this->getFilename())){
            throw new Exception('is not readable : ' . $filename);
        }
        return $this->readData($filename);
    }

    public function getFilename()
    {
        return $this->_filename;
    }

    protected abstract function readData($filename);
}

ReadFixedLengthDataStrategy.php

class ReadFixedLengthDataStrategy extends ReadItemDataStrategy
{
    protected function readData($filename){
        // implementation
    }
}

ReadTabSeparatedDataStrategy.php

class ReadTabSeparatedDataStrategy extends ReadItemDataStrategy
{
    protected function readData($filename){
        // implementation
    }
}

PHPでProxyパターン

■実装

とりあえずパターンの要ではないクラス。

class Item
{
    private $_id;
    private $_name;
    public function __construct($id, $name)
    {
        $this->_id   = $id;
        $this->_name = $name;
    }

    public function getId()
    {
        return $this->_id;
    }

    public function getName()
    {
        return $this->_name;
    }
}

ItemDao.php

interface ItemDao
{
    public function findById($id);
}
DbItemDao.php

DBからアイテムデータを取得して生成したインスタンスを返す本番用クラス。

class DbItemDao implements ItemDao
{
    public function findById($id)
    {
        $fh = fopen('data.txt', 'r');
        $item = null;
        while($buffer = fgets($fh, 4096)){
            $id1  = trim(substr($buffer, 0, 10));
            $name = trim(substr($buffer, 10));
            if($id === (int) $id1){
                $item = new Item($id1, $name);
                break;
            }
        }
        fclose($fh);
        return $item;
    }
}
MockItemDao.php

ダミーデータで作ったインスタンスを返すテスト用クラス。

class MockItemDao implements ItemDao
{
    public function findById($id)
    {
        $item = new Item($id, 'dummy');
        return $item;
    }
}

ItemDaoProxy.php

class ItemDaoProxy
{
    private $_dao;
    private $_cache;
    public function __construct(ItemDao $dao)
    {
        $this->_dao   = $dao;
        $this->_cache = array();
    }

    public function findById($id)
    {
        if(array_key_exists($id, $this->_cache)){
            return $this->_cache[$id];
        }
        $this->_cache[$id] = $this->dao->findById($id);
        return $this->_cache[$id];
    }
}

■クライアントコード

以下のようにすることで使うことができる。

switch($mode){
    case 'test':
        $dao = new MockItemDao();
        break;
    case 'production':
        $dao = new DbItemDao();
        break;
}
$proxy = new ItemDaoProxy($dao);

PHPでObserverパターン

class Cart
{
    private $_items
    private $_listeners;

    public function __construct()
    {
        $this->_items     = array();
        $this->_listeners = array();
    }

    public function add($cd)
    {
        $this->_items[$cd] = (isset($this->_items[$cd]))? ++$this->_items[$cd] : 1;
        $this->notify();
    }

    public function remove($cd)
    {
        $this->_items[$cd] = (isset($this->_items[$cd]))? --$this->_items[$cd] : 0;
        if($this->_items[$cd] <= 0){
            unset($this->_items[$cd]);
        }
        $this->notify();
    }

    public function get()
    {
        return $this->_items;
    }

    public function has($cd)
    {
        return array_key_exists($cd, $this->_items);
    }

    public function notify()
    {
        foreach($this->_listeners as $listener){
            $listener->update($this);
        }
    }

    public function addListener(CartListener $listener)
    {
        $this->_listeners[get_class($listener)] = $listener;
    }
}

Listener.php

以下のようにインターフェースを定義する。

interface Listener
{
    public function update(Cart $cart);
}

以下のようにListenerを実装する。

PresentListener
class PresentListener implements Listener
{
    const PRESENT_TARGET = '30:cookie';
    const PRESENT_ITEM   = '99:present';

    public function __construct()
    {
    }

    public function update(Cart $cart)
    {
        if($cart->hasItem(self::PRESENT_TARGET) && !$cart->hasItem(self::PRESENT_ITEM)){
            $cart->add(self::PRESENT_ITEM);
        }
        if(!$cart->hasItem(self::PRESENT_TARGET) && $cart->hasItem(self::PRESENT_ITEM)){
            $cart->remove(self::PRESENT_ITEM);
        }
    }
}
LoginListener
class LoginListener implements Listener
{
    public function __construct()
    {
    }

    public function update(Cart $cart)
    {
        var_dump($cart->get());
    }
}

Facade Pattern

■定義

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

■コード

クライアントコード

$order = new Order();

$itemDao = ItemDao::getInstance(); //itemデータアクセスオブジェクト
$order->addItem(new OrderItem($itemDao->findById(1), 2));
$order->addItem(new OrderItem($itemDao->findById(2), 1));
$order->addItem(new OrderItem($itemDao->findById(3), 3));

OrderManager::order($order);// 注文処理

Order.php

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

OrderItem.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;
    }
}

Item.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;
    }
}

OrderManager.php

class OrderManager {
    public static function order(Order $order) {
        $itemDao = ItemDao::getInstance();
        foreach($order->getItems() as $orderItem) {
            $itemDao->setAsign($orderItem);
        }
        OrderDao::createOrder($order);
    }
}

ItemDao.php

/**
 * シングルトン
 */
class ItemDao {
    private static $instance;
    private $items;
    private function __construct() {
        // 以下の構造を生成する
        // $this->items[アイテムのID] = アイテム 
    }
    public static function getInstance() {
        if(!isset(self::instance)) {
            self::instance = new ItemDao();
        }
        return self::instance;
    }
    public function findById($itemId) {
        if(array_key_exists($itemId, $this->items)) {
            return $this->items[$itemId]
        }
        else {
            return null;
        }
    }
    public function setAside(OrderItem $orderItem) {
        return $orderItem->getItem()->getName;
    }
}

OrderDao.php

class OrderDao {
    public static function createOrder(Order $order) {
        // orderをループしてゴニョゴニョ
    }
}

PHPでデザインパターン:FactoryMethod(ファクトリーメソッド)

メリットは「生成と処理を分離し利用側の変更なしに処理(クラス)を追加したりできる」ことである。

■コード

今回使用するのは以下のxmlとcsvファイルの2種類とする。

xml

<data>
    <person>
        <name>John</name>
        <occupation>racer</occupation>
    </person>
    <person>
        <name>Mike</name>
        <occupation>banker</occupation>
    </person>
    <person>
        <name>Nick</name>
        <occupation>runner</occupation>
    </person>
</data>

csv

John,racer
Mike,banker
Nick,runner

template methodで処理の枠だけ定義する。

interface Printer
{
    public function read();
    public function output();
}

以下のCsvFilePrinterクラスとXmlFilePrinterクラスで具体的な処理を記述する。

class CsvFilePrinter implements Printer
{
    private $_filename;
    private $_fh;
    public function __construct($filename)
    {
        $this->_filename = $filename;
    }
    public function read()
    {
        $this->_fh = fopen($this->_filename, 'r');
    }
    public function output()
    {
        print("<table>");
        while($data = fgetcsv($this->_fh)){
            print("<tr>");
            for($i = 0, $n = count($data); $i < $n; $i++){
                print("<td>{$data[$i]}</td>");
            }
            print("</tr>");
        }
        print("</table>");
        fclose($this->_fh);
    }
}
class XmlFilePrinter implements Printer
{
    private $_filename;
    private $_rh;
    public function __construct($filename)
    {
        $this->_filename = $filename;
    }
    public function read()
    {
        $this->_rh = simplexml_load_file($this->_filename);
    }
    public function output()
    {
        print('<table>');
        foreach($this->_rh->person as $person){
            print('<tr>');
            print("<td>{$person->name}</td>");
            print("<td>{$person->occupation}</td>");
            print('</tr>');
        }
        print('</table>');
    }
}

Factory部分

分岐してインスタンスを返すようになっている。

class PrinterFactory
{
    public function getPrinter($filename)
    {
        if(preg_match('/\.csv$/i', $filename)){
            return new CsvFilePrinter($filename);
        }
        elseif(preg_match('/\.xml$/i', $filename)){
            return new XmlFilePrinter($filename);
        }
    }
}

クライアントコード

以下のように中身を意識することなく使用できる。

$printer = new PrinterFactory();
$data = $printer->getPrinter('./dat.xml');
$data->read();
$data->output();
/*
John	racer
Mike	banker
Nick	runner
*/
$data = $printer->getPrinter('./dat.csv');
$data->read();
$data->output();
/*
John	racer
Mike	banker
Nick	runner
*/

PHPでデザインパターン:TemplateMethod(テンプレートメソッド)

メリットは「共通処理をまとめ具体的な処理をサブクラスに実装させられる」こと。

■コード

以下のように共通処理を定義する。具体的な処理が必要な部分は抽象メソッドにしておき、それらをdisplayからコールするようにする。

abstract class Display
{
    private $_data;
    public function __construct($data)
    {
        $this->_data = $data;
    }
    public function getData()
    {
        return $this->_data;
    }
    public function display()
    {
        $this->header();
        $this->body();
        $this->footer();
    }
    abstract protected function header();
    abstract protected function body();
    abstract protected function footer();    
}

以下のようにDisplayクラスを継承したクラス(DisplayAsPlain、DisplayAsList)を定義し、それぞれに具体的(⇔抽象)な処理を記述する。

class DisplayAsPlain extends Display
{
    protected function header(){}
    protected function body()
    {
        foreach($this->getData() as $key => $value){
            print("{$key} {$value}\n");
        }
    }
    protected function footer(){}
}

class DisplayAsList extends Display
{
    protected function header()
    {
        print("<ul>\n");
    }
    protected function body()
    {
        foreach($this->getData() as $key => $value){
            print("<li>{$key}: {$value}</li>\n");
        }
    }
    protected function footer()
    {
        print('</ul>');
    }
}

新しい表示形式を加えたい場合は、Displayを継承したクラスを作ってあげればよい。

クライアントコード

$data = array(
    'John' => 'banker',
    'Mike' => 'driver',
    'Nick' => 'scientist'
);
$plain = new DisplayAsPlain($data);
$plain->display();
/*
John banker
Mike driver
Nick scientist
*/
$list = new DisplayAsList($data);
$list->display();
/*
<ul>
<li>John: banker</li>
<li>Mike: driver</li>
<li>Nick: scientist</li>
</ul>
*/

PHPでデザインパターン:Adapter(アダプタ)

メリットは「既存のコードを再利用出来ること」である。

■コード

以下の様なクラスがあるとする。

class SoccerPlayer
{
    private $_name;
    public function __construct($name)
    {
        $this->_name = $name;
    }
    public function introduce()
    {
        print($this->_name);
    }
}

以下の様なインターフェイスがあり、メソッドを「getName」で統一したい。

interface Person
{
    public function getName();
}

更に、使用実績があるSoccerPlayerクラスのコードを変更することなく、「getName」でSoccerPlayerクラスのメソッド「introduce」をcallしたい。そんな要望に応えるのがAdapterパターンだ。

継承の場合

継承の場合はコンストラクタの引数は「$name」であり、内部に保持するのも「$name」である。

class SoccerPerson extends SoccerPlayer implements Person
{
    public function __construct($name)
    {
        parent::__construct($name);
    }
    public function getName()
    {
        parent::introduce();
    }
}
クライアント側

継承を使用しているので、当然ながら元のメソッド「introduce」も使えてしまう。

$john = new SoccerPerson('John');
$john->getName();//John
$john->introduce();//John

委譲の場合

継承の場合はコンストラクタの引数は「$name」であり、内部に保持するのは「SoccerPlayerのインスタンス」である。従ってgetNameした時、そのインスタンス経由でintroduceがコールできる。

class SoccerPerson implements Person
{
    private $_person;
    public function __construct($name)
    {
        $this->_person = new SoccerPlayer($name);
    }
    public function getName()
    {
        $this->_person->introduce();
    }
}
クライアント側

委譲(継承ではない)を使用しているので、メソッド「introduce」は使えない。

$john = new SoccerPerson('John');
$john->getName();//John
$john->introduce();//Fatal error

Decorator

■コード

Text.php

interface Text {
    public function getText();
    public function setText($str);
}

PlainText.php

class PlainText implements Text
{
    private $_textString = null;
    public function getText()
    {
        return $this->_textString;
    }
    public function setText($str)
    {
        $this->_textString = $str;
    }
}

TextDecorator.php

内部にテキストオブジェクトを保持し同様のインターフェースを提供する。

abstract class TextDecorator implements Text
{
    private $_text;
    public function __construct(Text $target) {
	    $this->_text = $target;
    }
    public function getText() {
        return $this->_text->getText();
    }
    public function setText($str) {
        $this->_text->setText($str);
    }
}

UpperCaseText.php

class UpperCaseText extends TextDecorator {
    public function __construct(Text $target) {
        parent::__construct($target);
    }
    public function getText() {
        $str = parent::getText();
        $str = strtoupper($str);
        return $str;
    }
}

DoubleByteText.php

class DoubleByteText extends TextDecorator {
    public function __construct(Text $target) {
        parent::__construct($target);
    }
    public function getText() {
        $str = parent::getText();
        $str = mb_convert_kana($str, 'RANSKV');
        return $str;
    }
}
$text = new PlainText();
$text->setText('This is a pen.');
$dbText = new DoubleByteText($text);
var_dump($dbText->getText());//string(42) "This is a pen."
$ucText = new UpperCaseText($text);
var_dump($ucText->getText());//string(14) "THIS IS A PEN."

Compositeパターン

Composite(コンポジット・パターン)を使うと木構造のような再帰的なパターンを表現することができる。

■コード

Component.php

abstract class Component
{
    abstract public function add(Component $branch);
}

Branch.php

枝のクラス。

class Branch extends Component
{
    private $_children;
    public function __construct()
    {
        $this->_children = array();
    }
    public function add(Component $branch)
    {
        return array_push($this->_children, $branch);
    }
}

Leaf.php

葉っぱ。

class Leaf extends Component
{
    public function add(Component $branch)
    {
        throw new Exception('leaves cannot have any leaves');
    }
}

■クライアントコード

$branch = new Branch();
$branch->add(new Leaf());
$branch->add(new Leaf());
$branch->add(new Leaf());

var_dump($branch);
/*
object(Branch)[1]
  private '_children' => 
    array
      0 => 
        object(Leaf)[2]
      1 => 
        object(Leaf)[3]
      2 => 
        object(Leaf)[4]
 */

Interpreter Pattern

言語の文法

<Job> ::= begin <CommandList>
<CommandList> ::= <Command>* end
<Command> ::= diskspace | date | line
interface Command {
    public function execute(Context $context);
}

JobCommand.php

<Job> ::= begin <CommandList>
class JobCommand implements Command {
    public function execute(Context $context) {
        if($context->getCurrentCommand() !== 'begin') {
            throw new RuntimeException();
        }
        $commandList = new CommandListCommand();
        $commandList->execute($context->next);
    }
}

CommandListCommand.php

<CommandList> ::= <Command>* end
class CommandListCommand implements Command {
    public function execute(Context $context) {
        while(true) {
            $currentCommand = $context->getCurrentCommand();
            if(is_null($currentCommand)) {
                throw new RuntimeException();
            }
            else if($currentCommand === 'end') {
                break;
            }
            else {
                $command = new CommandCommand();
                $command->execute($context);
            }
        }
        $context->next();
    }
}

CommandCommand.php

<Command> ::= diskspace | date | line
class CommandCommand implements Command {
    public function execute(Context $context) {
        $currentCommand = $context->getCurrentCommand();
        if($currentCommand === 'diskspace') {
            // 空き容量を計算して出力
        }
        else if($currentCommand === 'date') {
            // 日付出力
        }
        else if($currentCommand === 'line') {
            // 罫線を出力
        }
        else {
            new RuntimeException();
        }
    }
}

Context.php

class Context {
    private $commands;
    private $currentIndex = 0;
    private $maxIndex = 0;
    public function __construct($command) {
        $this->commands = split(' +', trim($command));
        $this->maxIndex = count($this->commands);
    }
    public function next() {
        $this->currentIndex++;
        return $this;        
    }
    public function getCurrentCommand() {
        if(!array_key_exists($this->currentIndex, $this->commands)) {
            return null;
        }
        return trim($this->commands[$this->currentIndex]);
    }
}

■クライアントコード

$job = new JobCommand();
$job->execute(new Context('begin date line diskspace end'));