Java | オブジェクト指向:クラス肥大化の兆候

Java Java
スポンサーリンク

クラス肥大化って何がまずいのか

クラス肥大化は
「とりあえずあれこれ押し込んでいるうちに、そのクラスだけ異様に太ってしまった状態」
です。

1つのクラスが、画面の制御も、ビジネスロジックも、DB のやりとりも、ログも、例外処理も、なんでも抱え始める。
最初は「全部ここにあって便利」に見えるんですが、あとから変更しようとすると一気に地獄になります。

変更の影響範囲が読めない。
どこを読めば何が分かるのか分からない。
テストを書こうとしても何をどう分ければいいか分からない。

だから「クラスが太り始めた兆候」を早めに感じ取って、
小さいうちに分割してあげるのが大事です。


兆候1:クラスファイルを開いたらスクロールバーが豆粒(物理的にデカい)

行数とメソッド数がやたら多い

一番分かりやすい兆候は、「単純に行数とメソッド数が多すぎる」です。

スクロールしてもスクロールしても終わらない。
クラス冒頭で宣言したフィールドが、どこでどう使われているか一発で追えない。
クラス名の意味から想像できないメソッドが大量にぶら下がっている。

例えば、こんな Service を想像してください。

public class OrderService {

    // フィールドもいっぱい

    public void placeOrder(...) { ... }
    public void cancelOrder(...) { ... }
    public void updateOrderAddress(...) { ... }
    public void sendMailToCustomer(...) { ... }
    public void exportOrderCsv(...) { ... }
    public void reCalcAllOrderAmounts(...) { ... }
    public void batchCloseOrders(...) { ... }
    // まだまだ続く…
}
Java

OrderService という名前なのに、
個別注文処理、バッチ処理、メール送信、CSV 出力など、何でもかんでも詰め込まれています。

行数・メソッド数が増えすぎてきたときは、
「このクラスは本当に1つの役割に集中できているか?」を疑うタイミングです。


兆候2:クラス名と中身が一致していない(責務が多すぎる)

「〜Service」「〜Manager」に何でも詰め込んでいないか

クラス名から想像できる責務と、実際のメソッド群がズレてくると危険です。

たとえば UserService が次のような責務を持ち始めたら要注意です。

  • ユーザー登録
  • ユーザー情報更新
  • パスワード変更
  • パスワードのバリデーション
  • ポイント計算
  • メール送信
  • CSV エクスポート
  • バッチ処理

こうなっているとき、そのクラスは実質
「User にちょっとでも関係ありそうなもの全部」
みたいなゴミ箱になっています。

責務が多すぎると、

1つの仕様変更で、関係ないはずのメソッドまで巻き込まれやすい
読む人から見て「このクラスは結局何をしてほしいのか」が分からない

という状態になります。

クラス名を見たときに、「この名前にしては面倒見すぎだな」と感じたら、
すでに肥大化の入り口に立っています。


兆候3:if / switch だらけで、「ルール」が一箇所に詰め込まれている

種別や状態ごとの分岐が山ほどある

クラス肥大化のよくあるパターンが、
「種別ごとの業務ルールを全部1クラスの if / switch に押し込めている」状態です。

例えば注文状態に応じて処理を切り替えるコードがあるとします。

public void handleOrder(Order order) {
    if (order.getStatus() == Status.NEW) {
        // 新規の時
    } else if (order.getStatus() == Status.PAID) {
        // 入金済みの時
    } else if (order.getStatus() == Status.SHIPPED) {
        // 出荷済みの時
    } else if (order.getStatus() == Status.CANCELED) {
        // キャンセルの時
    }
    // さらに似たような if が下に続く…
}
Java

こういう「状態ごとの振る舞い」が、クラスのあちこちに散らばっていると、
そのクラスは状態マシンみたいな役割まで抱え込んでしまいます。

本来は

状態ごとの違い → 状態オブジェクトや enum のメソッド
種別ごとの違い → ポリモーフィズム(サブクラス/戦略)

に切り出したほうが自然なのに、
全部1クラスの if で制御していると、そのクラスだけが異様に太っていきます。

if / switch が増え始めたら、
「これは本当は別のクラス(あるいは enum)が持つべき責務じゃないか?」
と疑ってみてほしいです。


兆候4:外部とのやりとりを何でも直にやっている

DB も HTTP もファイルもログも、全部このクラス

クラスの中で、

  • リポジトリを呼んで DB から読み書きし
  • 外部 API を直接叩き
  • ファイルに書き込み
  • ログを出し
  • 画面用の DTO を組み立て

といった「外の世界との接続」を全部やっていると、
そのクラスは一気に太ります。

例えば、こんな雰囲気です。

public class ReportService {

    public void generateMonthlyReport(...) {
        // DB からデータ取得
        // ビジネスロジックで集計
        // CSV 形式に整形
        // ファイルに書き出し
        // メール送信
    }
}
Java

本来は、

  • データ取得はリポジトリ
  • 集計はドメインサービス
  • フォーマットはフォーマッタ
  • 書き出しはアウトプット用の別コンポーネント

というように責務を分けられます。

外部 I/O をなんでも直に扱っているクラスは、
「境界の責務」まで全部持ってしまっていて、その分太っていることが多いです。


兆候5:テストしようとすると「全部一緒に」テストせざるをえない

ユニットテストを書こうとして詰む

クラス肥大化の実害が一番よく分かるのがテストです。

例えば OrderService

  • 注文の検証
  • 割引ロジック
  • 在庫チェック
  • DB 保存
  • メール送信

など全部やっていると、テストを書こうとしたときに

在庫だけ偽物にしたいのに、DB もメールも一緒に動いてしまう
割引のロジックだけ確かめたいのに、全部の依存をセットしないとコンパイルが通らない

という状況になります。

「このメソッドだけを、単体でテストする」ということが難しければ難しいほど、
そのクラスはたくさんの責務を抱えすぎている可能性が高いです。

クラスを見て、

このクラスのまともなユニットテストを書こうとしたら、mock が 5個も6個も必要になる
テストコード側で大量のセットアップをしないと動かない

と感じるなら、それは「肥大化しているからテストしづらい」のかもしれません。


兆候6:フィールドが多く、コンストラクタ引数もやたら長い(重要なサイン)

依存オブジェクトの数が、そのまま責務の数になっている

クラスのフィールドに注目してみてください。

public class HugeService {

    private final UserRepository userRepository;
    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;
    private final MailSender mailSender;
    private final PaymentGateway paymentGateway;
    private final Logger logger;
    private final CsvExporter csvExporter;
    private final ReportGenerator reportGenerator;
    // まだまだある…

    public HugeService(UserRepository userRepository,
                       OrderRepository orderRepository,
                       ProductRepository productRepository,
                       MailSender mailSender,
                       PaymentGateway paymentGateway,
                       Logger logger,
                       CsvExporter csvExporter,
                       ReportGenerator reportGenerator /* ... */) {
        // 代入
    }
}
Java

こういうクラスは、「依存している相手の数」だけ責務が増えているケースが多いです。

  • ユーザーも扱う
  • 注文も扱う
  • 商品も扱う
  • メールも送る
  • 決済もする
  • ログも管理する
  • CSV も出力する
  • レポートも作る

これを全部1クラスで面倒見た結果、
フィールドとコンストラクタ引数が爆増している状態です。

依存が 2〜3 個なら普通ですが、
5 個を超えたあたりから「本当に1クラスの責務か?」を疑ったほうがいいです。
コンストラクタが長くなるのは、「分割のチャンスを教えてくれているサイン」とも言えます。


まとめ:クラス肥大化を嗅ぎ分ける感覚を持つ

クラス肥大化は、いきなり「巨大クラス」が現れるわけではありません。
少しずつ便利そうなものを足し続けた結果、ある日ふと見たらモンスターになっている、という感じです。

だからこそ、次のような兆候を日常的に意識しておくと、防ぎやすくなります。

  • 開いたときにスクロールバーが豆粒(行数・メソッド数が多すぎないか)
  • クラス名の割に、やっていることの種類が多すぎないか
  • if / switch だらけで、種類ごとのルールを抱え込みすぎていないか
  • 外部 I/O(DB、HTTP、ファイル、メール)まで全部直に扱っていないか
  • まともなユニットテストを書こうとすると、依存だらけで苦しくないか
  • フィールドやコンストラクタ引数の数が異様に多くなっていないか

タイトルとURLをコピーしました