RSSで Stop Naming Your Variables “Flag”: The Art of Boolean Prefixes という記事が流れてきた。bool変数の命名規則の話。

記事の要点

主張はシンプルで、bool変数にflagやdoneのような中身のない名前をつけるな、というもの。flagdoneは「何の」フラグなのか、「何が」完了したのかが変数名から一切わからず、読み手は呼び出し元や定義側のコードを読みに行かないと意味が取れなくなる。

これを解消するために、is(状態)、has(包含)、can(機能)、should(意図・ビジネスルール)の4つのプレフィックスで命名すれば大半のケースをカバーできるとしている。あわせて「ネガティブな名前を使うな」というルールも強調されていて、isDisabledのような名前は将来!isDisabledという二重否定を生むので避け、isEnabledのように常にポジティブな形で持つべきだとしている。

もう一つ面白かったのが、これらのルールは状態を表すプロパティにはよく効くが、引数として渡されるboolには効かないという指摘。意味の伴わないbool引数を複数並べたメソッドは、呼び出し側のコードを読んだだけでは何が起きるか判断できない設計上の欠陥で、これを解消する手段としてメソッド分割・Enum・Configオブジェクトの3パターンが挙げられている。

詳細は元記事を読んでほしい。

「メソッド分割って結局引数じゃないの?」を考えた

ここから自分の考察。メソッド分割・Enum・Configオブジェクトの3パターンが並列に挙げられているのを見て、「メソッド分割だけ毛色が違うのでは」と思った。EnumもConfigオブジェクトも結局は引数として何かを渡している。メソッド分割だけが「引数を使わない」解決策に見える。

整理すると、これは「引数を使うかどうか」の話ではなく「型に意味を持たせているかどうか」の話になる。bool単体は型として「真か偽か」以上の情報を持たない。これを呼び出し側で見た時に何を意味するか判断できないのが元の問題で、3パターンはどれも「呼び出し側だけで意味が完結する形に情報を昇格させる」という同じ操作をしている。メソッド名に意味を込めるか、Enumの値に意味を込めるか、オブジェクトのプロパティ名に意味を込めるかの違いでしかない。

この観点で見ると使い分けの基準も見えてくる。状態が二択でこの先も増えない見込みならメソッド分割で呼び出し側の読みやすさを最大化できる。一方、状態が3つ以上ある、または今後増える見込みがあるならメソッド分割は破綻しやすい。「即時送信」と「キュー送信」の2メソッドに「リトライあり」という軸が加わると、組み合わせの数だけメソッドが必要になり、命名が事故る。こうなったらEnumかConfigオブジェクトに切り替えるべきサイン。

経験則として、メソッド名に「And」を入れたくなった瞬間が分割すべきタイミングだと思っている。「送信かつキュー投入」のような名前は、本来排他な選択肢を無理やり1つの操作として表現しようとしているか、1つの関数が複数の責務を抱え込んでいるかのどちらかで、どちらにせよ設計を見直すべきサインになる。

長い変数名・関数名はなぜダメなのか

ここからは命名の話を少し広げる。booleanの話とは別に、変数名や関数名が長すぎることそのものを問題視する意見もよく見る。これはbooleanの「曖昧すぎる」問題とは逆方向の弊害で、原因は大きく2つに分けられると思う。

一つは、命名で背負わせている責務が多すぎること。getUserDataAndValidateAndSendNotificationのような名前は、まさに「And」がそのまま並んでいる例で、これは命名の問題ではなく関数の設計自体の問題。複数の責務を1つの関数に詰め込んだ結果として名前が膨張している。この場合、長い名前を短くしようとするのではなく、関数自体を分割して短い名前に戻すのが正しい対処になる。

もう一つは、文脈の不足を変数名で無理やり補おうとしていること。例えばuserServiceUserIdForBillingCalculationのような名前は、本来クラス名や引数の型、コメント、あるいはスコープの狭さで伝えられるはずの文脈を、変数名一つに全部押し込めようとして起きる。クラス名がすでにBillingCalculatorなら、そのメソッド内でuserIdとだけ書けば文脈はクラス名から自然に補完される。長い名前が必要になっている時点で、実はそのクラスやモジュールの設計自体が文脈を提供できていない、というシグナルとして読むこともできる。

つまり長すぎる名前は、それ単体の修正対象というより「設計のどこかに無理が生じている」ことを教えてくれる兆候として扱うのが妥当だと思う。

デザインパターンとの接続

Boolean Trapの3パターン(メソッド分割・Enum・Configオブジェクト)は、実はそれぞれ古典的なデザインパターンの入り口になっている。

メソッド分割を突き詰めると、状態ごとに振る舞いを切り替えるクラス設計が必要になる場面が出てくる。状態の数がさらに増え、状態遷移そのものに意味を持たせたくなったら、Stateパターン(状態をオブジェクトとして切り出し、状態ごとに振る舞いをカプセル化する)に近づいていく。

Configオブジェクトは、複雑な初期化や生成のロジックを切り出したくなった時点でBuilderパターン(オブジェクトの組み立て手順そのものを別オブジェクトに任せる)と接続する。new ExportOptions { ... }のようなオブジェクトリテラルで足りているうちはConfigオブジェクトのままでいいが、必須項目とオプション項目が増えて初期化の組み合わせ自体が複雑になったら、Builderへ移行するタイミングになる。

Enumで振る舞いを切り替える設計が増えてくると、Strategyパターン(アルゴリズムや処理そのものを差し替え可能なオブジェクトとして扱う)とも近づく。Enumの値でswitch文を量産し始めたら、それは「Enumに対応する振る舞いをオブジェクトとして注入する」設計に切り替えるサインになる。

いずれも、最初からこれらのパターンを持ち出す必要はなく、Boolean Trapの3パターンで対処しているうちに複雑さが増した時に「次に進む先」として用意されている、という位置づけで捉えるのがちょうどいいと思う。

クリーンアーキテクチャとの接続

もう一段引いて見ると、これらの命名・設計判断は層の責務分離とも関係してくる。

クリーンアーキテクチャでは、ドメイン層(ビジネスルール)とインフラ層(外部APIやDBとのやり取り)を明確に分離する。「ネガティブな名前は外部APIとの境界線だけに押し込めて、ドメイン層には持ち込まない」という元記事のルールは、まさにこの境界の話そのものになっている。外部の都合(ネガティブな命名、bool引数の羅列)はインフラ層やアダプタ層で吸収し、ドメイン層には意味の伴った型(Enum、Stateオブジェクト、Configオブジェクト)だけを渡す、という設計にすれば、層をまたぐたびに命名の濁りが伝播するのを防げる。

逆に言うと、ドメイン層の奥深くまでbool引数の羅列やネガティブな命名が浸透している場合、それは単なる命名の問題ではなく、本来インフラ層で吸収すべき外部の都合がドメイン層まで漏れ出している設計上の兆候として読むこともできる。

まとめ

booleanの命名規則自体はそこまで難しい話ではないが、そこから「なぜそのルールが効くのか」を自分で詰めていくと、長い変数名の話、デザインパターン、層の分離まで地続きにつながっている。命名は単体の作法というより、設計全体の健全さを映す表面の一つだと捉えておくとよさそうだった。

元記事