コズログ

コズァドットハテナァブログゥドットッッジェーピゥィー

とはいっても多数の引数を一度に渡したい場合があるんだ

長過ぎる引数はコードの匂いです。

ですが、多数の引数を一度に渡したい場合が稀にあります。

PHPの(謎の)arrayをハッシュ的に使うと以下のようになります。

<?php

class ParamsUser
{
    public function piyo(array $params)
    {
        assert(is_string($params['moge']));
        assert(is_int($params['fuga']));
       
        echo $params['moge'];
        echo $params['fuga'];
    }
}

(new ParamsUser)->piyo(['moge'=> 'hello', 'fuga' => 100]);

keyにmogeとfugaを正しい型で定義しておく事前条件があるわけですが、これがメソッドのシグネチャで明示されていないのでクライアント側は戸惑います。

アサーションが書かれていないパターンが最悪です。よくあります。きをつけなはれや!

冴えたやり方

コレに対して最強にCoolなやり方はコレです。

<?php

class ParamsUser
{
    public function piyo(string $moge, int $fuga)
    {
        echo $moge;
        echo $fuga;
    }
}

(new ParamsUser)->piyo('hello',100);

必要なデータと型が明示されています。

・・・ちょ・・まてよ。

多数の引数を一度に渡す場合の話だろ?!

わかっていますとも。でも、まず検討すべきはメソッドの責務が十分に最小化されてて、且つまだ多数の引数が必要なのかということです。

メソッドを分割できるなら分割して引数を減らすべきです。(MUST)

それでも多数の引数を一度に渡したい場合

ですが、しばしばファサードやアダプターを書いていると、そうは言ってられない状況に直面します。

この問題に対して、引数オブジェクトの導入という手法があります。

<?php

class Params
{
    public function __construct(string $hoge, int $fuga)
    {
        $this->hoge = $hoge;
        $this->fuga = $fuga;
    }
    
    public function getHoge(): string
    {
        return $this->hoge;
    }

    public function getFuga(): int
    {
        return $this->fuga;
    }
}

class ParamsUser
{
    public function piyo(Params $params)
    {
        echo $params->getHoge();
        echo $params->getFuga();
    }
}

$params = (new Params('hello', 100));
(new ParamsUser())->piyo($params);

ParamsUserが利用する引数に対しては明示的になりました。

ですが、Paramsクラスのコンストラクタの引数は膨張していきます。

結局のところ、ParamsUserのpiyoが利用する大量の引数をParamsUserのpiyoメソッドから、Paramsのコンストラクタへ移動しただけです。

コンストラクタ引数が増えるのは気持ちが良いものではないですが、Paramsオブジェクトはその名の通り引数を受け渡すオブジェクトなので許容するという考え方もできます。

Setterについて考えてみよう

Setterは基本的に書かない派ですが、ここではParamsにSetterを作る事について考えてみます。

Setterを使えば、コンストラクタの引数を分解できます。

しかしそうすると、今度はParamsオブジェクトの不変条件を満たす事が難しくなります。

特定のSetterを呼び忘れていて、適切な初期値を設定していないとどうなるでしょうか。

適切な初期値が設定されている場合にこの方法が問題になることはあるでしょうか?

私がSetterを書かない理由は状態の変化が発生するからです。

メソッドに渡した後にSetterがコールされて、引数が上書きされるのは悪夢です。

では、イミュータブルなオブジェクトにしたらどうでしょうか。

つまり、Setter内で新たなオブジェクトを返すわけです。

この方法は試していませんが、もしかするとうまくいくかもしれません。

しかし、コスト高すぎませんか?

配列を引数に使いたい場合

引数のためにオブジェクトを定義するコストが高すぎるというケースもあります。

そういった場合には、Constが利用すると良いかもしれません。

<?php
class ParamsUser
{
    const METHOD_PIYO_PARAMS_KEY_HOGE = 'hoge';
    const METHOD_PIYO_PARAMS_KEY_FUGA = 'fuga';
    
    public function piyo(array $params)
    {
        assert(is_string($params[self::METHOD_PIYO_PARAMS_KEY_HOGE]));
        assert(is_int($params[self::METHOD_PIYO_PARAMS_KEY_FUGA]));
        
        echo $params[self::METHOD_PIYO_PARAMS_KEY_HOGE];
        echo $params[self::METHOD_PIYO_PARAMS_KEY_FUGA];
    }
}

(new ParamsUser)->piyo(
    [
        ParamsUser::METHOD_PIYO_PARAMS_KEY_HOGE => 'hoge',
        ParamsUser::METHOD_PIYO_PARAMS_KEY_FUGA => 100
    ]);

Constの名前がかなり冗長ですが、paramsのkeyに関してクライアントが考える必要がありません。

ただし、事前条件をチェックするのはpiyoメソッド側になるので、Constにしてもしなくても本質的には同じです。(一番最初のコードと同じです。)

配列のkeyをConst化するメリットは、keyの変更に強いというところです。

最近のIDEですとConstに対する静的解析が強力で、Const自体の名前を変換するのは容易だからです。(単にハードコードを減らしてるだけです。)

配列を引数に使うメリット

メソッドのシグネチャに全ての引数を展開して定義した場合、クライアント側からは順番の管理が必要になります。

配列をハッシュ的に用いて引数に渡す方法だと、クライアントは順番を気にする必要はありません。

ただし、上記のとおり制約が外れる、もしくは事前条件がわかりにくくなります。

この問題は、他の言語だといい感じに解決できたりします。

リフレクションを使えば、ハッシュで渡された引数をunpackして、メソッドのシグネチャにマップさせる事もできると思いますが、これもコストが高いと思われます。