switch文を使ってはいけない

Posted by Hiraku on 2014-08-14

Qiitaで書いた内容なんですが、PHPのswitch文は悪名高い「==」演算子で比較を行います。

<?php
switch (true) {
    case 0:
        echo '数字の0';
        break;
    case '0':
        echo '文字列の0';
        break;
    case '0.0':
        echo '文字列の0.0';
        break;
    case true:
        echo '真偽値のtrue';
        break;
}

このコードは「文字列の0.0」を出力します。大変分かりにくいですね。

この点はif ... elseif ...を使えば解決するんですが、switchで書きたくなるようなコードをelseifにするとおそらく読みにくくなるでしょう。

ではどうするか。 正解はオブジェクトのポリモーフィズム(多態性)機能を使うことです。

といっても、多態性で調べて出てくる記事とか書籍に関しては抽象的な説明が多いので、調べるときは「Stateパターン」とか「Strategyパターン」とかで検索した方がわかりやすいでしょう。

switchからState/Strategyへ

Stateパターンそのものの説明はTECHSCOREの記事が大変わかりやすいのでそちらを参照してください。 ちなみにStrategyパターンは用途が違うだけで、クラス構造としてはStateパターンと全く同じですので、ここではStateパターンのことだけ書きます。

↓こういうswitch文を見て、

//...
switch ($state) {
    case STATE_1:
        doSomething1();
        break;
    case STATE_2:
        doSomething2();
        break;
    case STATE_3:
        doSomething3();
        break;
}

そもそも↓こう1行で書けるようにしておけ、というのがStateパターンの趣旨です。

//...
$model->doSomething();

種明かしのためにベタベタ書いてしまうとこんな感じ。

//実際はファクトリ的なところで作り分ける
switch ($state) {
    case STATE_1:
        $model = new State1Model;
        break;
    case STATE_2:
        $model = new State2Model;
        break;
    case STATE_3:
        $model = new State3Model;
        break;
}

//...(中略)

$model->doSomething();

switch文が書きたくなる場所の遥か前で、あらかじめ$modelオブジェクトを作り分けておくのです。doSomethingメソッドはクラスごとに定義を変えてあって、switch文と同等のロジックが$model->doSomething();だけで実現できる、というわけです。

結局はどこかで条件分岐させているので、switchやif elseを書いているのですが、この書き方の方が優れているからこそGoFデザインパターンになっているのです。以下、Stateパターンのメリットをまとめてみましょう。

Stateパターンにした方がいい理由

デザインパターンは「経験的にこのような書き方をした方がよい」というものなので、あくまで統計とか経験の話になります。

メリット1. 条件分岐をDRYにできる≒ソースコードが短くなる

「何かの状態によってswitchで処理を振り分ける」というコードは、経験的にプログラム中に頻繁に登場します。

毎回switch文を書いていると、「この条件分岐、前もどこかで書いたな…」ってことになりがちで、全然DRY(Don't Repeat Yourself)な状態になりません。

<?php
class ActionController
{
    function hogeAction()
    {
        switch ($this->state) { ... }
    }

    function fooAction()
    {
        switch ($this->state) { ... }
    }

    function mooAction()
    {
        switch ($this->state) { ... }
    }
}

これをStateパターンにすると、オブジェクトを生成する場所であらかじめ条件分岐をしておけば、あとはswitch文を書かなくてよくなります。

<?php
class ActionController
{
    function hogeAction()
    {
        $this->model->doSomething1();
    }

    function fooAction()
    {
        $this->model->doSomething2();
    }

    function mooAction()
    {
        $this->model->doSomething3();
    }
}

条件分岐ロジックをDRYにした感じです。

「関数化するのと何が違うの?」と疑問に思う人がいるかもしれません。Stateパターンの場合、純粋に「条件分岐」だけをまとめている点が異なります。

switch ($state) {
    case STATE_1:
        doSomething();
        break;
    case STATE_2:
        //なにもしない
        break;
}

switch ($state) {
    case STATE_1:
        hogehoge1();
        break;
    case STATE_2:
        hogehoge2();
        break;
}

この2つのswitch文は、条件分岐部分は全く同じ構造ですが、分岐後のコードが違うため、単純に関数化することはできません。

しかしStateパターンであれば、まとめられます。

<?php
// 予め$modelは状態によって作り分けておく
$model->doSomething();
$model->hogehoge();

私個人の感覚として、100行のコードを10行にすることはできませんが、1万行のコードを1000行に抑えるぐらいの効果はある、と思っています。

メリット2. 仕様変更に強くなる

もしswitch文をあちこちにべた書きしていると、「状態が増えた」という仕様変更の際、全switch文をしらみつぶしに探して書き換えていく作業が発生します。

しかしStateパターンの場合、switch相当の箇所はオブジェクトを作り分ける一か所に絞られていますので、新しい状態に対応するクラスを作り、作り分け部分を修正するだけでよくなります。

あちこちに散らばっているのは、$model->doSomething();みたいなコードなので、そのままでも仕様変更に対応できるのです。

元のコードには一切手を触れなくても変更可能ということは、既存コードを壊すことは絶対にないということです。

変更に閉じて、拡張に開いている。

これは開放/閉鎖原則(OCP)にも従っていて、保守性が高くなると言えるのではないでしょうか。

メリット3(おまけ) マサカリとして使える

switch相当の構文がもっと高機能で高級な言語というのも存在します。

そういう言語を例に挙げて、PHPのswitch文が原始的過ぎるとDISってくる(break必須とかwww、など)人がたまーにいるんですが、 正直めんどうくさい ので、そういう輩に対して投げ返すマサカリとしてはStateパターンがいいんじゃないかと思います。比較的言語に依存しないテクニックですので。

「あぁ?switch使ってるお前の方がゴミだよ」



雑感

状態を属性ではなく型で表現する ーStateパターンこそテクニックとしてのオブジェクト指向の真骨頂みたいな感じなので、使いこなせるようになるとかっこいいですね。

ただ、「100行を10行にする能力はない」と書いた通り、もともと1回しか登場しないswitchをStateパターンで書いても別に短くならないので、まずはswitchで書いておいて、あとでリファクタリングしていくことを考えた方がいいのかもしれません。

例の記事について

switchのことを書きたくなったのはまあ、例の記事がきっかけです。

はてなブックマーク - 保守性・管理性が劇的に上がるPHPのスマートなコードの書き方12選 | BULK SERVER blog

他の方も書かれてますが、だいたいこの4つを参考にしていれば、PHPerとして道を踏み外すことはない、と思います。

個人的にはPSR-2は割と破って書いていたりするんですが、PSR-1あたりは守って当たり前の内容だと思います。

おまけ(PHPをDISりたい人向け)

switch文とPHPと言えば、最近は「default節を複数書いてもsyntax errorにならない」という挙動について、誰得ということで修正される方向で決まったようです。

<?php
//PHP5.5.15ではsyntax errorにならないコード

switch (true) {
    default:
        echo 'default1';
        break;

    default:
        echo 'default2';
        break;
}

PHP: rfc:switch.default.multiple

よかったですね。

PHPの最新記事