PHPで使うGoFパターン ひとり Advent Calendar - プロトタイプ


最終回です

プロトタイプってなに?
--
原型となるオブジェクトをcloneするパターンです。生成の手間が楽になりますね。他の生成に関するパターンと組み合わせることで真価を発揮します。例えば、巨大なテーブルを表現するORマッパの場合、既定値をある程度突っ込んだプロトタイプをフライウェイト内に保持しておき要求がある度、クローンして返すようにするなどなど。ところで、実はPHPには clone が 言語機能としてあります。

```php
<?php
class Prototype {
  private $_Values = array (); 
  public function __set ( $key, $val ) { 
    $this->_Values[$key] = $val;
  } 
  public function __get ( $key ) { 
    return $this->_Values[$key];
  }
}
function say ( $s ) { print "$s\n"; }

$obj = new Prototype();
$obj->hoge = 'fuga';
$obj2 = clone $obj;
say ( $obj->hoge == $obj->hoge ? 'OK' : 'NG' );
$obj2->hoge = 'piyo';

say ( $obj->hoge == $obj2->hoge ? 'NG' : 'OK' );
say ( $obj->hoge == 'fuga'  ? 'OK' : 'NG' );
say ( $obj2->hoge == 'piyo'  ? 'OK' : 'NG' );
```

ただ、これはディープコピーに見せかけて、そうでも無いのです。

```php
<?php
class Prototype {
  private $_Values = array ();
  public function __set ( $key, $val ) {
    $this->_Values[$key] = $val;
  }
  public function __get ( $key ) {
    return $this->_Values[$key];
  }
}
class Child {
  private $_Values = array ();
  public function __set ( $key, $val ) {
    $this->_Values[$key] = $val;
  }
  public function __get ( $key ) {
    return $this->_Values[$key];
  }

}
function say ( $s ) { print "$s\n"; }

$obj = new Prototype();
$obj->hoge = 'fuga';
say ( $obj->hoge == $obj->hoge ? 'OK' : 'NG' );

$obj2->hoge = 'piyo';
say ( $obj->hoge == $obj2->hoge ? 'NG' : 'OK' );
say ( $obj->hoge == 'fuga'  ? 'OK' : 'NG' );
say ( $obj2->hoge == 'piyo'  ? 'OK' : 'NG' );


$obj->Child = new Child();
$obj->Child->xyzzy = 'vim';
$obj2 = clone $obj;
say ( $obj->Child->xyzzy == $obj2->Child->xyzzy ? 'OK' : 'NG' );
$obj->Child->xyzzy = 'emacs';
say ( $obj->Child->xyzzy == $obj2->Child->xyzzy ? 'OK' : 'NG' );
```

ディープコピーが明示的に必要な場合は以下のようにします。

```
<?php

class Prototype {
  private $_Values = array ();
  public function __set ( $key, $val ) {
    $this->_Values[$key] = $val;
  }

  public function __get ( $key ) {
    return $this->_Values[$key];
  }

  /** ディープコピー **/
  public function __clone () {
    foreach ( $this->_Values as $key => $val ){
      if ( is_object ( $val ) ) $this->$key = clone $val;
      else                      $this->$key = $val;
    }
  }
}
class Child {
  private $_Values = array ();
  public function __set ( $key, $val ) {
    $this->_Values[$key] = $val;
  }
  public function __get ( $key ) {
    return $this->_Values[$key];
  }

}
function say ( $s ) { print "$s\n"; }

$obj = new Prototype();
$obj->hoge = 'fuga';
say ( $obj->hoge == $obj->hoge ? 'OK' : 'NG' );

$obj2->hoge = 'piyo';
say ( $obj->hoge == $obj2->hoge ? 'NG' : 'OK' );
say ( $obj->hoge == 'fuga'  ? 'OK' : 'NG' );
say ( $obj2->hoge == 'piyo'  ? 'OK' : 'NG' );

$obj->Child = new Child();
$obj->Child->xyzzy = 'vim';
$obj2 = clone $obj;
say ( $obj->Child->xyzzy == $obj2->Child->xyzzy ? 'OK' : 'NG' );

$obj2->Child->xyzzy = 'emacs';
say ( $obj->Child->xyzzy  == 'vim' ? 'OK' : 'NG' );
say ( $obj2->Child->xyzzy == 'emacs' ? 'OK' : 'NG' );
```

実際どうなの?
--
非常に便利です。自分ひとりで扱う分には。いきなりネガティブな意見でアレなのですが、cloneする場所を限定的にするとかしないとバグった時に理由が分かり難かったりしてハマります。故に僕は、このパターンに対してちょっとネガティブです。自分ひとりでやっている分には良いのですが、多人数が絡むとやりたい放題やる人も居るので危いです。ある程度の規模になったらcloneキーワード自体を禁止したりする方が安全かも知れません。とは言え、そんなことでは何も解決しないのでcloneする場所をファクトリメソッド内だけにして「本当に原型」を作るだけにする。オブジェクトをやたらめったらコピーさせないなど、決まりを作って運用するのが妥協点でしょうか。