はじめに
私はコーディング時に関数・メソッドの行数は短く保つように心がけています。
関数・メソッド行数を短く保つことにはいくつかのメリットがあるので、今回は私が感じているそのメリットについてまとめてみたいと思います。
小さいものは美しい
UNIXの哲学の一つに『小さいものは美しい』という考え方があります。
それぞれを機能毎に分け、一つ一つを小さく保つと
- 単純化する
- 組み合わせやすい
- 保守しやすい
などのメリットが生まれるというものです。
これは、コーディングのレイヤーに落とし込んでも通用する考え方で、関数・メソッドにも通用するのではないでしょうか。
こちらのメリットについては、この後掘り下げて行きたいと思います。
どうやって関数を分けるか
関数・メソッド化の指標ですが、単一責任原則に則って『いい塩梅に』(← やり過ぎ注意という意味で)関数を分けていきます。
単一責任原則の説明は自分のこちらの記事にまかせて、関数やメソッドに関しても単一責任原則を意識して分けていくとキレイに関数を分けていけると思っています。
長い関数を見たときに、この関数にメインにやらせたいことは何かを考えると分けるべきところが見えてきます。
関数を短く保つことのメリット
単純化する
言い換えると複雑度が下がるとも言えるでしょう。
単純化することによって、下記のようなメリットが生まれます。
- 1つ1つの問題が簡単になる
- 読み手にこの関数は何をしたいものか伝わりやすい
- 変数のスコープが短くなる
- 処理のまとまりに名称がつく
1つ1つの問題が簡単になる
大きな1つの問題を小分けにしていくと問題が1つ1つ単純で、それを1つ1つ解決していくことで簡単にクリアできるというのがあると思います。
コーディングでも同じことが言え、1つ1つの問題が単純だと機能実装しやすくなりますし、実際に出来上がったコードもかなり単純化されていきます。
読み手がこの関数が何をしたいものか伝わりやすい
1つの単純な問題を解決するコードはとても読みやすいものになります。
読み手とはチームの新卒かもしれませんし、2週間後の自分である場合もあります。
単純さを保つことは理解の助けに大きく寄与します。
変数のスコープが短くなる
変数のスコープが長いと、あっちこっちで同じ変数が使われていて修正が難しくなるパターンがあります。
例えば物語の前半で入れた修正が、物語の後半に影響が無いかチェックするのが辛くなり、これが長編の物語になるほどチェックが難しくなります。
処理のまとまりに名称がつく
これは個人的にですが、
// startAtが今日より前で、 expiredAtが今日より後で、 かつstatusが3で...
if ($sample['startAt'] <= $now && $sample['expiredAt'] > $now && $sample['status'] == 3) {
// なんか処理する...
}
などの判断をif文の1行で書くのもいいですが(コメントがついていれば...でもコメントの場合だとコメント自体がメンテナンスされてない場合もありますしね)、
それを関数にまとめて関数化して再利用可能にしたうえで + 処理に名前がつくと色々と捗ります(可読性とかも)。
(このifでいろいろ日付とかチェックしているのはつまりどういうことをチェックしたいの? → isEditable()にまとめとくとか)
// 関数化
if (isEditable($sample)) {
// なんか処理する...
}
// または$sampleをオブジェクトにしたりしてメソッド化
if ($sample->isEditable()) {
// なんか処理する...
}
// (...まぁ、場合によっては$sample['isEditable']とかの変数自体を用意してもいいかもしれないですけど)
後述の『下位の問題の抽出』と併せてメリットが大きくなります。
組み合わせやすい
- 下位の問題の抽出
- 機能追加しやすい
- 改修が行いやすくバグに繋がりにくい。
下位の問題の抽出
リーダブルコードという本で言うと無関係の下位の問題の抽出という項目に近いです。
問題とは『メインで解決したい問題』と、『それを解決するために付帯する下位な問題』に分けることが出来るかと思います。
例えばファイルの中身を全部読み込みたいという目的があった場合、PHPではfile_get_contents()などの関数がありますが、無いような言語の場合、ファイルサイズ分のバッファを用意してファイルサイズ分読み込んで〜など目的を解決するための下位な問題が発生します。
こういった下位の問題を関数化して抽出します。ユーティリティ関数として全体から使えるようにするのも良いでしょう。
メインの処理部分ではこういった下位の問題を意識せずに集中できるようにして肥大化を抑えることもできます。
機能追加しやすい
追加する際はある程度は新しいコードを追加する事になりますが、すでにある部分に関しては予め機能、役割ごとに分けられたものを組み合わせる作業になります。
上記のように役割ごとに分けられていたり、下位の問題などの再利用性が保たれていたコードではコピペコードの発生をある程度抑えることができます。
保守しやすい
基本的に関数、メソッド単位で単体テストが行われると思うのですが、問題が単純化されているコードは修正が容易になります。
また、長い関数よりかは短い関数にまとまっている方が修正する側は手を入れやすいはずです。
上手く単一責任原則で関数が別れていると短い関数のin(引数)とout(返り値)を保証して修正するだけになるので遥かに楽です。
よくある質問
関数・メソッドで分けるとかえって処理が長くなる
関数化しても全体の長さは変わらない or むしろ長くなるじゃないかとも言われるかもしれませんが、
そうったパターンは確かにあります。ただ、本当に役割ごとに分けられているコードの方が小さくなる事が多いです。
デグレの度合いで言うと関数・メソッド毎でinとoutが保証されていて連なっている方が抑えられたりと、特に単体テストがしっかり行われていたり、静的型付け言語の場合特にメリットが大きくなりますが、そうでない場合でも効果は着実にありますし、メリットの寄与のほうが大きいと思います。
関数・メソッドが多くて処理を追うのが大変
全部の関数・メソッドを追う必要はないと思います。
下位の問題の処理はわざわざ深追いする必要は無いですし、問題があるケースの調査をするならば関数の引数と返り値をチェックしていって問題がなければ読み飛ばしてもいいパターンが多いです。
実装スピードについて
関数・メソッドで分けて設計している場合のほうが、分けないで実装する場合に比べると初回リリースまでの実装スピードは遅い場合が多いです(個人的な今までの経験則)。
ただしそれは瞬発力的な速さであり、長いスパンで見て機能追加したり修正したり、共通化として切り出していった場合にが不具合が出やすかったりします。
常に実装スピード優先で実装するプロダクトは、何処かで見直さない限りは後々に問題になってるパターンが多いですし、見直す前提で作っても大抵見直さないことが多いと思います。
(ただ、本当に瞬発力が求められる場合などもあるかと思いますので、柔軟に対応できるようにやり方としては持っておいてもいいとは思います。)
終わりに
これが基本的に守られていれば例えばコントローラのアクションがファットになったり、
秘伝のタレ化といったものは『ある程度』改善されるのではないかと思います。
じゃあ、実際にどの程度の短さで収めているかかというと、例えば画面に収まりきるレベル、具体的な行数で言うと、個人的には30行程度が目安になるのではないかと思っています。
30行と言うと短いと感じる人もいるでしょうし、何を根拠に30行としているのかなどのツッコミが入りそうですが、強いて言えば、1つの役割だけをもたせた関数と言うのは大体そんくらいに収まるもんじゃないかなという個人的な意見です。
しかし、大抵仕事などでは実装はチームで行なうことも多いので、空気読んで『いい塩梅』が求められる場合がありますので、そちらも尊重しつつプロダクトに徐々に導入していくのがいいかと思います。