「関数」に出会って、どういう基準で関数化すべきか悩んでいるくらいの人向けの初学者向け内容です。
関数を使うとコード量が減る???
関数を作る目的やメリットとして「プログラムの記述量が少なくなる」「処理をまとめられる」として紹介されている参考書やWebページをよく見かけます。
しかしながら・・・
「プログラムの記述量が少なくなる」を目的として覚えさせるのはかなり誤った教え方だと思いますし、メリットとしても誤っていると思います。
まぁ要はケースによってはプログラムの記述量が増えます。
また「処理をまとめられる」は間違いではないのですが、まとめた結果どうメリットがあるのかを考える必要があり、
単にコード量が増えたらまとめるという「記述量」に引っ張られた考え方では関数のメリットが出ません。
あくまで私見になってしまうかもしれませんが
私の考える関数を作る目的は「処理の依存性を分割すること」であり、
それによるメリットは「再利用性/移植性が向上」「可読性が向上」「保守性が向上」に当たると思います。
なぜ「プログラムの記述量が少なくなる」と教えるのが誤りか
例えば以下のようなコードがあったとします
# データベースからでも何でもいいが名前与えられたとする
member_name = "Tanaka Ichiro"
# 苗字と名前を取得。半角スペースで分割する
[myoji, namae] = member_name.split()
苗字と名前を作る処理は一行であり、「プログラムの記述量が少なくなる」目的や「処理をまとめる」という目的だとしたら関数を作る必要は必要ないことになります。
しかし、開発が進んだと想像してみてください。
そして「苗字と名前を取得する処理」が100か所になったとします。
[myoji, namae] = member_name.split()
~
[myoji, namae] = member_name.split()
~
....
# 別のファイルでも
[myoji, namae] = member_name.split()
...
[myoji, namae] = member_name.split()
さて、この状態で仕様がかわって、苗字と名前はフルネームを分割するのではなく、フルネームをもとにデータベースから取得することになりました。
処理はどうでもいいのですがこうなったとします
[myoji, namae] = getDB(member_name)
そうなると先ほどの100か所のコードをすべて修正する必要があります。
(まぁこのくらいの問題なら全置換でもいいっちゃあいいのですが)
コード量が増えても関数化したほうが良いケースはたくさん
このような問題を避けるためには1行だとしても関数を作っておいたほうが良いです。
def getMyojiNamae(name):
return member_name.split()
[myoji, namae]= getMyojiNamae(name)
#(細かくいうとミドルネームとかも考えるとこの返却値設計もよくないけど話が逸れるので保留)
こうしておけば、仮に苗字と名前の取得手段が変更になったとしても関数の中身を変更すれば、100か所の苗字と名前の取得処理に影響は出ません。
これは保守性の問題だけではありません
可読性の観点で言ってもメリットがあります
# 処理意図として「名前を分割する」と言う手段はわかるが
# 「苗字と名前を取得する」という目的は憶測する必要がある
member_name.split()
# 「苗字と名前を取得する」が目的である。手段は知らん。
getMyojiNamae(name)
要は処理を書いたときそれが目的ではなく手段の記載になっていたら、処理の長さに関係なく関数にしてしまったほうが良いことが多いです。
そして大事なのは 「関数の呼び元は、関数が行う手段を知る必要がない。むしろ知るべきではない」 ということです。
逆に 「関数は、呼び元のロジックを意識すべきでない」 も同様に言えます。
苗字と名前をどう使うのか知りませんが、とにかく苗字と名前を取り出すことに集中すべきです。
もちろんその際に必要な情報をくれないと処理はできないので、必要な分の依存性は渡します。
上記の例でいうとボスである関数の呼び元は「フルネーム」から「苗字と名前が欲しい」ということさえ知っておけばよく、実際部下がsplitだろうがデータベースだろうがAPIから取得してこようが知ったことではない、
という状態にしておかなければいけません。
つまりボスの処理目的に対して、部下はその手段を必要な分の依存性を持つべきです。
1行の処理は少し極端な例でしたので、なんでもかんでも関数にしろということではないですが、
「処理を短くしたい」ということを目的にすべきでないというところはご理解いただけたかと思います。
再利用性を高めるにも処理の依存性を意識
さて、保守性と可読性の観点で処理の依存性をなくすべきと述べましたが、
再利用性を高めるには「処理をまとめる」ことが必要です。
しかしながら最初に述べたように単に「処理をまとめる」とあまり再利用性は高まりません。
例えば何か加工品を作る関数で材料を切って穴をあける処理があったとします
def 加工(材料):
# 切る
xxメーカーの電ノコに刃をセット
のこぎりスピードをxxに設定
電ノコスイッチOn
材料を切る
# 穴をあける
マキタのドリルに10mmの刃をセット
材料を固定
穴をあける
この加工関数は、一見再利用性がありそうですが
材料の仕様が変わって切らずに穴をあけたい場合に使えません。
ブロックコメントで#切る #穴をあけると書きましたが、
目的の手段を達成するにあたってさらに目的が二つあるなら、その目的ごとに関数を分けたほうが再利用性が高まります。
また、先ほどの通り依存性の意識から言っても
この関数は加工するという目的に対して、切る方法詳細な意識したり、ドリルのメーカーを意識したりです。
加工担当の班長の下に切る職人、削り職人、穴あけ職人がいたとして
班長にいちいち作業のたびに職人のこだわりを確認する必要がないので
班長としては「とにかく仕様通りに切ってくれれば電ノコだろうが斧だろうが手段はどうでもいいから」という気分なのです。
ただ穴のサイズは班長から指定したい時があるので、そういうものは班長の目的と穴あけ手段に依存性があります。
def 加工(材料,10mm)
材料 = 切る関数(材料)
材料 = 穴あけ関数(材料, 10mm)
このような感じで、メイン関数を大ボス、その下の関数を中間管理職、その下が・・・
そしてなるべく上司様の目的以外は手段について余計な意識を上司様にさせないような、ちょっとブラック企業気味な形が依存性の分割になります。
慣れれば自然とClass設計も出来るように
プログラミング初学者の方は、関数を覚えたらその次にClass設計を覚えることになると思います。
しかしながらClass設計するにあたっての根柢の考え方に必要なのはまさに依存性のコントロールとなります。
おそらく参考書などを読み進めると結合度や凝集度、オブジェクト指向やDI(依存性の注入)と言った話が出てくると思いますが、
要は処理の呼び元と呼ばれ先の関係が「誰が何をどこまで処理を意識するべきか」を考えることが入り口になると思います。