PHPで使うGoFパターン ひとり Advent Calendar - テンプレートメソッド


お詫び
--
Advent Calendar なのに投稿しない日が出来てしまいました。申し訳ないです。このタイトルは不適切になってしまいましたね。リリース前でアレしていました。


テンプレートメソッドって?
--
これはさすがに日本語で読むよりもコード読んだ方が早いです。以下は良く有るWebアプリの処理の流れをテンプレートメソッドパターンにして実装したものです。 GETにせよ、POSTにせよ、validateがNGならエラーを返す、validateがOKならview用のHTMLを返すの流れが基本です。基本の流れをテンプレートメソッドにして、vaildate, doMainを子クラスで実装するという仕組みです。

テンプレートメソッドの構造
--

* 抽象クラス - 基本的な流れを実装します。流れの中で abstractなメソッドを利用します
* 具象クラス - 抽象クラスのabstractなメソッドを実装します。基本的な流れを気にしないで良いです。

```
<?php
function say ( $l ) { print "$l\n"; }

//
// テンプレートメソッド
//
abstract class AbstractAction {
  private $_Request   = null;
  private $_Session   = array();
  private $_Assigned  = array();
  private $_Errors    = array();
  private $_ErrorPage = 'error.view';

  // これを子クラスで実装する
  abstract function validate ();
  // これを子クラスで実装する
  abstract function doMain ();

  // validateしてエラーページを返すか
  // 処理を続行するだけなので
  public function doAction () {
    if ( !$this->validate () ) return $this->_ErrorPage;
    return $this->doMain ();
  }

  public function __construct () { }
  public function assign ( $key, $val ) { $this->_Assigned[$key] = $val ; }
  public function getAssigned ( $key ) { return $this->_Assigned[$key]; }
  public function getRequestValue ( $key ) { return $this->_Request[$key]; }
  public function setSessionValue ( $key, $val ) { $this->_Session[$key] = $val; }
  public function getSessionValue ( $key ) { $this->_Session[$key]; }

  public function setRequest ( $request ) {
    if ( !is_array ( $request ) ) throw new Exception ( '配列じゃないもん' );
    if ( $this->_Request )        throw new Exception ( '既に設定されているもん' );
    $this->_Request = $request;
  }

  public function setSession ( $session ) {
    if ( !is_array ( $session ) ) throw new Exception ( '配列じゃないもん' );
    if ( $this->_Session ) throw new Exception ( '既に設定されているもん' );
    $this->_Session = $session;
  }

  public function setError ( $key, $val ) { $this->_Errors[$key] = $val; }
  public function getError ( $key )       { return $this->_Errors[$key]; }
  public function hasError ()             { return count ( $this->_Errors ); }
  public function getErrors ( $key )      { return $this->_Errors; }
  public function getModel ( $key )       { return $this->_ModelFactory->create ( $key ); }
  public function setModelFactory ( $obj ) {
    if ( !( $obj instanceof ModelFactory ) ) throw new Exception ( 'こんなオブジェクト無理だもん' );
    $this->_ModelFactory = $obj;
  }
}

//
// /user/index
//
class UserIndexAction extends AbstractAction {
  public function __construct () { }
  //
  // 絶対に見せるのでanything ok
  //
  public function validate () {
    return true;
  }

  //
  // 何もしないでviewを返すだけ
  //
  public function doMain () {
    return 'index.view';
  }
}


//
// /user/entry ( フォームからPOSTされてくるイメージ )
//
class UserEntryAction extends AbstractAction {
  protected $_ErrorPage = 'redirect: /UserForm';
  public function __construct () { }

  public function validate () {
    if ( !$this->getRequestValue ( 'name' ) ) {
      $this->setError ( 'name', 'not_required' );
    }

    if ( !$this->getRequestValue ( 'mail_address' ) ) {
      $this->setError ( 'mail_address', 'not_required' );
    }
    // この正規表現は適当過ぎるので真似しちゃダメね
    elseif ( !preg_match ( '#[^@]+\@[^@]+#', $this->getRequestValue ( 'mail_address' ) ) ) {
      $this->setError ( 'mail_address', 'invalid_format' );
    }
    return !$this->hasError();
  }

  public function doMain () {
    $user               = $this->getModel ( 'User' );
    $user->name         = $this->getRequestValue ( 'name' );
    $user->mail_address = $this->getRequestValue ( 'mail_address' );
    $user->save();
    $this->assign ( 'User', $user );
    return 'redirect: /UserFinished';
  }
}

class ModelFactory {
  public function __construct () { }
  public function create ( $class ) {
    // 本当はrequire したりしてください
    return new $class;
  }
}

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

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

  public function save () {
    return true;
  }
}

//
// indexが普通に走るテスト
//
$index = new UserIndexAction ();
$index->setModelFactory ( new ModelFactory () );
say ( $index->validate() == true         ? 'OK' : 'NG' );
say ( $index->doMain()   == 'index.view' ? 'OK' : 'NG' );
say ( $index->doAction() == 'index.view' ? 'OK' : 'NG' );

//
// Entryがvalidateで引っかかるテスト1
//
$entry = new UserEntryAction ();
$entry->setModelFactory ( new ModelFactory () );
say ( $entry->validate()                 == false  ? 'OK' : 'NG' );
say ( $entry->getError( 'name' )         == 'not_required'  ? 'OK' : 'NG' );
say ( $entry->getError( 'mail_address' ) == 'not_required'  ? 'OK' : 'NG' );
//
// Entryがvalidateで引っかかるテスト2
//
$entry = new UserEntryAction ();
$entry->setModelFactory ( new ModelFactory () );
$entry->setRequest ( array ( 'name' => 'hoge' ) );
say ( $entry->validate() == false  ? 'OK' : 'NG' );
say ( $entry->getError( 'mail_address' ) == 'not_required'  ? 'OK' : 'NG' );

//
// Entryがvalidateで引っかかるテスト3
//
$entry = new UserEntryAction ();
$entry->setModelFactory ( new ModelFactory () );
$entry->setRequest ( array ( 'name' => 'hoge', 'mail_address' => 'hoge' ) );
say ( $entry->validate() == false  ? 'OK' : 'NG' );
say ( $entry->getError( 'mail_address' ) == 'invalid_format'  ? 'OK' : 'NG' );

//
// Entryがvalidateで引っかかるテスト
//
$entry = new UserEntryAction ();
$entry->setModelFactory ( new ModelFactory () );
$entry->setRequest ( array ( 'name' => 'hoge', 'mail_address' => 'test@test.test' ) );
say ( $entry->validate() == true   ? 'OK' : 'NG' );
say ( $entry->doAction() == 'redirect: /UserFinished' ? 'OK' : 'NG' );

$user = $entry->getAssigned ( 'User' );
say ( $user instanceof User ? 'OK' : 'NG' );
say ( $user->name == 'hoge' ? 'OK' : 'NG' );
say ( $user->mail_address == 'test@test.test' ? 'OK' : 'NG' );


// Webで実行する時は、このActionをURLにしたがって、動的にローディングして
//   $action = new class(); 
//   $class->setRequest ( $_REQUEST );
//   $class->setSession ( $_SESSION );
//   $view = $class->doAction ();
//   $_SESSION = $this->getSession();
// とかして、
// 戻り値を元にviewをレンダリングしたりエラーページを表示したり、
// jsonでリクエストされたら Assigned を jsonにエンコードしたり
// 色々すると良いと思います
```

どういう時に使うの?
--
決まりきった手順(テンプレートメソッド)があって、ちょっとだけ違うような処理がいっぱい有る時に使います。ヘッダーを書いて、本文を書いて、フッターを書いて とか、絞り込んで、リストを抽出して、リストを出力するんだけど、リストのフォーマットが違うとか。

実際どうなの?
--
便利です。実装するのは。若干、抽象クラスのテストが面倒です。抽象クラスをテストするためのテスト用の具象クラスを用意してテストすることが出来ます。ハマり始めると具象クラスをいっぱい作るハメになって凄く微妙な状態になったりするので、ご利用は計画的に。