
その変数名、3ヶ月後に自分で読んで「これ何だっけ」ってなるやつじゃん?🥺
これ絶対あるじゃん??
flagって名前の変数がファイルに4個いて、それぞれ全然違う意味を持ってるprocess(data)って関数、名前だけ見ても何をprocessするのか完全に不明tmpが一時変数なのか最終的に返す値なのか、追うまでわからない- 3ヶ月前の自分が書いたコードに「これ誰が書いた?」って思ったことがある
これがMysterious Name(謎めいた名前)、以降「幽霊ネーム」って呼ぶね、が原因なんだよね。
てかシンプルに言うとね
幽霊ネームってのは、「名前はついてるのに何のためにあるか分かんない変数や関数のこと」っつーこと。💡
それの何がまずいの?ってなる気持ち、わかる!「動くじゃん別に」ってね。でもコードって動かすためだけじゃなくて、読むために書くものじゃん。名前をつけるのは「次にここを触る人への手紙」みたいなもんなんだよね。その手紙が白紙だと、読んだ人は解読作業から始めなきゃいけなくなる。
解読に時間かかる → 「このコード触りたくない」って気持ちになる → そのまま放置される → どんどん触れないコードが増えていく。このどんどんヤバくなるループの入り口だったりするんだよね。
ちょっと待って、先に全然関係ない話していい?🏠
中堅の会計事務所に、田中さんっていうベテラン経理担当がいたんだけどさ。そこで15年働いてて、毎月の締め処理も確定申告も完璧にこなしてたの。
問題は、田中さんのデスクにあった。
引き出しが6段あって、ラベルは上から順に「A」「B」「C」「その他」「stuff」、最下段はラベルなし。田中さんの頭の中には完全な地図があって、「Aは月次請求書、Bは領収書の控え、Cは未処理の支払い依頼...」って全部把握してた。15年間、それで困ったことは一度もなかった。
12月に、田中さんが急に産休に入ることになった。
引き継ぎは1週間。後任の山田さん(中途入社、経理経験3年)が受け持つことになった。
「引き出しのAとBが月次関係で、Cが支払い系です」
「え、それだけ?」
山田さんはメモを取りながらそう思ったけど、田中さんはもう翌週から休みに入るし、深く聞けなかった。
1月末、月次締め処理の日。
「山田さん、12月分の○○社への請求書がシステムに入ってないんだけど」と佐藤部長が近づいてきた。
「すぐ確認します!」
山田さんはAを開けた。月次請求書のはずなのに、なぜかそこには去年11月の領収書が混ざってる。Bを開けると、請求書の控えと未処理の書類が一緒に入ってる。Cには、支払い依頼じゃなくて確定申告の資料が入ってた。
「これ、田中さんの頭の中の分類と全然違う...!」
「山田さん、○○社との契約、今月更新なんだけど請求書出てないと困るんだよね」
佐藤部長の声が低くなってきた。
3時間後、山田さんはすべての引き出しを机に広げて、書類を1枚ずつ確認してた。
結局、請求書はCの一番下に「その他」フォルダに挟まって出てきた。理由は後でわかった——田中さんの中では「C = 今月処理するもの全部」だったから、支払いも請求も入ってたんだって。
これが本番で起きてるやつが、幽霊ネームってこと。
じゃあコードで見てみよっか👀
def process(data, flag=False):
tmp = []
result = None
for d in data:
if d['flag']:
tmp.append(d)
if tmp:
result = tmp
flag = True
return result, flag
これ、何してるか分かる?
マジで、わたしも昔これと同じコード書いたことあるから言えるんだけど、書いた直後は「わかるじゃん」って思うんだよね。でも3ヶ月後に見ると——
dataって何のデータ?ユーザー?注文?在庫?flagは初期値Falseで渡すのに返り値にもなってる。どっちのflagを指してる?tmpが最終的にresultになってる。一時変数じゃないじゃん。d['flag']って、dictの中にもflagがいるの?別のflagじゃん。
ってなる。
で、これが本番コードに埋まってたとして、仕様変更が来たとする。「有効なユーザーだけじゃなく、アクティブなユーザーも含めて処理して」。
flagの意味を追うために呼び出し元を全部確認して、d['flag']が何に対応してるか別ファイルのモデル定義を開いて、tmpが何を指してるか処理フローを追って——気づいたら30分溶けてる。
そして「たぶんここだろ」で修正したら、別のところでflagを参照してた処理が壊れて深夜にSlackが来る。えぐい。
やばいのまだあんだけど、バグに気づけない問題😭
さっきの田中さんの話、続きがある。
山田さんが「Aは月次請求書」と思い込んで作業してたとする。2月も3月も、Aを開けて処理して閉じてた。でも実は田中さんのルールは「A = 今月末までに処理するもの全部」で、月によって中身が変わってた。
山田さんのやり方でも「処理できた」ように見える月がある。でも5月に、Aに入ってるはずの請求書が実は別の引き出しに分散してた月があって、そこで初めて「あれ、3ヶ月ずっとズレてたじゃん」ってなった。
コードでも同じことが起きる。
def process(data, flag=False):
# ...
return result, flag
# テストを書こうとすると...
def test_process():
# dataってどんな構造? → モデル定義を確認しに行く
# flagがTrueで渡すケースとFalseで渡すケースを両方テストする?
# d['flag']がTrueのデータとFalseのデータで結果が変わる
# 外から渡すflagとd['flag']の組み合わせで何パターン?
# → True/False × True/False = 4パターン
# さらにdataが空のケース、Noneのケース...
pass # ここで思考停止する
名前が不明だと、テストを書く人間が「この変数、何をテストすればいい?」から考え直す羽目になる。flagが2個いるだけでテストの組み合わせが4倍になって、「とりあえずhappy pathだけ」ってなって、バグが隠れる余白が生まれる。
幽霊ネームは壊れやすいだけじゃなく、バグを隠す設計でもある。
分けてた会社の話もするね〜✨
同じ会計事務所に、隣の課の松本さんという担当がいた。
松本さんのデスクの引き出しには、こう書いてあった。
- 「01_今月請求書(送付前)」
- 「02_今月請求書(送付済み)」
- 「03_領収書_2024年度」
- 「04_未処理_支払い依頼」
- 「05_確定申告2024」
松本さんが育休に入ったとき、後任の鈴木さんは引き継ぎ初日に「あ、全部わかった」って言ったらしい。
月末に「02のやつ確認して」で伝わる。「04がたまってきたから処理して」で終わる。田中さんの引き出し事件は、松本さんの課では一度も起きなかった。
コードで見るとこうなる。
def filter_active_users(users: list[dict]) -> tuple[list[dict], bool]:
"""アクティブなユーザーだけ返す。一人でもいれば has_active_user が True になる。"""
active_users = []
has_active_user = False
for user in users:
if user['is_active']:
active_users.append(user)
has_active_user = True
return active_users, has_active_user
これなら——
usersを受け取ってactive_usersを返してる。読んだ瞬間に「ユーザーを絞り込む関数」とわかるhas_active_userが何を意味してるか、名前だけで伝わる- テストも「アクティブなユーザーが混在するリスト」を渡せばいいってわかる
- 仕様変更が来ても、「
is_activeの条件をis_active and not is_deletedに変えればいい」って判断が一瞬でできる
深夜Slackは来ない。
ちゃんとした名前もあるから一応言うね📚
正式名称は2つセットで覚えて。
- Mysterious Name(謎めいた名前):アンチパターン側の正式名称。Martin Fowlerの「Refactoring」第2版(2018)で定義されてる、意図が読み取れない命名のコードスメル。
- Intention-Revealing Names:正しい設計原則側の名前。Kent Beckが1996年の『Smalltalk Best Practice Patterns』で最初に言い出して、Robert C. Martin(通称Uncle Bob)が「Clean Code」(2008)の第2章で広めた考え方。「読んだ瞬間に何のためか分かる名前をつけよう」ってこと。
記事内で「幽霊ネーム」って呼んでたのはMysterious Nameのこと。Kent Beckの「Implementation Patterns」(2007)でも「Intention-Revealing Name」として同じ概念が登場してて、命名が設計の一部だって話はかなり前からあるんだよね。
難しそうに聞こえるけど、要は「この名前を見た人が、コードを読まずに意図を理解できるか?」を問い続けるだけなんだよね。
これだけ覚えて帰って👌
合言葉はこれ。imf(いみふ)
コードを書くとき・レビューするとき、この問いを使って。
「この変数(関数)の名前が変わるとしたら、どんな出来事が起きたとき?」
例えばflagという変数。
- 「ユーザーが有効かどうか」を表す出来事が起きたとき変わる →
is_active - 「処理が完了したかどうか」を表す出来事が起きたとき変わる →
is_completed - 「エラーが発生したかどうか」を表す出来事が起きたとき変わる →
has_error
出来事が3種類あるってことは、「意味が3つあるflagが1個いる」か「ちゃんと名前がついた変数が3個いる」かのどちらかってこと。前者を選んだとき、田中さんの引き出し問題が発生する。
同じくprocess()という関数。
- 注文データを処理する出来事 →
process_order() - ユーザー登録を処理する出来事 →
register_user() - 支払いを処理する出来事 →
charge_payment()
「どんな出来事が起きたとき変わるか」が答えられないなら、その名前はまだ幽霊ネームなんだよね。
まとめるとこういうことね
| 幽霊ネーム | 意図を表す命名 |
|---|---|
flag, data, tmp, result | is_active, user_list, temp_file_path, filtered_orders |
関数名が process() handle() execute() | filter_active_users() charge_payment() send_welcome_email() |
| 読む人が処理を全部追わないと意味がわからない | 名前を見ただけで意図が伝わる |
| テストの組み合わせが爆発する | テストすべきケースが名前から自明になる |
| 仕様変更のたびに名前の解読コストが発生する | 変更箇所が名前で特定できる |
命名は「動くコードを書く作業」じゃなくて、「次にここを触る人に地図を渡す作業」なんだよね。田中さんの引き出しを引き継ぐことになった山田さんにならないために、ラベルをちゃんと貼ろ。🫶
沼りたい人はこっちも読んで📖
書籍
- Robert C. Martin 著「Clean Code: A Handbook of Agile Software Craftsmanship」(2008, Prentice Hall)
命名設計の基礎はすべて第2章「Meaningful Names」に詰まっている。英語だが図が少なく読み進めやすい。 - Dustin Boswell, Trevor Foucher 著「リーダブルコード ——より良いコードを書くためのシンプルで実践的なテクニック」(2012, オライリー・ジャパン)
日本語で読める命名設計入門として最適。第2章・第3章が命名に特化している。 - Kent Beck 著「Implementation Patterns」(2007, Addison-Wesley)
Intention-Revealing Nameの一次資料。命名をシステマティックな視点で論じている。
一次資料
- Martin Fowler: Two Hard Things(martinfowler.com)
Phil Karltonの格言「コンピュータサイエンスで難しいことは2つ——キャッシュの無効化と命名」をFowlerが紹介したページ。 - Clean Coder Blog(blog.cleancoder.com)
Uncle Bob自身による補足。命名原則の背景が読める。
無料で読める入門記事
- Qiita・Zennで「命名規則」「Intention-Revealing Names」で検索すると日本語の具体例が大量に出てくる。
Dev-Here「ギャルでも分かる設計の勘ドコロ!」シリーズ 第6回