メソッドの引数を柔軟にするあれの件

screenshot関数/メソッドのオプションを柔軟に受け渡す - Blog::koyhoge::Tech

を読んで感じたことでも。
やっぱ後から引数追加とかって結構あるんで予めオプション系の引数を増やしやすいようにするわけです。

引用元の例では固定引数が一つとオプション用の引数が渡せるようになっています。

 <?php

 function getItemsByMode($key, $options = null)
 // 固定引数       $key 
 // オプション引数 $options

これだとオプション引数の追加はいくらでも出来ますが、下記のように省略可能な引数を増やしたい場合に困ります。

 <?php

 function getItemsByMode($key, $key2 = null, $options = null)
 // 固定引数       $key 
 // 省略可引数     $key2 
 // オプション引数 $options

で、こういう拡張をできるようにするために僕が良くやるのが引数の最後を常にオプション引数と見なすやり方です。

どういう実装になるかというと、

 <?php

 function getItemsByMode () {
     $args    = func_get_args();
     $options = is_array($args[count($args)-1]) ? array_pop($args) : array();
     $key     = array_shift($args);
     $key2    = count($args) ? array_shift($args) : null;
     
 }
 
 // 以下どのパターンでもOK
 getItemsByMode('key');
 getItemsByMode('key',$opts);
 getItemsByMode('key','key2');
 getItemsByMode('key','key2',$opts);

って感じになりますね。まるでPerlのソースみたいになっちゃいましたね。
いつかのエントリでも書いたんですけどPHPPerlっぽく書くのは嫌われるらしいのであまりやりすぎは良くないとは思いますが、
こういう実装になっていると後で機能拡張がしやすいのは事実なのでどこまでやるか微妙なところです。

さてさてことのついでにもう一つ。
オプション変数($options)についてなんですけど、
想定して無いオプション引数渡たされた場合、例えば、

 <?php

 // mode と書くつもりが moda になってしまっている。
 // でもエラーにはならず、modeにデフォルト引数が適応されてしまう。
 getItemsByMode('key',array('moda' => 1));

こういう場合って元のコードのままだとデフォルト引数が適応されちゃうので意図した動きにならない。
だから想定して無いオプション変数が渡された時にエラーになるようにしといた方が後々のためにも良いかと思われる。

引用元のソースを拡張したとすると、

 <?php

 function array_val(&$data, $key, $default = null) {
     if (!is_array($data)) {
         return $default;
     }
     
     $ret = isset($data[$key])? $data[$key]: $default;
     unset($data[$key]);
     return $ret;
 }
 
 function getItemsByMode($key, $options = null) {
     // オプション解析
     $mode   = array_val($options, 'mode'  , DEFAULT_MODE);
     $limit  = array_val($options, 'limit' , 0);
     $offset = array_val($options, 'offset', 0);
     if( count($options) ){
         // 想定外エラー!!!!
     }
 }

かな。

ハッシュのキーをどんどん削除していくので$optionsは空になってるはず。
最終的に$optionsに要素があったら変なオプション引数が渡されたってことでエラーにするって感じ。

さて最後にこれをPerlで書いた場合の例でも書いておしまいとします。

 sub get_items_by_mode {
     my $options = ref $_[-1] eq 'HASH' ? pop : {};
     my $key     = shift;
     my $key2    = shift;
     
     my $mode   = delete $options->{mode  } || 'default';
     my $limit  = delete $options->{limit } || 'default';
     my $offset = delete $options->{offset} || 'default';
     # ちゃんとやるなら下記のようにexists使った方が良いが大抵は上記のやり方で問題無いかと
     # my $mode = exists $options->{mode} ? delete $options->{mode} : 'default';
     
     if( %$options ) {
         # 想定外エラー!!!
     }
 }

# ちなみにRubyの例も書こうと思ったんだけど、
# いまいち良いコードが書けなかったのでやめました。とほほ。