関数の書き方・分け方
はじめに
本当はもっときれいに書きたいけど処理が複雑になってくるとどうしても汚くなってしまう。関数が長くなりすぎて処理を分けたいんだけど複雑に絡まりあっていてもう手を入れられない。ということがあると思います
関数の分け方というのは規則で機械的に決定できるような分かり易い正解というのは無いので説明しづらいのですが、ここではいくつか指針を示して説明としたいと思います
関数の分け方
なぜ機械的に関数を分けることができないか
機械的に適用できるような関数の分け方の規則というようなものが無いのはなぜでしょう
これは関数の分け方というのが文法に基づいたものではなく意味に基づいたものだからです
関数は途中になんらかの文法要素があったらそこで分ければいいというようなものではなく、関数は意味の単位に分けるものです
いつか意味を理解するアルゴリズムが開発されるかもしれませんが、現在は意味が分かるのは人間だけですから人間がここからここまではこういう意味だからこういう関数に分けようと地道にやるより他ありません
処理内容を言葉にする
プログラミングの基本というのはプログラミング言語を読み書きできることではありません
プログラミングの基本というのはコンピューターにやらせたい処理の内容を言葉にするということです
プログラミング言語でやりたいことをコンピューターに伝える前にまず、自然言語でやりたいことを自分自身に伝えるところからプログラミングは始まります
ですから関数を分けるということの前にもまずは地道に関数の処理の内容を言葉にするところから始めましょう
どんなプログラムでもその中に意味の無い処理などというものは一つもありません
すべてのステップには必ず意味があります
それをまずは言葉にしてみましょう
変数と関数
そして上手く関数を分けることができないという人は次に処理の結果を受け取るローカル変数を整理してみましょう
ローカル変数にきちんと適切な名前を付けること、ローカル変数のスコープを適切に区切ること、関連するローカル変数のグループを意識すること、何度も同じ計算をしていたら一つのローカル変数にまとめることなどなど
きちんと変数を分けられるようになると、その変数に入れる値を計算している処理やその変数を使う処理というのもまとまってくるでしょう
そうした変数に入れる値を計算している処理やその変数を使う処理というのは関数に分けるポイントです
何のために変数を用意し、何に変数を使うのか
それをそれぞれ関数にすればいいのです
意味のある処理に名前を付ける
変数というのは意味のある値に名前を付ける手段です
関数というのは意味のある処理に名前を付ける手段です
一つの関数の中でもここからここは○○の処理、ここからここは××の処理と分けられる場合があると思います
そこをそのまま関数に分ければいいのです
まったく別々の処理じゃなく共通する部分もあるというならその共通しているところを引数にするなり共通の関数に分けるなりすればいいことです
ローカルブロックの中と外
ローカルブロックは関数内にある名前の付いていない関数であると考えることもできます
関数内のローカルブロックの外からその中にある変数は見えません
これは別の関数の中にあるローカル変数が見えないのと同じことです
関数内のローカルブロックの中からその外にある変数は見えます
ローカルブロックの外にある変数から値を読み込んでいるものがあればそれは関数に指定された入力引数と同じことです
ローカルブロックの外にある変数に値を書き込んでいるものがあればそれは関数に指定された出力引数と同じことです
ローカルブロックはいちいち関数の引数を決めなくてもその外にある変数は何でも見えるので便利なのですが、しかしそれが関数を分ける妨げになっていたりはしないでしょうか
なぜローカルブロックになっているのでしょうか
それはそこに何か意味の区切りがあるからではないでしょうか
意味を持っているものなら名前を付けてあげましょう
言い訳が重要
ある関数を分割しようと思ってすぐできない場合、何か言い訳があると思います
ここを分けたいんだけどここは○○だから分けられない。そこも××だから分けられない
その言い訳が重要です
言い訳のあるところこそ関数を分ける際に注目すべきポイントです
同じ処理を一つにまとめる
何度も同じ処理をしているようなものは関数に分ける第一候補です
同じ処理だけど使う変数が違うから一つにはまとめられないというものもあるでしょうか
違うものは引数にすればいいのです
そういうものこそ関数に分けるポイントです
違うものが型ならテンプレートを使えます
引数が多すぎて面倒だったら構造体にまとめる
分けたいんだけどいくつもローカル変数を参照しているから分けるのが大変というのもよく聞く言い訳ですが、必要なものは全部関数の引数にしてしまえばいいのです
毎回関数の引数に並べるのが面倒なら使う変数を構造体にまとめればいいのです
変数とその変数を使う関数をセットにしてクラスとしてもいいでしょう
同じ変数を使う関数があるならクラスにまとめる
変数とその変数を使う関数のグループがあるようならそれをクラスとしてまとめてもいいでしょう
一度しか使わなくても分ける
プログラミングで関数というと再利用を前提とした説明をされることが多いので、標準関数のように他でも使う価値のある一般的な処理だけが関数となるべき資格を持つのだ。というような思い込みのある人もいるかもしれませんが、関数を分けるときに再利用について考える必要はありません
何度も使うわけじゃなくてここで一度使うだけだからということはよく聞く言い訳ですが、使う回数は関係ありません。処理の一般性や特殊性も関係ありません
それが意味を持つ処理なら使う回数など気にせず分ければいいのです
何のために関数を分けるのか
一度しか使わないものを何のために分けるのか、そんなことに何の意味があるのかというと、これは単純に可読性のためです
コンピューターにとっては実行コードが途中で別の関数になろうがなるまいがそんなことはどうでもいいことです
アプリケーションの利用者にとっても中身が関数に分けられているかどうかなどということはどうでもいいことです
関数分けというのは純粋にコードを書いているプログラマのためのものです
人間が一度に把握できる状態の数には限りがあります
関数に分けるということはその入力と出力だけがはっきりしていれば中身の処理については気にしなくていいということです
これは処理を関数に分けると覚えておくべき状態の数を減らすことができるということです
気にする必要のある状態の数が少ないということは関数の挙動を理解し易いということです
だれが理解しやすいのかというとプログラマが理解し易いということです
つまり関数はプログラマがその挙動を理解し易くするために分けているのです
ただそれだけのことです
どこまで分ければいいのか
その関数そのままで挙動のすべてが十分に理解し易いのならそれ以上分ける必要はありません
そうでないならそうなるまで分ければいいことです
どのくらいの大きさで分ければいいのか
どうも関数を分けるときにはある程度の大きさでないといけないと思っている人もいるようですが、しかし例えば標準のテンプレートライブラリのメンバ関数の実装を覗いてみましょう
ただメンバ変数を返しているだけ、ただメンバ変数に値を代入しているだけというような関数がいくらでもありますね
関数に分ける単位はある程度大きくないといけないなどということはありません
たった一行であっても関数にしていいんです
たった一回使うだけのものでも関数にしていいんです
size_type size() const { return m_size; } bool empty() const { return !size(); }
関数呼び出しのコストを無視する
関数呼び出しはまずジャンプそのものが比較的コストが高い処理です
また関数の引数をスタックに積んだり仮想関数テーブルを参照したり一時オブジェクトの生成と破棄があったりなどなどいろいろなコストがかかります
速度を気にして関数を分けられないという人もいるかもしれません
しかしそんなことは気にする必要はありません
関数呼び出しのコストは無視して構いません
そういうところは上手に関数を分割できるようになってから気にすればいいのです
まずは関数を上手に分割することを優先してください
他人に見せるわけではなくても分ける
他人に見せるわけじゃないからというのもよく聞く言い訳ですが、実際に他人に見せるわけじゃなくても他人に見せるつもりでコードを書きましょう
いつまでもコードを書いたときのことを憶えていられるわけではありません
忘れてしまった未来の自分は他人です
きれいなコードを書くコストは無駄じゃない
他人に見せるわけでもないコードをきれいにするなんて無駄なコストはかけられないというのもよく聞く言い訳ですが、無駄なコストではありません。そんなコストは無視して構いません
常に他人に見せるつもりでコードを書くようにしてください
将来忘れてしまった汚いコードを読み解くのは余計なコストでしょう
他人に見せないからきれいに書かないというのはコストを先送りしているだけです
我々は趣味のプログラミングを楽しんでいるアマチュアではなく、商売としてコードを書いているプロですからそこにコスト意識を持つことは大切なことです
しかしコスト意識があるなら今きれいなコードを書くようにしましょう
こんなところに注目
関数は文法で分けるわけではないと言いましたが、意味の分け目となるような文法要素もあります
ローカルブロック
関数内のローカルブロックはそのまま関数に分けられます
名前を付けて関数とした方がよくはないか検討してみましょう
ブロック外の変数も見ているから分けられないというならそれを引数にすればいいことです
見ている変数がたくさんあるならその変数を構造体なりクラスなりにすればいいことです
一度しか使わなくても構いません
ループ
ループもローカルブロックです
そのループだけの関数にできないかどうか検討してみましょう
条件分岐
その条件分岐は何をしているのでしょう
何か意味があるはずですね
その意味に名前を付けることはできないでしょうか
名前があるなら関数にできるはずです
関数にした方がよくはないか検討してみましょう
switch-case
switch-case は switch-case だけの関数に分けられる場合が多いので、関数に分けられないかどうか検討してみましょう
goto
goto を使いたいところは関数に分けられるという徴です
関数の途中で使っているならその goto の範囲を関数に分けられるはずです
関数の最後に終了処理をさせるために使っているならそれはもう時代遅れの発想です
そういうことはデストラクタにやらせましょう
つまりその関数をクラス化すべきであるという徴です
関数一つのためにわざわざクラス化するほどのことでもないというのもよく聞く言い訳です
そんなことは気にせずクラス化してください
コピー&ペースト
コピー&ペーストをするということはもちろん同じ処理をしているということです
そこを関数に分けられないかどうか検討しましょう
まったく同じなわけではなく少し違うんだというならその違うものを引数にします
引数が多すぎて分けられないというなら引数をまとめてクラスにすればいいことです
違うものが型ならテンプレートを使えます