関数の責任範囲は命名で(ほぼ)決まる
記事の立ち位置
関数の命名のとき考えていることをすこし言語化できたので記録。
関数を作るとき単一責任になるように作るのが重要。
では実際、関数が負うべき責任の範囲はどのように設定すべきなのか分かるか?
熟練のプログラマーならともかく、初学者にはよくわからないのではないか?
感覚を言語化できたので書いてみた。
要約・結論
理想的には関数名 = 役割 = 責任範囲
であるべき。
実際には責任範囲にはコーナーケースや例外処理なども含まれるので関数名だけで説明しきるのは難しいことが多いが、関数名と役割の不一致は大抵おかしいことが起きている。
具体的な名前の関数
文字列が部分文字列を含むか判定する関数を例にする。
public static boolean contains(String str,String searchStr){
...
}
contains = 文字列が部分文字列を含むか判定する
のように処理の内容がそのまま関数名になっている。
この手の関数は以下の特徴があると思っている。
- 処理内容は具体的で分かりやすいが、どういう用途で使われるかは不明。(呼び出し側依存なので)
- シンプルな役割故に利用範囲が大きくなりやすい。
- 普通、仕様変更はないし、あるとしても軽微で前方互換があることが多い。
それゆえ、もし仕様変更したくなったら、別の関数の作成、使用を検討すべき。
関数名=役割という関係が崩れてしまいかねない。
この例で関数の責任範囲を遵守すると(処理がシンプルため責任範囲から逸脱することのほうが難しいかもしれないが)
- 呼び出し側は
文字列が部分文字列を含むか判定する
こと以外の役割を期待してはいけない。 - 関数の作成者、変更者は
文字列が部分文字列を含むか判定する
こと以外の役割を与えてはいけない。
となる。
抽象的な名前の関数
何かしらのメールの宛先人を抽出する関数を例にする。
public static List<User> extractQuxMailRecipients(List<User> user) {
...
}
どんな条件で誰が抽出されるかを関数名で表していない。関数名(=役割)が抽象的になっている。
この手の関数は以下の特徴があると思っている。
- 関数がなにかを抽象化している。
- ビジネスロジックの変更で破壊的変更の可能性がある。
この例で関数の責任範囲を遵守すると
- 呼び出し側はメールの
メールの宛先人を抽出する
こと以外の役割を期待してはいけない。 - 関数の作成者、変更者は
メールの宛先人を抽出する
こと以外の役割を与えてはいけない。
となる。
具体的な名前の関数と異なり仕様が変わりやすいため、この責任範囲を守るというやり方は有効だと思う。最小限の変更で処理を変更できるようになる。
この例で仕様変更を考えてみると
ユーザーの年齢が30歳以下の人を対象にする ⇒ 40歳以上の人を対象にする
と変更してもメールの宛先人を抽出する
という役割は変わっていない。
そのため、呼び出し側の変更は必要ない。
思いもよらない使い方
上記で責任範囲を変えないなら呼び出し側の変更は不要、と述べた。が、多くの場合は呼び出し側も変更が必要になる。関数の責任範囲を誤っている、つまり本来想定されていない、誤った使い方をしているからだ。プログラムを書く主体(人間、AI)が間違えるのは仕方ないことだが、責任範囲を遵守することを目指すべきだと思う。
結論
具体的な名前の関数
と抽象的な名前の関数
では責任の種類が違う。
どちらにせよSRPは遵守しよう。
考え中
具体的な名前の関数
と抽象的な名前の関数
という書き方にしたが、
抽象的な名前の関数
はビジネスロジックを表す関数
と呼んだほうがいいのかも。
具体的な名前の関数
は何と呼べばいいんだろう。
Discussion