PHPで使うGoFパターン ひとり Advent Calendar - ステート


ステートってなに?
--
状態によって振舞が違うよねっていうことです。パターンとしてはストラテジーの発展形とも言えるのかなと思います。使いようによっては適用出来る箇所が実に多いパターンだと思いますので、というよりは、これを使わないせいでカオスになっているコードとかよく見かけるので覚えておくと良いと思います。

ステートの構造
--

* IState - 状態インターフェース
* Context - 状態を持つオブジェクト

つまり、状態自身がオブジェクトで振舞を持ち、状態によって振舞が違う箇所の処理を状態に移譲するというパターンです。以下のコード例はブログの記事クラスだと思ってください。下書き中は他の人に見せたくない。そもそもクローズしてたら見せたくない。みたいな感じ

```
<?php
interface IArticleState {
  public function showArticle ( $article ); 
  public function canShow ( $member ); 
  public function isPublished (); 
}

class Article {
  private $_Subject = null;
  private $_Body = null;
  private $_State = null;

  public function __construct ( $id ) {
    $article =  $this->find ( $id );
    $this->_Subject = $article['sbj'];
    $this->_Body    = $article['body'];

    // 生成はもっと工夫すると良いと思います
    if     ( $article['state'] == 1 ) $this->_State = new ArticleStateOpen ();
    elseif ( $article['state'] == 2 ) $this->_State = new ArticleStateClosed ();
    elseif ( $article['state'] == 3 ) $this->_State = new ArticleStateDraft ();
    else                              throw new Exception ( 'そんなステートありません -> { id:' . $id . ',state:' . $article['state'] );
  }

  public function getState () {
    return $this->_State;
  }
  public function getSubject () {
    return $this->_Subject;
  }

  public function getBody () {
    return $this->_Body;
  }

  public function showArticle () {
    return $this->_State->showArticle ( $this );
  }

  public function canShow ( $member ) {
    return $this->_State->canShow ( $member );
  }

  public function isPublished () {
    return $this->_State->isPublished ();
  }

  //DBからデータ取ってくるイメージで
  public function find ( $id ) {
    $data = array (
      'id-1' => array ( 
        'sbj' => 'article1_subject',
        'body' => 'article1_body',
        'state' => 1,
      ),
      'id-2' => array ( 
        'sbj' => 'article2_subject',
        'body' => 'article2_body',
        'state' => 2,
      ),
      'id-3' => array ( 
        'sbj' => 'article3_subject',
        'body' => 'article3_body',
        'state' => 3,
      ),
    );
    $result = $data['id-' . $id];
    if ( !$result ) throw new Exception ( 'そんな記事はありません id:' .  $id );
    return $result;
  }
}

class ArticleStateOpen implements IArticleState {
  public function showArticle ( $article ) {
    return $article->getSubject () . "\n\n".
       $article->getBody () . "\n";
  }
  public function canShow ( $member ) {
    return true;
  }

  public function isPublished () {
    return true;
  }
}
class ArticleStateClosed implements IArticleState {
  public function showArticle ( $article ) {
    return 'この記事は非公開です';
  }

  public function canShow ( $member ) {
    return false;
  }
  public function isPublished () {
    return false;
  }
}

class ArticleStateDraft implements IArticleState {
  public function showArticle ( $article ) {
    return $article->getSubject () . "(下書き中)\n\n".
       $article->getBody () . "\n";
  }

  public function canShow ( $member ) {
    return $member == '本人です';
  }

  public function isPublished () {
    return false;
  }
}
function say ( $s ) { print "$s\n"; }

$opened = new Article ( 1 );
say ( get_class ( $opened->getState() )  == 'ArticleStateOpen' ? 'OK' : 'NG' );
say ( $opened->canShow ( '本人です' ) == true ? 'OK' : 'NG' );
say ( $opened->canShow ( '本人じゃありません' ) == true ? 'OK' : 'NG' );
say ( $opened->isPublished () == true ? 'OK' : 'NG' );
say ( $opened->showArticle () == "article1_subject\n\narticle1_body\n" ? 'OK' : 'NG' );

$closed = new Article ( 2 );
say ( get_class ( $closed->getState() )  == 'ArticleStateClosed' ? 'OK' : 'NG' );
say ( $closed->canShow ( '本人です' ) == false ? 'OK' : 'NG' );
say ( $closed->canShow ( '本人じゃありません' ) == false ? 'OK' : 'NG' );
say ( $closed->isPublished () == false ? 'OK' : 'NG' );
say ( $closed->showArticle () == "この記事は非公開です" ? 'OK' : 'NG' );

$draft = new Article ( 3 );
say ( get_class ( $draft->getState() )  == 'ArticleStateDraft' ? 'OK' : 'NG' );
say ( $draft->canShow ( '本人です' ) == true ? 'OK' : 'NG' );
say ( $draft->canShow ( '本人じゃありません' ) == false ? 'OK' : 'NG' );
say ( $draft->isPublished () == false ? 'OK' : 'NG' );
say ( $draft->showArticle () == "article3_subject(下書き中)\n\narticle3_body\n" ? 'OK' : 'NG' );
```

ステートパターンを適用すべきか継承すべきか?
--
慣れてくるとステートパターンで実装するべきか継承してサブクラスにすべきかというのが悩みどころになってくると思います。オブジェクト指向の教科書的回答をするのであれば、それは「状態」なのか「種別」なのかで判断するのが良いと思います。現実的な回答をするのであれば「親クラスの変更の影響を受けたいかどうか」という判断で良いと思います。「状態」という言葉だけに惑わされず、移譲で使う上手いポリモーフィズムの実現方法だと考えた方が幸せだと思います。勿論、出自が「状態」で利用するポリモーフィズムなので「状態」による振舞の違いを吸収するのに便利です。