データと振る舞いを分離させない!凝集度の高いクラス設計のすすめ

この記事は、読みやすいコードを書くための実践ガイドシリーズの一つです。前回の記事「深すぎるネストはバグの温床!ガード節でシンプルにする条件分岐の書き方」では、条件分岐をシンプルにする方法を解説しました。

こんにちは。今回は、オブジェクト指向設計において非常に重要な「凝集度」という概念と、それを無視した「データクラス」がもたらす様々な問題について解説します。

目次

データクラスとは? なぜ問題なのか?

データクラスとは、フィールド(データ)とそのゲッター/セッターを持つだけで、ビジネスロジック(振る舞い)をほとんど持たないクラスのことです。一見すると、単純で分かりやすいように思えるかもしれません。

【Before】改善前の設計

データしか持たないクラス

// 注文金額を保持するだけのクラス
public class SalesOrder {
    public int amountIncludingTax; // 税込金額
    public BigDecimal salesTaxRate;  // 消費税率
}

ロジックを処理する別のクラス

public class OrderManager {
    public SalesOrder salesOrder;

    // 税込金額を計算するロジック
    public int calculateAmountIncludingTax(int amountExcludingTax, BigDecimal salesTaxRate) {
        BigDecimal multiplier = salesTaxRate.add(new BigDecimal("1.0"));
        BigDecimal amount = multiplier.multiply(new BigDecimal(amountExcludingTax));
        return amount.intValue();
    }

    // 注文を確定する処理
    public void confirmOrder(int amountExcludingTax, BigDecimal salesTaxRate) {
        int amountIncludingTax = calculateAmountIncludingTax(amountExcludingTax, salesTaxRate);

        this.salesOrder = new SalesOrder();
        this.salesOrder.amountIncludingTax = amountIncludingTax;
        this.salesOrder.salesTaxRate = salesTaxRate;
    }
}

この設計では、「注文金額」に関するデータ(SalesOrder)と、それに関するロジック(OrderManager)が完全に分離してしまっています。このように、関連性の高いデータと振る舞いが別々の場所に置かれている状態を「凝集度が低い」と言います。

そして、この「低凝集」な設計が、様々な悲劇を引き起こすのです。

データクラスが招く6つの悲劇

1. 仕様変更時に牙を剥く(低凝集)

「税率の計算方法を変更したい」という場合、SalesOrderクラスではなく、OrderManagerクラスを修正しなければなりません。データとロジックの置き場所が離れているため、関連する修正箇所を探すのが困難になります。

2. 重複コードの温床

もし別のBillingManagerクラスでも同様の税込金額計算が必要になったらどうなるでしょうか? OrderManagerの計算ロジックをコピー&ペーストしてしまい、コードの重複が発生しがちです。

3. 修正漏れのリスク

重複したコードがある場合、仕様変更時に片方を修正し、もう片方を修正し忘れるという致命的なバグが容易に発生します。

4. 可読性の低下

SalesOrderのデータがどのように使われているのかを理解するために、OrderManagerやその他のXXXManagerクラスをすべて読み解かなければならず、プログラム全体の把握が困難になります。

5. 未初期化状態(生焼けオブジェクト)の発生

SalesOrderは、ただnewするだけでインスタンスを作成できてしまいます。しかし、フィールドが初期化されていない「生焼け」の状態でも存在できてしまうため、思わぬところでNullPointerExceptionが発生する可能性があります。

// フィールドがnullのままの「生焼けオブジェクト」
SalesOrder order = new SalesOrder();
// ここで NullPointerException が発生!
System.out.println(order.salesTaxRate.toString());

6. 不正値の混入

フィールドがpublicになっているため、外部からどんな値でも自由に設定できてしまいます。これにより、ありえないデータが紛れ込む可能性があります。

SalesOrder order = new SalesOrder();
// マイナスの税率という不正な値を設定できてしまう
order.salesTaxRate = new BigDecimal("-0.1");

改善策:データと振る舞いをカプセル化し、凝集度を高める

これらの問題を解決するには、関連するデータと振る舞いを一つのクラスにまとめ(カプセル化)、凝集度を高めることが重要です。

【After】改善後の設計

public class SalesOrder {
    // データは外部から直接変更できないようにprivateにする
    private final int amountIncludingTax;
    private final BigDecimal salesTaxRate;

    // コンストラクタで初期化を強制し、不正な値は受け付けない
    public SalesOrder(int amountExcludingTax, BigDecimal salesTaxRate) {
        // バリデーション:税率は0以上でなければならない
        if (salesTaxRate.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("税率はマイナスにできません。");
        }
        // バリデーション:税抜金額は0以上でなければならない
        if (amountExcludingTax < 0) {
            throw new IllegalArgumentException("金額はマイナスにできません。");
        }

        this.salesTaxRate = salesTaxRate;
        // 自身のデータを使ってロジックを実行する
        this.amountIncludingTax = this.calculateAmountIncludingTax(amountExcludingTax);
    }

    // 金額計算ロジック(振る舞い)をクラス内に持つ
    private int calculateAmountIncludingTax(int amountExcludingTax) {
        BigDecimal multiplier = this.salesTaxRate.add(new BigDecimal("1.0"));
        BigDecimal amount = multiplier.multiply(new BigDecimal(amountExcludingTax));
        return amount.intValue();
    }

    // 外部からは値を取得できるだけ(必要に応じてゲッターを用意)
    public int getAmountIncludingTax() {
        return amountIncludingTax;
    }

    public BigDecimal getSalesTaxRate() {
        return salesTaxRate;
    }
}

この新しいSalesOrderクラスは、以下の点で改善されています。

  • 高凝集: 「注文金額」に関するデータと計算ロジックが同じクラスにまとまっています。
  • 不変性: コンストラクタで一度値が設定されたら、後から変更できません(final修飾子)。これにより、インスタンスの状態が安全に保たれます。
  • 完全なオブジェクト: コンストラクタで全てのデータが初期化されるため、「生焼け」の状態が存在しません。
  • 自己防衛: コンストラクタでバリデーションを行うため、不正な値を持つインスタンスは作成されません。

このように設計することで、「注文金額を扱うときは、このSalesOrderクラスを使えば安全で正しい」という信頼が生まれます。これが、堅牢でメンテナンスしやすいソフトウェアの基礎となるのです。

まとめ

オブジェクト指向の原則である「データと振る舞いのカプセル化」は、凝集度を高め、ソフトウェアをよりシンプルで堅牢にするための強力な武器です。

安易にゲッター/セッターを持つだけのデータクラスを作るのではなく、「このデータに関する責任(振る舞い)は何か?」を常に考え、データ自身に仕事をさせる設計を心がけましょう。

▼「読みやすいコードを書くための実践ガイド」シリーズ このシリーズでは、読みやすいコードを書くための基本的なテクニックをご紹介しました。他の記事もぜひご覧ください。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次