私は昔からタイトルのようなことを思っていたが、同じようなことを主張している人はおらず、もしかしたら私が間違っているのか。と思い、あまり自信が無かったのだが、書いてみる。意見があったらTwitterとかコメント欄に書いてくれると嬉しい。
ユーティリティクラスって何だ
ユーティリティクラスとは、一つのクラスにたくさんのstaticメソッドを記述したようなものです。言語によってこのクラスはシングルトンだったり、staticクラスだったりする。
たとえば、オブジェクトを適切な文字列表現に直したり、オブジェクトを別の似たようなオブジェクトに変換したり、ログを書きだしたりするような処理は一つのクラスにまとめておいてグローバルにアクセス可能なスコープに配置し、必要になったら都度そのくらすのメソッドを呼ぶ・・・みたいな使い方をする。
私の主張
完全に要らないとは言いませんが、ほとんどの場合ユーティリティクラスは要らないのではないかと思っている。少なくとも、私のプログラミング経験の中では必要であったことはほとんどない。私の経験上での話になってしまうが、ユーティリティクラスを定義しているプロジェクトというのは、オブジェクト指向的な設計になってないのでユーティリティクラスを必要としている場合がほとんどだった。ちゃんと教科書的にオブジェクト指向設計をしていれば、ユーティリティクラスはほとんどの場合不要である、これが私の主張です。
同じような主張がないかネットで探し、日本語のページではありませんでしたがstackoverflowに私と同じような主張をしている人がいた。以下は私の勝手な意訳。
Answer:
ユーティリティクラスは必ずしも悪ではありません。ただし、良いオブジェクト指向設計の原則に反することがあります。良いオブジェクト指向設計では、殆どのクラスは一つの要素を表し、その要素のすべての属性と操作を内包します。もしあなたが何かに操作を加えるならば、その操作は「何か」のメソッドであるはずです。
しかしながら、さまざまなメソッドを一つのクラスにまとめたユーティリティクラスを使いたい時というのはあります。たとえばjava.util.Collectionsクラスです。これには、JavaのすべてのCollectionクラスで使えるたくさんのメソッドが入っています。これらは特定の一つのコレクションではなく、どのようなコレクションにも使えるようなアルゴリズムとして実装されています。
実装しなくてはならない処理は、それをするのにもっとも理にかなった場所に置くようなデザインになるよう考えるべきです。普通は、それら(実装しなくてはならない処理)は、クラスの中にある操作でしょう。しかしながら、ユーティリティクラスになる場合も実際にあります。ただし、そのような場合であっても無秩序にメソッドを放り込むのではなく、目的や機能によって整理されるべきでしょう。
私はこの回答に同意する。絶対に要らないとは言わない。でも私が今まで見てきたユーティリティクラスは不必要で、かつ、あまり良いオブジェクト指向デザインになってないと思われるものがほとんどであった。
実例
ここで、私が実際に見たことのあるユーティリティクラスと、その代替案を書いていこうと思う。以下に示すのはFileUtilというユーティリティクラスだ。
FileUtilクラスはプログラムのデータをCSVで出力するための処理をまとめたユーティリティクラスだ。下記がFileUtilクラスを使う擬似コードである。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 | private void outputCsvFile( string csvFilePath) { // check if (!FileUtil.isCsvFile(csvFilePath)) return ; // read csv InputStream is = FileUtil.getInputStream(csvFilePath); // something calculation ...... // write csv OutputStream os = FileUtil.getOutputStream(newFilePath); FileUtil.writeStream(os, processedData); } |
このユーティリティクラスの問題点は、大して処理を抽象化できていない点であると思う。おそらく、ファイルストリームを作るなどという処理はCSVファイルに限らずファイル入出力があれば必ず使うのでどこからでもアクセスできるようにしておきたかったのかもしれない。ちなみに話がそれるが、getXXStreamの中ではStreamを作る処理をすべてtry-catchで囲み、Exceptionでcatchしてcatch節の内容は空、つまり、エラーを見なかったことにするという処理を実装していた。おそらく担当者は、いちいちtry-catchと書かなくてもいいよね、程度の浅はかな考えで書いたのだろう。嘆かわしいことこの上ない。
このようなクラスは、CSVファイルを扱うという目的が決まっているのだから、CsvFileというクラスとして設計すべきだ。もし、FileUtilにCSVファイルだけでなく他のファイル入出力処理で使うメソッドが存在するのでCsvFileの中に処理を実装されては困る、という人も居るかもしれない。そういう場合はその処理を汎化させるべきだ。
つまり、以下のような設計にすべきだと考える。
すべてのファイルに共通になる処理はTextFileとしてまとめる。TextFileからCsvFileが派生する。その後、HTMLファイルについてもgetInputStreamなどの処理が必要になったときには、これもまたTextFileから派生させてメソッドを利用する。FileUtilはあらゆる場面で利用可能であるが、これでは適用範囲が広すぎる。あらゆるところから利用可能な処理は、簡単にソースコードの可読性と保守性を落とす。処理は適切な場所で実装され、スコープが制限されるべきだ。
先ほどの擬似コードと同じ処理をCsvFileを使って実装すると、以下のようになる。
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 | private void outputCsvFile( string csvFilePath) { // check if (!CsvFile.isCsvFile(csvFilePath)) return ; // read csv CsvFile csv = new CsvFile(csvFilePath); // something calculation ...... // write csv csv.writeToNewFile(newFilePath); } |
CSVファイルがオブジェクトとして扱われ、処理がクラス中に隠ぺいされたので、処理対象と、それに対して何をしているかが読み取り易い。スーパークラスとしてTextFileクラスを抽出したので再利用性も損なわない。もっと踏み込めば、CSVファイル読み込み時に毎回isCsvFileメソッドでチェックするのはスマートでないので、私ならばインスタンス化されるときに例外を送出するような設計をすると思う。
まとめ
ちょっと他にもいろいろ例を挙げて説明したかったのですが、これまで携わったプロジェクトのコードを見るたびため息が出て、余りにもバカバカしくなり何も手がつかないような状態に陥ったので諦めました。
大規模なシステムを作るときは、まずUMLを書いてみるべきでしょう。普通にUMLを書き、普通にオブジェクト指向設計していれば、「どのような処理にも適用できるが、クラスとして実装することが困難な処理」なんてのはほとんど出てこない筈です。クソコードを眺めているとその根拠を説明するところまで書く気力が出てこなかったので諦めます。
私の携わったプロジェクトのソースを今一度眺めたのですが、スーパークラスもインタフェースも抽象クラスも継承もありませんでした。各画面ごとに1クラスずつが対応していて、そのほかにいくつかのユーティリティクラスがある程度です。そういえば、こないだ「俺がこの仕組みを構築したからXXXX時間を節約できた!!!」って先輩が鼻息荒く自慢してたからコードを見たら、ただ単に共通処理をスーパークラスで抽出しただけだったなあ~。それIDEのリファクタリング機能でサポートできるレベルなんだけどなあ~。普通にオブジェクト指向設計してたらそうなるんじゃないのかなあ~。あはは。もう無理。転職してえ。