はじめに
新人のコードでよく見るのがこれです。
function makeCurry() {
// ニンジンを切る
cutCarrot();
// ジャガイモを切る
cutPotato();
// 肉を炒める
fryMeat();
// 水を入れる
addWater();
// 煮込む
boil();
// カレールーを入れる
addCurryRou();
// さらに煮込む
boilMore();
}
一見、何をしているか分かる、何の問題もないソースコードのように見えます。
ですがこのコード、実際の業務ではすぐに詰みます。
なぜか?
カレー専用すぎて、他に使えないから
人間の認識 vs プログラムの設計
人間は「カレーを作る」と考えます。
しかしプログラムではこのように考える必要があります:
「材料を切って煮込んで、最後に味付けする処理」
発想を変える
あなたがカレーを作るとき、それはカレーを作るのではありません。
「カレールーを入れた煮込み料理」 を作っていると考えましょう。
そうすることで、カレーを作るという作業のうち、どこを共通化して切り出せるかを意識することができるようになります。
コードで考える
/**
* 煮込み料理を作る処理
*
* @param array $zairyoList 材料のリスト(ニンジン、肉など)
* @param string $ajitsuke 味付け(カレー、シチューなど)
* @return string 完成した料理
*/
function cookNikomi($zairyoList, $ajitsuke) {
// 材料を切る
$kitta = kiru($zairyoList);
// 煮込む
$nikonda = nikomu($kitta);
// 味付けして完成
return ajitsuke($nikonda, $ajitsuke);
}
このように書くことによって
- カレー専用ではない
- シチューにもポトフにもトマト煮込みにも使える
- 味付けは引数で変わる
という普遍的な処理にすることができました。
命名の考え方
❌ よくある命名
// カレー専用の処理
makeCurry($zairyoList);
// ニンジン専用の処理
cutCarrot();
これは「何を」どうするかという対象の部分が、特定のなにかに依存してしまっています。
✅ 良い命名
// 煮込み料理を作る
cookNikomi($zairyoList);
// 材料を切る(引数ににんじんを渡すとにんじんを切ってくれる)
kiru($carrot);
これであれば、あくまでも 「何をするか(操作)」を表しているだけで、具体的に「何を」対象にするかは引数次第で変更することができます。
操作ごとに名前をつけるために必要な「抽象化」
抽象化とは「共通している部分だけを取り出すこと」を指します。
例えばカレーとシチューであれば、
- 材料を切る
- 炒める
- 煮込む
ここが共通なのでまとめられます。
例:ニンジンを切る
人間の理解:
ニンジンを切る
プログラムの理解:
材料をカットする(何を切るかは引数次第)
よくある失敗
❌ 抽象化しない
makeCurry();
makeStew();
makeHayashiRice();
最悪ですね!
まあもしかしたらこういうレシピ本もあるかもしれないですが、ほぼ同じなのであれば何度も書く必要はありません。
また似たような処理を量産することのデメリットとして、3つに共通する改善処理を行いたいとき、3つとも直す必要が発生してしまうことが挙げられます。
例えばきのこが嫌いなので「きのこが来たらfalseで返す」という処理にしたいとき、まとまっていれば1箇所の修正で済みますが、増殖させればさせるほどに何箇所も直す必要が発生し、また改修の漏れも発生しやすくなります。
カレーとシチューにはきのこを入れないでくれるのに、ハヤシライスにはきのこが入っていたら、それは抽象化がミスっているということです。
❌ 抽象化しすぎる
一方で抽象化させすぎることも避けたい事態です。
// 材料を処理する(何をするのか不明)
processZairyo($zairyoList);
// 調理処理(広すぎる)
cookingProcess($zairyoList);
// 料理を作る(ほぼ何でもできてしまう)
makeMeal();
これでは、メソッドの中で行われていることを想像すること自体はできますが、具体的には何をしているかは分からないですよね。
おそらく料理を開始しているのだとは思いますが、あいまいな命名を考えると、もしかしたらインドにスパイスの調達に行くところから始めている可能性もあります。
その点、先ほどのように切る・炒める・煮込むと明記してくれていると、とても安心できますね。
// わかりやすい処理の例
// 材料を切る
$kitta = kiru($zairyoList);
// 煮込む
$nikonda = nikomu($kitta);
// 味付けして完成
return ajitsuke($nikonda, $ajitsuke);
これはカレーの例だけではなく実際のソースコードにも言えます。
例えば処理の初めにDBアクセスして大量の顧客レコードを取得する処理が入っているのか・いないのか、処理の最後にクライアントに結果を通知する処理が入っているのか・いないのかで、この処理は実行していいのかどうか・安全に実行できるのかどうか、の安心感が格段に変わります。
抽象化というのはあくまでも「何度も使われる処理の共通部分を取り出す」ことであって、メソッド名を抽象的なものにするという意味ではありませんので注意しましょう。
私個人の感覚としては、「同じ処理 2回書いたら 抽象化(575)」です。
まとめ
- 「カレーを作る」と考えるな
→ 処理の構造で考える - 命名は「対象」ではなく「操作」
- 引数で変わるようにする
- 抽象化は共通部分をまとめること