Geminiが作ったUtilityクラスについてのレポート

By あままこ(amamako) (@darakeru.com)
Published:

Gemini 2.5 Proに以下のような質問を投げてみたら、面白いレポートを作成してきたので共有してみる。

『リーダブルコード』では、汎用的なUtilityを切り分けることを推奨してるけど、他方でオブジェクト思考においてはUtility Classはアンチパターンとされていることもある。それを踏まえて、Utility Classはどのような場面で使うべきだと思う?

ユーティリティクラスのパラドックス:読みやすさとオブジェクト指向設計を両立させるための実践的アーキテクトガイド

はじめに

ソフトウェア開発、特にオブジェクト指向プログラミング(OOP)の領域において、古くから存在する一つの緊張関係があります。それは、局所的なコードの可読性を追求するプラクティスと、大局的なアーキテクチャの健全性を維持するという原則の間に生じるものです。この緊張関係は、開発者が直面する「『リーダブルコード』は汎用的なUtilityの切り分けを推奨するが、オブジェクト指向設計ではUtilityクラスはアンチパターンとされる」という鋭い問いに見事に集約されています。

この問いは、単純な二者択一で解決できるものではありません。むしろ、現代のソフトウェア開発における中心的なジレンマの一つを浮き彫りにしています。本レポートの目的は、この一見矛盾する二つの見解を単に並べるのではなく、それらを統合し、より高次の設計思想へと昇華させることにあります。そのゴールは、開発者が日々のコーディングにおいて、自信を持って設計上の意思決定を下せるような、明確かつ実践的なフレームワークを提供することです。

本稿では、まず『リーダブルコード』が推奨する汎用コード抽出の価値を深く掘り下げ、次いでUtilityクラスがなぜアンチパターンと見なされるのか、その構造的な問題をオブジェクト指向の諸原則に照らして解剖します。そして、この二つの視点を統合する核心的な判断基準を提示し、具体的なリファクタリング戦略や、現代のプログラミング言語が提供するより洗練された代替案を探求します。最終的に、開発者が局所的な読みやすさと大局的な柔軟性・保守性を両立させ、真に堅牢なソフトウェアを構築するための知見を提供することを目指します。

第1部 明瞭性の追求:『リーダブルコード』の主張の解体

コードの品質を議論する上で、『リーダブルコード』が提示した原則は、多くの開発者にとって指針となっています。特に、汎用的なコードを切り出すという推奨は、コードの可読性を向上させるための強力な手法として広く受け入れられています。このセクションでは、その主張の核心を解体し、なぜそれが重要なのかを明らかにします。

1.1 「無関係の下位問題を抽出する」という原則

『リーダブルコード』の第10章で提唱されている中心的なアイデアは、「無関係の下位問題を抽出する」ことです 1。これは、ある関数やコードブロックをレビューし、「このコードのハイレベルな目標は何か?」と自問することから始まります。そして、コードの各行がその高レベルな目標に直接貢献しているか、あるいは、目標とは直接関係ない下位問題を解決しているかを判断します 1。

例えば、ユーザー情報を処理する関数の中に、複雑な文字列フォーマットや日付のパース処理が混在している場合、これらの処理は「無関係の下位問題」と見なされます。これらを抽出して別の関数に切り出すことで、元の関数は「ユーザー情報の処理」という本来の目的に集中できます。この抽出の主な目的は、認知負荷の軽減です。開発者は、主要なロジックを読む際に、文字列操作やファイルI/Oといった詳細に気を取られることなく、コードの全体像をスムーズに理解できるようになります 2。

このプラクティスは、本質的には「手続き的分解」と「抽象化」という、プログラミングにおける普遍的な原則に基づいています。しかし、この一見単純で有益な行為が、オブジェクト指向の文脈において新たな問いを生じさせます。すなわち、「抽出したこの汎用的なコードは、どこに配置すべきか?」という問題です。非OO言語であればモジュールやヘッダーファイルがその受け皿となるでしょう。しかし、JavaやC\#のようなクラスベースの言語では、最も手軽な選択肢として「静的なUtilityクラス」が選ばれがちです。開発者が『リーダブルコード』の教えに従い、可読性のためにコードを抽出し、その置き場所に窮した結果、意図せずしてアンチパターンへの第一歩を踏み出してしまうのです。問題は抽出行為そのものではなく、そのコードを収めるアーキテクチャ上の「容器」の選択にあります。

1.2 抽出がもたらす具体的なメリット

汎用的なコードを抽出することには、多くの具体的な利点があります。

可読性と集中の向上: 低レベルの詳細をうまく命名された関数に移動させることで、主要なコードパスは、その目的を物語るような、明確な一連のステップとして記述できます 2。これにより、コード全体の流れがスムーズになり、読み手の混乱を防ぎます。 再利用性: 抽出された汎用コードは、特定のプロジェクトのロジックから独立しているため、複数のプロジェクトで再利用できます。これにより、コードの重複が削減され、開発効率が向上します 1。 保守性と改善の容易さ: 独立したユーティリティ関数は、特定のビジネスロジックから切り離されているため、改善や最適化が容易になります。例えば、ある文字列操作関数のパフォーマンスを改善したい場合、その関数だけを修正すればよく、他の部分への影響を心配する必要がありません 1。 ミクロレベルでのテスト容易性: 例えば、特定のフォーマットに文字列を変換するような小さな純粋関数は、様々な入力値を用いて単体テストを行うことが非常に容易です。これにより、その関数の正しさを隔離された環境で保証できます。

1.3 警鐘:小さな関数の作りすぎ問題

一方で、『リーダブルコード』自身も、この抽出アプローチの行き過ぎには注意を促しています。あまりにも多くの小さな関数にロジックを分割しすぎると、開発者はコードの振る舞いを理解するために、あちこちのファイルや定義にジャンプし続けなければならなくなります。これにより、かえってコードの全体像が掴みにくくなり、可読性が損なわれる可能性があります 1。この警告は、次に議論するUtilityクラスのアンチパターンがなぜ問題となるのかを理解する上で、重要な布石となります。

第2部 アーキテクチャ上の警告:Utility/Helperアンチパターンの解剖

『リーダブルコード』が推奨するコードの抽出は、局所的な可読性を高める上で非常に有効です。しかし、その抽出されたコードの受け皿として安易に「Utilityクラス」を選択すると、オブジェクト指向設計の根幹を揺るがすアンチパターンに陥る危険性があります。このセクションでは、Utilityクラスがなぜ問題視されるのか、その構造を解剖します。

2.1 アンチパターンの定義

Utilityクラス(またはHelperクラス)とは、主に静的メソッド(static method)で構成され、インスタンス化されることを意図していないクラスを指します 3。これらのクラスは、

Util、Common、Helperといった汎用的な名前が付けられることが多く、特定の状態を持たずに様々な場面で再利用できる処理を集約する目的で作成されます 3。

ここで、「Utility」クラスと「Helper」クラスを区別すると、より深い理解が得られます。「Utility」クラスは、java.lang.Mathのように、状態を持たず、完全に汎用的な機能を提供します。一方、「Helper」クラスは、UserHelperのように特定のクラス(この場合はUser)を補助する目的で作成されることが多く、これはより強いコードの臭い(code smell)と見なされます 6。なぜなら、その存在自体が、補助対象のクラスの設計に不備があることを示唆しているからです。

2.2 中核にある設計上の欠陥:オブジェクト指向原則への違反

Utilityクラスがアンチパターンとされる根本的な理由は、オブジェクト指向の基本原則に違反する点にあります。

単一責任の原則(Single Responsibility Principle \- SRP): CommonUtilやAppUtilsといった名前のクラスは、明確な単一の責任を持ちません。その「責任」は、「他のどこにも収まらない雑多な機能を集めること」になりがちです。結果として、日付フォーマット、ファイル操作、文字列処理、データベース接続といった、互いに無関係な機能が同じクラスに詰め込まれ、肥大化した非凝集的な「神クラス(God Class)」が誕生します 3。 機能の羨望(Feature Envy)と低い凝集度: これは深刻なコードの臭いです。例えば、UserHelperクラスにcalculateAge(User user)というメソッドがあり、Userオブジェクトを受け取ってその公開データから年齢を計算する場合、このメソッドはUserクラスの機能を「羨んで」いる状態です 10。このロジックは、本来 Userクラス自身の振る舞いとしてカプセル化されるべきです (user.calculateAge())。Helperクラスの存在は、多くの場合、元のクラスのAPIが不完全であったり、設計が不十分であったりすることの証左となります 6。 手続き型プログラミングへの回帰: Utilityクラスの利用は、しばしばオブジェクト指向の皮を被った手続き型思考の表れです。オブジェクトにメッセージを送って振る舞いを依頼する(例:user.calculateAge())代わりに、開発者はオブジェクトのデータを引数として受け取り、外部から操作する手続き(例:UserUtil.calculateAge(user))を書いてしまいます 5。これはカプセル化を破壊し、データと振る舞いが分離された貧血ドメインモデル(Anemic Domain Model)を助長します。

2.3 名前の臭い

クラス名自体が、設計上の問題を強く示唆しています。Util、Common、Manager、Helperといった名前は曖昧で、そのクラスが具体的に何をするのかを伝えません 3。責任が曖昧であるため、開発者は新しい機能を追加する際に、深く考えることなく安易にこれらのクラスを「ゴミ箱」のように利用してしまいます。優れた設計では、クラスはその責任を明確に反映した名前を持ちます。例えば、「通貨を変換する」責務を持つクラスは

CurrencyConverter、「Markdownを解析する」責務を持つクラスはMarkdownParserと名付けられるべきです 6。

一度CommonUtil.javaのようなファイルがプロジェクトに存在すると、それは「割れ窓」のように機能します。つまり、その存在が「このプロジェクトではアーキテクチャの一貫性は重要視されていない」という無言のメッセージとなり、他の開発者が安易に無関係なメソッドを追加する心理的な障壁を下げてしまうのです。例えば、ある開発者が郵便番号を検証する機能を追加する必要に迫られたとします。プロジェクト内に既に2000行を超えるCommonUtilクラスが存在し、そこには日付処理や文字列操作が混在しているのを見れば、前例に倣うのが最も簡単な道です。「このロジックはAddress値オブジェクトに属するべきか?」「AddressValidationServiceのような新しいサービスを作るべきか?」といった設計上の考察を放棄し、public static boolean validateZip(String zip)をCommonUtilに追加してしまうでしょう。こうして、アンチパターンは自己増殖していくのです。

第3部 技術的基盤:静的メソッドとそのシステム全体への影響

Utilityクラスがアンチパターンとされる技術的な核心は、その実装に多用される「静的メソッド(static method)」の性質にあります。静的メソッドは一見便利に見えますが、その利用はコードの結合度、拡張性、そしてテスト容易性に深刻な影響を及ぼし、システム全体の健全性を損なう可能性があります。

3.1 問題の根源:静的結合

MyUtils.doSomething()のような静的メソッドの呼び出しは、コンパイル時にMyUtilsという具象クラスへの依存関係をハードコードします 12。これにより、呼び出し元のクラスと

MyUtilsクラスは密に結合され、柔軟性のない硬直したコード構造が生まれます。

これは、コンポーネントが具象クラスではなく抽象(インターフェース)に依存することを推奨する、現代的な依存性注入(Dependency Injection, DI)アーキテクチャの思想とは正反対です。静的メソッドへの呼び出しは、DIコンテナによる依存関係の管理を迂回し、コンポーネントの独立性を著しく低下させます。

3.2 SOLID原則への違反

静的メソッドを多用するUtilityクラスは、オブジェクト指向設計の基本原則であるSOLID原則のいくつかに違反します。

依存性逆転の原則(Dependency Inversion Principle \- DIP): この原則は「上位のモジュールは下位のモジュールに依存してはならない。両方とも抽象に依存すべきである」と定めています。しかし、静的メソッドを持つUtilityクラスはインターフェースを実装することができず、DIコンテナを介して注入することもできません。MyUtils.doSomething()を呼び出すクラスは、MyUtilsという具体的な実装に永久に束縛されます。テスト時にMockUtilsに差し替えたり、将来的にMyNewUtilsという新しい実装に切り替えたりするには、呼び出し元のコードを直接変更するしかありません 9。これはDIPの最も重大な違反です。 オープン・クローズドの原則(Open-Closed Principle \- OCP): この原則は「ソフトウェアのエンティティは拡張に対して開いており、修正に対して閉じているべきである」と述べています。静的メソッドはサブクラスでオーバーライドすることができないため、その振る舞いを変更したり、ポリモーフィズムを利用して代替実装を提供したりすることができません 8。機能を変更または拡張したい場合、開発者はUtilityクラスのコードを直接編集するしかなく、これは「修正に対して閉じている」という原則に明確に違反します。

3.3 テストの悪夢

純粋な静的メソッド(入力のみに依存し、副作用がない関数)自体をテストすることは非常に簡単です 13。しかし、その静的メソッドを

利用するクラスをテストすることは、極めて困難になります 8。

モック化の問題: 標準的なモックフレームワーク(Mockitoなど)は、インターフェースを実装したプロキシオブジェクトやサブクラスを動的に生成することで機能します。静的メソッドはオーバーライドも注入もできないため、これらの標準的なテクニックは通用しません。 侵襲的なツールへの依存: 静的メソッドをモックするためには、PowerMockitoのような特殊なライブラリに頼らざるを得ません。これらのツールは、バイトコード操作やカスタムクラスローダーといった「力づく」の技術を用いて、実行時に静的メソッド呼び出しを乗っ取ります 9。このアプローチは、テストの実行を遅くし、設定を複雑にし、そして非常に壊れやすいです。このような侵襲的なツールが必要になること自体が、設計に欠陥があることの強い兆候と言えます。 状態を持つ静的メソッドの危険性: もし静的メソッドがデータベース、ファイルシステム、ネットワーク、あるいはグローバルな静的変数といった外部の状態と相互作用する場合、事態はさらに悪化します 14。この静的メソッドを呼び出すテストは、現実のファイルシステムに書き込みを行ったり、データベースに接続しに行ったりするため、実行が遅く、結果が予測不能になります。これは、高速で決定論的、かつ隔離されているべき単体テスト(unit test)の目的を完全に破壊します。

静的メソッドの呼び出しは、いわば「隠れた依存関係」です。new MyService(new UserRepository(), new EmailClient())のようなコンストラクタを見れば、MyServiceが何に依存しているかは一目瞭然です。これはクラスの「契約」として明示されています。しかし、クラスのプライベートメソッドの奥深くでFileUtils.readFile()のような静的メソッドが呼び出されている場合、それはコンストラクタの契約には現れない、不可視で管理外の依存関係となります。開発者がMyServiceを単体テストしようとするとき、コンストラクタを見てUserRepositoryとEmailClientをモックするでしょう。しかしテストはFileNotFoundExceptionで失敗します。デバッグの末、彼らは隠れた静的呼び出しが実際のファイルシステムにアクセスしようとしていたことに気づきます。このように、静的呼び出しはDIメカニズムを迂回し、クラスの公開APIからは予測できない依存関係を導入するため、コードの理解とリファクタリングを著しく困難にするのです。

第4部 統合:Utilityコードのための意思決定フレームワーク

これまでの議論で、『リーダブルコード』が推奨するコード抽出の価値と、Utilityクラスがもたらすアーキテクチャ上の弊害という、二つの対立する側面を見てきました。このセクションでは、これら二つの視点を統合し、開発者が「いつ、どのようにユーティリティコードを作成すべきか」を判断するための、実践的な意思決定フレームワークを提示します。

このフレームワークは、開発者が直面するパラドックスを解決するための羅針盤となります。『リーダブルコード』が推奨する「良い」ユーティリティと、アンチパターンとなる「悪い」ユーティリティを明確に区別するための基準を提供します。

4.1 最重要のリトマス試験紙:状態と副作用

ユーティリティとして切り出す関数が許容されるかどうかを判断する最も重要な基準は、その関数が\\純粋関数(Pure Function)\\であるか、あるいはそれに限りなく近いか、という点です。

純粋関数の定義:

良い候補(Stateless): Math.sin()、文字列フォーマット関数、チェックサム計算関数、日付オブジェクトをISO 8601形式の文字列に変換する関数など。これらは入力のみに依存し、外部の状態を変更しません 14。 悪い候補(Stateful): データベース、ファイルシステム、ネットワーク、システムクロック、グローバルな静的変数と相互作用するあらゆる関数。これらの関数の出力は外部の状態に依存し、また外部の状態を変更する可能性があります 14。

4.2 基準2:ドメイン特異性

次に問うべきは、「そのロジックは真に汎用的でアプリケーション非依存か、それともビジネスルールを含んでいるか」という点です。

良い候補(Generic): is\valid\email\format(string)(メールアドレスの書式を検証する)、sanitize\html(string)(HTMLを無害化する)など。これらは特定のビジネスドメインに依存しない、普遍的に有用な処理です。 悪い候補(Domain-Specific): is\premium\customer(user)(プレミアム顧客かどうかを判定する)、calculate\order\tax(order)(注文の税金を計算する)など。これらは明確なビジネスロジックであり、ドメインモデル(例:UserやOrderクラスのメソッド)や、ドメインサービス(例:TaxCalculationService)に属するべきです。

4.3 基準3:凝集度

提案されているUtilityクラス内のすべての関数が、単一の、狭く、明確な目的に関連しているかどうかも重要です。

良い候補(Cohesive): 通貨のフォーマットとパースに関するメソッドのみを持つCurrencyUtilsクラス。ファイルシステムのパス操作に特化したPathUtilsクラス。これらの名前自体が、高い凝集度を示唆しています。 悪い候補(Incoherent): 日付フォーマット、データベース接続ハンドリング、XMLパースのメソッドが混在するCommonクラス。これは典型的な低凝集のアンチパターンです 3。

4.4 意思決定マトリクス

これらの基準をまとめたものが、以下の意思決定マトリクスです。開発者が新しいユーティリティ的な関数を作成しようとする際に、このマトリクスをチェックリストとして利用することで、設計上の誤りを未然に防ぐことができます。

| 評価基準 | 良い候補(Utility関数/モジュールとして許容) | 悪い候補(注入可能なオブジェクト/サービスとしてリファクタリングすべき) | | :---- | :---- | :---- | | 状態と副作用 | 純粋にステートレス。入力のみに依存し、副作用がない 14。 | 状態を管理・操作する。DB、ファイルI/O、グローバル変数などと相互作用する 14。 | | 依存関係 | 外部サービスへの依存がない。自己完結している。 | 外部サービス(DB、ネットワーク、APIなど)に依存する。 | | ドメインロジック | アプリケーション非依存。汎用的なアルゴリズムや変換処理。 | ビジネスルールやドメイン固有の知識を含んでいる。 | | 凝集度 | クラス/モジュール内の全関数が単一の狭い概念を共有している(例:StringUtils)6。 | 互いに関連性のないタスクの寄せ集めになっている(例:CommonUtils)3。 | | テスト容易性 | 隔離された環境で容易に単体テストが可能。 | テストに複雑なセットアップや侵襲的なモックツールが必要になる 9。 |

このフレームワークを用いることで、当初のパラドックスは解消されます。『リーダブルコード』が抽出を推奨する「無関係の下位問題」の例(文字列操作など)は、このマトリクスの「良い候補」の条件(ステートレス、汎用的、凝集的)にほぼ完全に合致しています 1。一方で、アンチパターンとして批判されるUtilityクラスは、「悪い候補」の列に分類されるような、状態を持つ、ドメイン固有の、非凝集的な関数を含んでしまっているのです。つまり、両者のアドバイスは元々矛盾していたのではなく、後者にはアーキテクチャ的な文脈が欠けていただけだったのです。

第5部 モダンな代替案と言語固有のイディオム

これまで議論してきた古典的なUtils.javaのようなアンチパターンは、ある意味で、古い言語パラダイムの制約によって生まれた側面があります。現代のプログラミング言語は、この問題をよりエレガントに解決するための、洗練された機能を提供しています。これらの機能は、コードの再利用性と分離という本来の目的を、よりオブジェクト指向や関数型の思想に沿った形で実現します。

5.1 モジュール・アズ・名前空間パターン(Python, TypeScript/JavaScript)

Pythonのような言語では、ステートレスな関数を不必要にクラスでラップすることは推奨されません。代わりに、関連する関数群を一つのファイル(モジュール)にまとめます。例えば、数学関連のユーティリティ関数はmath\utils.pyというファイルに集約されます 16。

利用する側は、import math\utilsのようにモジュールをインポートし、math\utils.calculate\mean(...)といった形で関数を呼び出します。これにより、不要なstaticキーワードやクラス定義の煩雑さなしに、自然な形で名前空間による整理が実現されます。これは、ユーティリティ関数を組織化するための、直接的でクリーンなアプローチです 17。

5.2 シンタックスシュガー・パターン(C\#とKotlinの拡張メソッド/関数)

これは、元のクラス(例えば、標準ライブラリのstringやサードパーティ製のクラス)を直接変更できない場合に、「機能の羨望(Feature Envy)」問題を解決する、最もエレガントな手法の一つです 19。

仕組み: 開発者は静的メソッドを定義しますが、それを特定の型に対する「拡張(extension)」としてマークします。するとコンパイラは、その静的メソッドをあたかもその型のインスタンスメソッドであるかのように呼び出すことを許可します。例えば、StringUtils.ToTitleCase("my-string")と書く代わりに、"my-string".ToTitleCase()と書けるようになります 21。

利点: インスタンスメソッドのような高い可読性を享受しつつ、実装は物理的に分離され、凝集性を保つことができます。これは、JavaでStringHelperのようなクラスが生まれる原因となった問題を直接的に解決します 6。

欠点: 多用しすぎると、どのメソッドが本来のクラスのもので、どれが拡張メソッドなのかが分かりにくくなり、名前空間を汚染する可能性があります。また、IDEの支援なしには、メソッドの定義場所を探すのが困難になることもあります 21。

5.3 コンポジション・パターン(RubyのMixin)

Rubyでは、モジュール(Mixin)を用いてクラス間で機能を共有します。例えば、Loggingというモジュールを定義し、それを任意のクラスにincludeするだけで、そのクラスにロギング機能を追加できます 25。

これは実装の多重継承の一形態であり、非常に強力ですが、メソッド名の衝突や複雑な継承階層(いわゆる「スパゲッティ継承」)を生み出す危険性もはらんでいます 26。

5.4 TypeScriptのユーティリティ型

これらは関数ではありませんが、「ユーティリティ」という概念に関連するモダンなアプローチです。TypeScriptのユーティリティ型(Partial\<T\>, Pick\<T, K\>, Omit\<T, K\>など)は、型定義を操作するための、組み込みの「型のための関数」です 28。これらはグローバルに利用可能で、定型的な型変換をボイラープレートコードなしに実現します 30。これは、再利用可能な「ヘルパー」という概念を、型システムのレベルで実現した現代的なアプローチと言えます。

これらの言語機能の進化は、明確なトレンドを示しています。それは、言語設計者自身が古典的な静的Utilityクラスの不便さや問題点を認識し、同じ目的(コードの再利用と分離)を達成するための、より優れた第一級のツールを提供しようとしている、というトレンドです。例えば、「Stringクラスにメソッドを追加したいが、できない」という問題に対して、Javaの歴史的な答えは「StringUtilsクラスを作る」でした。一方、C\#の答えは「拡張メソッドを使う」です。この文脈において、C\#の解決策は客観的により優れています。なぜなら、オブジェクトの豊かなインターフェースという幻想を保ちつつ、物理的な実装の分離を維持できるからです。これは、アンチパターンがしばしば「言語の制約を回避するためのパターン」であり、その制約が取り除かれればパターン自体も陳腐化することを示しています。

第6部 実践的戦略:リファクタリングとベストプラクティス

理論的な理解を深めた上で、次はその知識を日々の開発にどう活かすかという実践的な側面に焦点を当てます。ここでは、新規プロジェクト(Greenfield)と既存プロジェクト(Brownfield)の両方で、Utilityクラスのアンチパターンを回避し、健全なアーキテクチャを構築するための具体的な戦略を提示します。

6.1 Greenfieldプロジェクト:最初から正しく設計する

新しいプロジェクトを開始する際は、アンチパターンが生まれる土壌を作らないことが肝要です。

意思決定マトリクスの活用: 開発チーム内で第4部で提示した「意思決定マトリクス」を共有し、ユーティリティ的なコードを実装する際の判断基準とします。新しい関数が状態を持つか、ドメインロジックを含むかを常に問い、マトリクスの「悪い候補」に該当する場合は、安易に静的メソッドとするのではなく、注入可能なサービスオブジェクトとして設計する文化を醸成します。 具体的な命名規則: クラス名には、FormatUtilsのような曖昧な名前ではなく、CurrencyFormatterやMarkdownParserのように、その責任を具体的かつ明確に表す名前を付けます 6。これにより、クラスの責任範囲が限定され、「何でも屋」になるのを防ぎます。 サービスオブジェクトをデフォルトに: 状態を持つ可能性が少しでもある処理や、ビジネスロジックに関わる処理は、デフォルトで注入可能なインスタンスベースのサービスオブジェクトとして設計します。静的なユーティリティメソッドは、純粋関数であることが明白な場合にのみ許容される「例外」と位置づけます。

6.2 Brownfieldプロジェクト:アンチパターンのリファクタリング

既存の巨大なCommonUtilクラスを解体するのは困難な作業ですが、体系的なアプローチによって管理可能です。

ステップ1:分析と分類: まず、巨大なUtilityクラス内に存在するすべてのメソッドをリストアップし、意思決定マトリクスの基準(状態の有無、ドメイン特異性、関連性など)に従って分類します。これにより、どのメソッドをどこに移動すべきかの全体像が明らかになります。 ステップ2:「メソッドの移動」で機能の羨望を解消: calculateAge(User user)のように、特定のドメインオブジェクトを主たる引数とし、そのデータを操作しているメソッドを特定します。これらは「機能の羨望」の典型例です。リファクタリング手法「メソッドの移動(Move Method)」を適用し、これらのメソッドを本来あるべきドメインオブジェクト(この場合はUserクラス)のインスタンスメソッド(user.calculateAge())として移動させます 6。 ステップ3:「クラス/サービスの抽出」で責任を分離: 税金計算関連のメソッド群のように、特定のドメインロジックを扱う凝集度の高いメソッド群が見つかった場合、リファクタリング手法「クラスの抽出(Extract Class)」を適用します。これらのメソッドを、TaxServiceのような新しい非静的クラスに抽出し、ITaxServiceのようなインターフェースを定義します。そして、このサービスをDIコンテナで管理し、必要な場所に注入します 6。 ステップ4:真に凝集したユーティリティの作成: 上記のステップで移動されずに残った、純粋でステートレスな汎用関数群を、その機能に基づいて再分類します。そして、StringUtils、DateUtils、CollectionUtilsといった、凝集度の高い、具体的な名前を持つ新しいユーティリティクラス(またはモジュール)に整理します。

6.3 命名の力

リファクタリングの過程全体を通じて、命名の重要性を常に意識することが不可欠です。HelperやUtilといった接尾辞を避け、クラスの責任を正確に表現する名前を選ぶだけで、そのクラスが将来的にアンチパターンへと堕落するのを防ぐ強力な抑止力となります。XmlTransformerやImageResizerといった名前は、そのクラスが何をすべきで、何をすべきでないかを明確に示してくれるのです 6。

結論

本レポートで探求してきた「Utilityクラスのパラドックス」は、局所的なコードの読みやすさと大局的なアーキテクチャの健全性という、二つの重要な価値観の間に存在する緊張関係から生じます。しかし、この分析を通じて明らかになったのは、これが真の矛盾ではないということです。

『リーダブルコード』が提唱する「無関係の下位問題を抽出する」というアドバイスは、コードをクリーンに保つための、正しく価値あるプラクティスです。アンチパターンは、この抽出行為そのものから生まれるのではありません。それは、抽出されたコードの受け皿として、アーキテクチャ的に不適切な「容器」――すなわち、状態を持ち、ドメインロジックを含み、非凝集的な、モノリシックな静的Utilityクラス――を選択してしまうことに起因します。

このジレンマを解決するための鍵は、明確な意思決定フレームワークを持つことです。我々が作成するユーティリティ的なコードは、以下の三つの原則に照らして厳しく評価されなければなりません。

これらの基準を満たすものだけが、静的なユーティリティとして許容されます。それ以外の、状態を持つ、あるいはドメインロジックを含む機能は、依存性注入によって管理される、適切に設計されたオブジェクト(サービスやドメインオブジェクト)の責務とすべきです。

さらに、C\#の拡張メソッドやPythonのモジュールといった現代的な言語機能は、この問題をよりエレガントに解決する道を示しています。これらは、言語の制約から生まれた古いパターンに固執するのではなく、より洗練されたツールを積極的に採用することの重要性を教えてくれます。

最終的に、この問題を乗り越えた開発者は、偽りの二項対立から解放されます。彼らは、局所レベルで高い可読性を持ちながら、大局レベルではテスト可能で、柔軟かつ堅牢なアーキテクチャを構築する能力を手にします。それは、適切な場面で適切なツールを使い分ける知恵に他なりません。真のユーティリティには純粋関数を収めたシンプルなモジュールを、そしてそれ以外のすべてには適切に設計されたオブジェクトを用いることで、我々は真に保守性の高い、優れたソフトウェアを創造することができるのです。

引用文献