入社してプロダクトのコードを見たときに、同じようなロジックがいくつかのソースにあることに気付きました。
当初は「これはまとめた方がいいのでは?」と疑問に思っていました。wikipediaを調べてみても、悪者であるようにしか書いてありません。(DRY原則についても、大体そんな印象です)
しかしこの記事で書いたようなモジュール分割の考え方に照らせば一般にはそうではないとわかります。
具体例を挙げて説明します。1webサービスか何かで、UIに表示するモジュールとメール送信するモジュールを分割する設計にしたとします。それぞれでユーザー文字列を変換する処理を組むと以下のように重複します。
// ui_user_name.c
void print_user_name(const char* raw) {
char buf[64];
int j = 0;
for (int i = 0; raw[i] != '\0' && j < 63; i++) {
char c = raw[i];
if (c == '\t' || c == '\n') {
continue;
}
// 大文字を小文字にする
if (c >= 'A' && c <= 'Z') {
c = c - 'A' + 'a';
}
buf[j++] = c;
}
buf[j] = '\0';
display_buf("ユーザー名: %s\n", buf);
}
// mail_user_name.c
void make_mail_user_name(const char* raw, char* out) {
int j = 0;
for (int i = 0; raw[i] != '\0' && j < 63; i++) {
char c = raw[i];
if (c == '\t' || c == '\n') {
continue;
}
// 大文字を小文字にする
if (c >= 'A' && c <= 'Z') {
c = c - 'A' + 'a';
}
out[j++] = c;
}
out[j] = '\0';
}
一見「ユーザー名正規化」のような関数をつくって共通化すればいいように見えますが、それは設計として微妙です。なぜなら、この2つの処理はたまたま一致しているだけなのに共通化したい意図を埋め込んでしまうからです。
プログラムは単なる命令の羅列ではなく、意図を埋め込むものです。違うものを共通であるように書いたら、「共通であるべき」意図が誤って伝わってしまいます。
今回で言うと、「メールとUIで同じ表現をする決まりになっている」とか「システムに標準の表記が定義されている」という誤解を招きます。
実際は(特別な事情がなければ)「UIはレイアウトの都合で改行処理の変更がありうる」「メールは規約変更の影響を受ける」など個別の事情があるので、同じ表現にするべきというわけではないです。
また、実害が発生することも考えられます。
無理やり共通化しようとすると以下のような関数を作ることになります。
バグとして顕在化することは少ないと思いますが、その他以下のようなことが考えられます。
- 片方だけロジックを変える場合に、モード引数を導入して可読性が劇的に落ちてしまう
- 第三の呼び元がこれを呼び出して良いかの判断が必要
- 第三第四の呼び元ができたあとに、分割にかかる修正コストが大きい
今考えれば当然なのですが、昔は分かってなかったな、という話でした。
-
余談ですが、この具体例はchatGPTに提案してもらいました。なかなか秀逸な例で、自分では思いつきそうにないので悔しいです。 ↩