メソッド引数定義の考え方
メソッド引数は、なかなか定義が難しいですよね。
引数は、メソッド名と並んで外部に公開される「 そのクラスの顔(=インターフェース) 」として振る舞われます。
今回は 引数をどう設計するか の参考になりそうなお話を少し。
用語
- 実装者 : 自分が実装したクラスのこと
- 利用者 : 自分が実装したクラスを実際に使ってもらうクラスのこと
- 要求 : ロジックを実装する時に必要な、外部のインスタンスやパラメータ
基本原則 : メソッド引数による要求数は最小化する
利用者には出来るだけわかりやすい(≒数の少ない)パラメータ要求をするべきです。
とりあえず外から必要な値を全部引数に入れて動かそう、とすると、すぐ引数の数が3, 4, 5, …と増えていきます。
引数は基本的に0個が理想。そして多くても3個程度に留めるのが現実的というのが一般論です。 皆さんは4個以上のメソッドを今までどれだけ作ってきましたか?(ブーメラン)
ここの設計を怠ると、改修時のコストがどんどん高くなってしまいます。
(そもそも引数を5個も6個も要求するのは、そのメソッドの責務=処理内容が重すぎるから、というメソッド分割の話は今回は割愛)
OCP(The Open-Closed Principle)オブジェクトは拡張に対して開いており、修正に対して閉じていなければならない。
SOLID原則の1つですが、拡張は使いやすく、内部の修正を外に波及させない、というものです。引数は特にここに引っかかります。
引数が1つ増えたら、そのメソッドの全ての利用者が修正する必要があります。様々な場所で使われていたら、手がつけられません。
基本原則:コンストラクタで外部要求を完結させる
外部から必要なインスタンスやパラメータを、コンストラクタの段階で全て要求してしまうのは有効な手段です。
$hoge->setFuga($param)
のようにsetterメソッドを用意するのはよくあることですが、そうするとインスタンスのプロパティが可変になり、setterメソッドを呼ぶかどうかで挙動が変わってしまいます。
可変と不変
例えば、「インスタンス化してから $inst->init($a, $b)
を呼んでください」のように要求した場合、
- 利用者が
init
を呼ばないことも出来る -> 全部の内部メソッドで「init
が呼ばれたかどうか」を確認する必要がある init
はいつでも・何度でも呼べる -> 二回呼ぶとデータがおかしくなる場合もあるかもしれない
など、「可変(Mutable)」であることにより面倒を見る場所が増えてしまいます。
しかし、「$inst = new Inst($a, $b)
してください」とすれば、
という「不変(Immutable)」であることによる利点は大きいです。テストもパターンが減るためやりやすくなります。
※もちろん、各メソッドがもらったパラメータを修正しないことが前提の話です。外部からもらったパラメータは変えちゃいけない。 ※ループの中で効率よくインスタンスを使い回したいなど、例外はもちろんあります。
ということで、出来るだけ 不変 (=インスタンス生成後にインスタンスプロパティが変わらない)なクラスを作るのに、コンストラクタ完結が有効ですよ。
さて、基本原則にのっとると、どのようなメソッドが良いでしょうか。実例を2つ挙げてみます。
静的値を渡すメソッドは、静的値ごとにメソッド化
離散値で少数のEnum(booleanやhoge_type in (1, 2, 3) など)を引数に渡すこと、よくありますよね。
外部(DBやリクエスト)の値をそのまま利用する場合は引数にして渡すしかありませんが、
<?php $user->addRole(User::ROLE_ADD);
のような形で、呼び出し時に実装者側の定数を指定していること、ありませんか?
実際に利用者がやりたいことは、上記の例の場合は 追加権限を付与する ではありませんか?
しかし、この引数実装では User::ROLE_ADD権限を付与する と、 利用者がやりたいこと ではなく 実装者(ここでは$user)が実際に欲しいもの を渡してもらっている状態です。
こういった実装はクラスの利用者に 無駄な要求 をしてしまっている状態です。
より分かりやすいインターフェースにするには、 利用者がより分かりやすいメソッドを用意する ことにあります。
<?php $user->addRoleAdd();
これで 追加権限を付与する ということだけを利用者に要求出来ます。 User::ROLE_ADD 定数の外部依存も減ります。
実装側も簡単で、
<?php public function addRoleAdd() { return $this->addRole(self::ROLE_ADD); } public function addRole($role_type) { // 実際の追加処理 }
とラップメソッドを用意するだけです。もしROLE定数が増えても、
<?php public function addRoleDelete() { return $this->addRole(self::ROLE_DELETE); }
と1個メソッドを追加するだけです。もし定数が変更になってもメソッドはそのまま。OCP原則にのっとっています。
別の例として、
<?php $logging = true; $inst->execute($logging); // ... $logging = false; $inst->execute($logging);
のように、boolean値を直接指定していること、ありませんか?
これはローカル変数として名前をつけているので、そのbooleanが何を意味するのか分かりますが、
<?php $inst->execute(true);
もう true
が何をしているのかわからなくなってしまいます。
こういう場合は、
<?php public function executeWithLog() { return $this->exec(true); } public function execute() { return $this->exec(false); } private function exec($logging) { // 実際に何かする if ($logging) { Log::info("hoge"); } }
これでメソッド名を見ればオプション操作がどうなるか分かりますね。
利用者へのbooleanという不要な要求が減ります。
フラグ引数で主要処理を分岐させない
public function hoge($is_fuga) { // なんか共通の処理 if ($is_fuga) { // fugaな時の処理 } else { // fugaじゃない時の処理 } }
具体例がでなくて申し訳ないのですが、引数にtrue/falseとなりうる値を用意して、それぞれに対して処理するのは良くないです。
つまり1つのメソッドに2つ以上の役割をもたせている状態なので、それぞれの条件に対してメソッドを分割するべきです。
switchしてそれぞれ別の処理をしている場合などもそうですね。
先ほどの $logging
はオプション引数だったのでまた別の話です。
いつも基本原則を意識して引数を実装するようにしていくと、自然と使いやすい・テストしやすい・直しやすいクラス設計に近づいていくのではないでしょうか。今回の話が1つの参考になれば幸いです。