はじめに
本記事は 「カレーを作るな、カレールーを入れた煮込みを作れ」:超新人エンジニア向け 抽象化と命名の話 の第2弾です。
前回は、カレー作りを例にして、処理をこのように切り出しました。
/**
* 煮込み料理を作る処理
*
* @param array $zairyoList 材料のリスト(ニンジン、肉など)
* @param string $ajitsuke 味付け(カレー、シチューなど)
* @return string 完成した料理
*/
function cookNikomi($zairyoList, $ajitsuke) {
// 材料を切る
$kitta = kiru($zairyoList);
// 煮込む
$nikonda = nikomu($kitta);
// 味付けして完成
return ajitsuke($nikonda, $ajitsuke);
}
cutPotatos()のようなジャガイモ限定の処理もなく、cookingProcess()のようなもしかしたらインドへのスパイス調達旅行から始まっているかもしれない曖昧な料理処理もなく、カレーを作る処理は十分簡潔になりました。
しかし実際の業務では、カレーを作るだけではありません。たとえばあなたが顧客情報に関する処理を作っているとして、顧客にメールを送る処理、顧客一覧をエクセルでダウンロードする処理、解約済み顧客を過去リストに移管する処理などなど・・・。
つまりあなたが顧客情報を使って料理するシェフだとして、カレーのみを作るのではなく、オムライスを作ったりそうめんを作ったりすることもあるということです。
その場合、キッチンの中は使うものを整理整頓しておいたほうがいいですよね。
今回はそんな整理整頓のお話です。
悪い例:とりあえず1つにまとめる
「カレーに必要なものを集めた class CurryZairyoを作りました!」
/**
* カレーの材料
*/
class CurryZairyo {
public const CARROT = 'carrot';
public const POTATO = 'potato';
public const MEAT = 'meat';
public const CURRY_ROUX = 'curry_roux';
public const SALT = 'salt';
public const FROZEN_CORN = 'frozen_corn';
public const KNIFE = 'knife';
public const CUTTING_BOARD = 'cutting_board';
public const POT = 'pot';
}
食品庫で考える
上記のクラスがなぜ悪いのか、キッチンを想像してみてください。
- 冷蔵庫(野菜や肉)
- 冷凍庫(冷凍食品)
- 食品庫(パスタ・カレールー・調味料)
- 調理器具の棚
これを全部その辺に雑多に置いていては、散らかるし、見つけるのも大変だし、ないと思って同じのを2個買っちゃったりしそうですよね。(コードは傷みませんが食材の場合は適切な温度管理というのもありますし)
役割を整理する
整理整頓をするには、まず役割を整理していきましょう。
これらは確かに全部「カレー作りに使うもの」ではありますが、役割はやや異なっています。
- ニンジン・肉 → 食材
- カレールー・塩 → 味付け
- 包丁・まな板・鍋 → 調理器具
このように整理することで、雑多なキッチンは少し整理することができそうです。コードも同じです。
良い例:役割ごとに分ける
/**
* 冷蔵の食材
*/
class ReizoZairyo {
public const CARROT = 'carrot';
public const POTATO = 'potato';
public const MEAT = 'meat';
}
/**
* 冷凍の食材
*/
class ReitoZairyo {
public const FROZEN_CORN = 'frozen_corn';
}
/**
* 味付け
*/
class AjitsukeZairyo {
public const CURRY_ROUX = 'curry_roux';
public const SALT = 'salt';
}
/**
* 調理器具
*/
class CookingTool {
public const KNIFE = 'knife';
public const CUTTING_BOARD = 'cutting_board';
public const POT = 'pot';
}
当初は「カレーに使う」という理由だけで、材料さらには調理器具も1つのクラスにまとめていましたが、こうすると、
- 食材はどこにあるか
- 調味料はどこか
- 調理器具はどこか
がすぐに分かります。
もちろん、さあ今から料理に取り掛かるぞ!というタイミングではこれらの材料や調理器具が手元にある必要がありますが、日頃保管しておく場所はこのように整理されているほうがいいと思いませんか?
どうして整理整頓が必要なのか?
結論:他人と協働するときに困るからです。
(そして新人の皆さん、今は信じられないかもしれませんが、自分の成長とともに、そして自分の忘却とともに、いつか 過去の自分が書いたソースコードの言っている意味が本当に全く分からなくなる日が来ます。 過去の自分は他人、未来の自分も他人です。 )
あなたが仮に1人で開発していたとしても、未来の自分(他人)のために整理をしていく必要があります。
仮に散らかした先に、どんなバッドエンドがあるのか実際に見てみましょう。
話を戻しまして、あなたは複数人のシェフが働くキッチンにいます。
あなたが肉じゃがを作りたくなって「お肉」と「じゃがいも」はないかとキッチンを見渡した時、class CurryZairyo(カレー専用材料置き場)にお肉とじゃがいもがあることを発見しました。
カレー専用材料置き場にお肉とじゃがいもを置いた人は、なんとなくそこにお肉とじゃがいもを置いておいただけで、別にカレー以外に使ってもらっても構わなかったとします。
このとき、どんな問題が生じるでしょうか。
1. 無意味な増産
料理をする人はカレー以外に使ってもいい材料だとは知らず、「おや、どうやらこれはカレー専用のお肉とじゃがいもらしいぞ」と考えました。結果、同じだから新しく用意しなくてもよかったのに、わざわざ同じものを用意してしまいました。
2. 疑心暗鬼
料理をする人は、材料を置いてくれた人と直接話して、そこのカレー専用材料置き場のお肉とじゃがいもは自由に使っていいと確認を取りました。しかし完成した料理を見た人がこう言います。
「これ、カレー専用お肉って書いてありますけど、本当に使っちゃってよかったんですか?」
3. 前はいいって言ってたのに
「ああそうだよ、確認したら、カレー専用材料置き場のものは使っていいって言ってたよ」
それを聞いた別のシェフは、ちょうどからあげを作りたかったのでカレー専用材料置き場のお肉を拝借することにしました。
「おい!!!なんでカレー専用のお肉をからあげに使ってるんだよ!!!」
えええ、、、前はいいって言ってたんじゃないんですか、、、。
「カレーのお肉はからあげに使っちゃいけないに決まっているだろ!!!!」
・・・結局、カレー専用材料置き場のものは使っていいのか使ってはいけないのか、「1:無意味な増産」や「2:疑心暗鬼」や「3:前はいいって言ってたのに」を繰り返しながら、古株さんと新人さんを巻き込みながら、キッチンは散らかり混沌へと向かっていきます。
「カレー専用」を尊重させるために
上記のような混沌を回避すべく、役割を整理し、管理するべき場所に置いておくことで、無駄にカレー専用材料置き場として扱う必要はなくなります。
これは前回 「カレーを作るな、カレールーを入れた煮込みを作れ」:超新人エンジニア向け 抽象化と命名の話 にて処理を「じゃがいもを切る」ではなく「引数に渡された材料を切る」に抽象化したのと少し似ているかもしれません。
ですが実際に業務をしていると、「いやこれは本当にここでしか使わない処理なんです!」ということもあるでしょう。その時に専用処理を作ってはいけないということが言いたいのではありません。
「専用処理」と言うのであれば、本当にその処理は「専用」として、それ以外の共通化できる部分はしっかり切り分けて区別しておくことが重要です。それが 「専用」が尊重されるために必要なことです。
ただし、分け方に正解はない
ここでさらに重要な注意を付け加えたいと思います。
キッチンの場合はまだ分かりやすいですが、常に「どのように分けるべきか」が明確とは限りません。
クローゼットで考える
今度は服の収納で考えてみましょう。
服の分類の仕方にはかなり方法があります。
- 季節で分ける(夏 / 冬)
→ でも上着を羽織れば冬でも着れる・・・ - 人で分ける(夫 / 妻 / 子供)
→ でも娘が大きくなってきたから、娘とシェアしてる服もある・・・ - 用途で分ける(仕事 / 私服)
→ でもジャケットを羽織れば仕事用としても着れる・・・ - 子供の成長が早いのでベビーサイズで分ける(50cm / 80cm / 100cm)
→ でも男子デザインと女子デザインが混ざっちゃう・・・
いろいろな分け方がありそうですし、どの分け方がベストかというのは、その家庭のその時々の事情によって変わりますよね。
ソースコードでも同じです。分類の方法に正解はなく、プロジェクトによってよりよい分け方は変わると思います。
バッチ処理の例
悪い例からまず見ていきましょう。
/**
* 物件出力バッチで使う定数
*/
class BukkenExportBatchConst {
public const DB_HOST = 'localhost';
public const CSV_HEADER = ['id', 'name'];
public const FTP_HOST = 'ftp.example.com';
public const LOG_STATUS_SUCCESS = 'success';
public const TMP_DIR = '/tmp';
public const CHARSET = 'SJIS-win';
}
「このバッチで使うから」という理由でまとめています。
これは class CurryZairyo カレー専用材料置場と同じ問題です。
良い例:責務ごとに分ける
/**
* DB接続に関する設定
*/
class DbConfig {
public const HOST = 'localhost';
}
/**
* CSV出力に関する設定
*/
class CsvConfig {
public const HEADER = ['id', 'name'];
public const CHARSET = 'SJIS-win';
}
/**
* FTP接続に関する設定
*/
class FtpConfig {
public const HOST = 'ftp.example.com';
}
/**
* 出力ログに関する定数
*/
class ExportLogConst {
public const STATUS_SUCCESS = 'success';
}
/**
* 一時ファイルに関する設定
*/
class TmpFileConfig {
public const DIR = '/tmp';
}
このように分けていくと、何がどこにあるかがわかりやすく、また次に処理を追記する人のために 「どこに追記するのがこのプロジェクトにとってよいか」という方針を示す ことができています。
判断基準
分類するときは、次を意識していきましょう。
- 同じクラスの中身が同じ役割か
- 読むときに探しやすいか
- 新しい要素を追加するとき迷わないか
ここまで読んでお気づきの方もいるかもしれませんが、そのプロジェクトのソースコードを 最初に書く人の責任はとても重いです。
ですがきっと多くの人はチーム開発をしていることでしょう。まず自分の考えを持ちつつ、周囲の人にこの案でどう思うか聞いてみましょう。もちろん個人開発の人も、AIに聞くなどして壁打ちしてみるのもいいですね。
まとめ
- 「一緒に使うもの」でまとめない
- 「同じ役割のもの」でまとめる
- まとめていくことで結果的に「専用処理」が際立つ
- 分け方に唯一の正解はない、周囲の人と相談して正解を模索する