1.はじめに
正直このタイトルはどうかと思わなくもないのですが、本稿はPHPの可変関数についてのトピックになります。延々投稿を続けているgoogleAssistantから呼び出せるジュークボックス作成について、中心的な部分についてはできあがっているのですが、何しろノンプランアプローチで作成したため見通しが非常に悪い状態です。今後も手を入れて機能拡張するにしても、このまま進めて悪夢化するのは目に見えているので気になっている個所を一旦整理することにしました。
その整理を進める中で可変関数を使った箇所があり、過去の記事を検索しても具体的な応用例として書かれたものはちょっと見当たらなかったので本稿としてまとめることにしました。
2.ビフォー
タイトルにあるように対象となるのはステートマシンとして動作するロジックです。今手掛けているジュークボックスにはシンプルな文字列を受け付けるインターフェースがあります。単純なPLAY, STOP程度なら良いのですが、SAVEやLOADでファイルアクセスを行いたい場合、SAVE(やLOAD)を送信した後にファイル名を送信する、という動きをします。この場合ファイル名は不定文字列として扱わなければならない(コマンドとして解釈してはならない)わけですが、不定文字列の情報としては他に曲目リストの指定などもあります。
ここまで長く書きましたが、要はジュークボックス側の状態によって不定文字列の解釈を変えなければならないわけです。
2.1.具体例
ここでもともとどういう状態だったのかという具体的なコードを出しておきます。正直、こういうものを公開するのは如何なものかという感じですが、生々しくて良いのではないかと開き直っておきます。
case 'LOAD':
$fLoad=true;
break;
case 'SAVE':
$fSave=true;
break;
default:
if($fLoad){
//LOAD list from list.json($strFileName)
$strFileName = $cmd;
if(file_exists($strFileName)){
$obj = json_decode(file_get_contents($strFileName), true);
foreach($obj as $key => $val){
$arrLists[$key] = $val;
}
}
$fLoad = false;
} else if($fSave){
//SAVE list to list.json($strFileName)
$strFileName = $cmd;
file_put_contents( $strFileName, json_encode($arrLists, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE) );
$fSave= false;
} else {
//set playlist from SETLIST($cmd)
$strSetList = $cmd;
if(isset($arrLists[$strSetList])){
$arrPlayList = null;
$arrPlayList = array();
if(isset($arrLists[$strSetList])){
foreach($arrLists[$strSetList] as $item){
if(is_array($item)){
$arrPlayList[] = array("path"=>$item['path'], "dest"=>$item['dest']);
} else {
$arrPlayList[] = array("path"=>$item->path, "dest"=>$item->dest);
}
}
$idxPlay=0;
}
}
}
default:
とあるように、これは渡された文字列をまずswitch~caseで特定のコマンドかどうかを判定して、最後に不定文字列の処理としてif~elseの塊で終末処理をしています。良く読めばフラグで制御を切り替えていることが読み取れるとは思いますが、この調子で機能拡張を続けていくとメンテナンスがクリティカルな作業になっていくのは目に見えています。
3.アフター
if~elseの塊でやりたいことは、不定文字列を渡された時の状態によって文字列の取り扱いを変えることです。感覚的にはフラグで呼び出す関数を切り替えるような動きをさせるわけですが、で、あれば、フラグではなくて不定文字列を処理する関数そのものを状態として持たせても良いはずです。つまり、可変関数を利用します。
3.1.実装方針
プログラムファイル全体の見通しを良くする目的もあったので、ジュークボックスのリストを取り扱う機能をクラス化し、そのクラスがステートマシンとして振る舞うような造りとします。コーディングは以下のようになりました(抜粋)。
private function md_loadList($strFileName){
//LOAD list from list.json($strFileName)
if(file_exists($strFileName)){
$obj = json_decode(file_get_contents($strFileName), true);
foreach($obj as $key => $val){
$this->arrLists[$key] = $val;
}
}
$this->strMode = "";
}
private function md_savelist($strFileName){
//SAVE list to list.json($strFileName)
file_put_contents( $strFileName, json_encode($this->arrLists, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE) );
$this->strMode = "";
}
public function noncommand($cmd){
$strfunc = "md_".$this->strMode;
if( method_exists( $this, $strfunc) ){
echo "Call ".$strfunc."\n";
$this->$strfunc($cmd);
} else {
$this->md_chengeList($cmd);
}
md_
ではじまるのが可変関数として呼び出される内部的なメソッドで、noncommand
メソッドが呼ばれた時にオブジェクトの内部変数strMode
に設定された文字列により実際に呼ばれるメソッドが決まります。method_exists()
関数により、実装されていないメソッドが指定されている(あるいはそもそも指定されていない)場合はmd_changeList
メソッドを実行するような動きをさせています。
3.2.まあ、どうしたことでしょう
前述するクラスを作成したことで、先ほどのdefault:
は以下のように書き直すことができました。
case 'LOAD':
$objMsv->strMode = "loadList";
break;
case 'SAVE':
$objMsv->strMode = "saveList";
break;
default:
$objMsv->noncommand($cmd);
}
$objMsv
の内部変数strMode
に状態を設定した後に受け取った不定文字列はnoncommand
メソッドに渡され、noncommandメソッド内部でしかるべき関数にディスパッチされます。
一見簡略化されたように思えますが、これは単にコーディングブロックを他所に飛ばしただけで、ロジックそのものが単純化されたわけではありません。ただ、switch~case
の動きとしては把握しやすくなっていると思います。
4.おわりに
if~elseの塊のままにするよりはクラス内部で処理ブロックがメソッドとして分離した方がメンテはしやすくなりました。ぶっちゃけ探しやすくなりました。
ただ、この方式だとオブジェクト内部のプライベート関数名が外部に露出していることになるのでもうちょっとうまい書き方を探した方が良いかもしれません。可変関数の使い方としては無理にクラスに押し込まなくても良いので、そこは作り方次第という気もしています。