オブジェクト指向設計の基本

保守性の高いクラスを設計するための、3つの基本的な考え方を学びます。

保守性を高める3つの柱

優れたクラス設計は、以下の3つの概念を理解し、バランスを取ることから始まります。

  • カプセル化 (Encapsulation)
  • 凝集度 (Cohesion)
  • 結合度 (Coupling)

① カプセル化 とは?

オブジェクトの内部データや実装の詳細を、利用者から隠蔽することです。

利用者は、公開された安全なインターフェース(publicなメンバ関数)だけを通してオブジェクトを操作します。これにより、クラスの内部をブラックボックス化でき、利用者は複雑な実装を意識する必要がなくなります。

カプセル化の図解

カプセル化の本質とアンチパターン

「メンバ変数をprivateにする」ことだけがカプセル化ではありません。

メンバ変数に直接アクセスするための `getter` や `setter` を安易に用意してしまうと、カプセル化は崩壊します。 なぜなら、クラスの内部構造を外部に漏らしてしまい、クラスの外でロジックが組まれる原因となるからです。

原則:「尋ねるな、命じよ (Tell, Don't Ask)」

良いカプセル化を実践するための重要な指針です。オブジェクトからデータを取得して(尋ねて)外部で処理するのではなく、処理そのものをオブジェクトに「命令」します。

悪い例:`getter`/`setter`で「尋ねる」

プレイヤーの移動処理を、クラスの外側で行っている例です。`Player`クラスの内部データ(位置や速度)を一つ一つ「尋ねて」計算しています。

// プレイヤーの移動処理 (悪い例)
Vector2 position = player.getPosition(); // 尋ねる
position += player.getVelocity(); // 尋ねる
player.setPosition(position);  // 設定する

これでは、移動ロジックが変更されるたびに、このコードも修正する必要があり、`Player`クラスがカプセル化されている意味がありません。

良い例:「命じる」

移動するという「振る舞い」そのものを`Player`クラスに担当させます。利用者は「動け」と命じるだけです。

// プレイヤーの移動処理 (良い例)
player.move(); // 「動け」と命じる

移動の具体的な計算方法は`Player`クラスの中に隠蔽されています。将来、移動方法(例:ジャンプ、ダッシュ)が変更されても、修正は`Player`クラスの内部だけで完結し、この呼び出しコードを変更する必要はありません。

確認テスト (カプセル化)

「尋ねるな、命じよ」の原則に最も合致するコードはどれですか?(HPを10回復する処理)

② 凝集度 とは?

クラスがどれだけ1つの役割に集中しているかを示す尺度です。

  • 凝集度が高い (High Cohesion): クラスが1つの明確な責務(役割)だけを持っている状態。(良い設計)
  • 凝集度が低い (Low Cohesion): 1つのクラスが、関係のない多くの責務を抱え込んでいる状態。(悪い設計、いわゆる「神クラス」)

凝集度が高いクラスは、目的が明確で理解しやすく、変更時の影響範囲も限定されます。

凝集度が低い例

この`Game`クラスは、「得点の計算」「制限時間の制御」「キャラクター管理」という3つの異なる役割をすべて担当してしまっています。

class Game {
private:
    int   score_;  // 得点
    float timer_;  // 制限時間
    std::list actors_; // キャラクターリスト
};

このように多くの役割を持つクラスは、複雑で修正が困難になりがちです。

凝集度を高めるには?

それぞれの役割を独立したクラスに委譲(分離)します。

// 得点計算のみ担当するクラス
class Score { /* ... */ };

// 制限時間の制御のみ担当するクラス
class Timer { /* ... */ };

// キャラクターの管理のみ担当するクラス
class ActorManager { /* ... */ };

// Gameクラスは「各クラスの制御役」という1つの役割に集中
class Game {
private:
    Score score_;
    Timer timer_;
    ActorManager actors_;
};

これにより、各クラスの役割が明確になり、それぞれが凝集度の高い状態になります。

確認テスト (凝集度)

以下のうち、最も凝集度が低いと考えられるクラスはどれですか?

③ 結合度 とは?

あるクラスが、ほかのクラスとどれだけ密接に関連しているかを示す尺度です。

  • 疎結合 (Loose Coupling): クラス間の依存関係が最小限である状態。独立性が高い。(良い設計)
  • 密結合 (Tight Coupling): クラスが他の多くのクラスに依存しており、独立性が低い状態。(悪い設計)

クラスは、なるべくお互いに影響しあわないほうが良い関係と言えます。

結合度の図解

疎結合のメリット

疎結合なクラスほど良いとされています。なぜなら、以下の利点があるからです。

  • 変更に強い: あるクラスを修正しても、他のクラスへの影響が少ない。
  • 再利用しやすい: 他のクラスへの依存が少ないため、別のプログラムでも使いやすい。
  • テストが容易: クラス単体でのテスト(単体テスト)が簡単にできる。

設計のヒント

「このクラスの単体テストは簡単にできるだろうか?」と考えてみることは、結合度の良し悪しを判断する良い方法です。もしテストのために多くの他のクラスの準備が必要なら、それは密結合のサインかもしれません。

確認テスト (結合度)

クラスAがクラスBの非公開メンバ(private変数など)を直接参照している場合、この2つのクラスの関係を何と呼びますか?

練習問題

以下の凝集度が低い`UserProfile`クラスがあります。これを、凝集度が高い複数のクラスに分割するリファクタリングを考えてみましょう。(解答例を参考に、どのようなクラスに分割できるかイメージしてください)

Before (凝集度が低いクラス)

class UserProfile {
private:
    // 基本情報
    std::string userId_;
    std::string userName_;
    
    // 住所情報
    std::string postalCode_;
    std::string address_;

    // ログイン情報
    std::string passwordHash_;
    Date lastLoginTime_;
};

After (解答例)

class User {
private:
    UserId id_;
    UserName name_;
    LoginInfo loginInfo_;
    Address address_;
};

// それぞれの役割に特化したクラス
class Address { /* ... */ };
class LoginInfo { /* ... */ };

演習問題

オンラインショップの「商品(Product)」クラスを設計してください。以下の要件を満たし、カプセル化と高い凝集度を意識してください。

  • 商品には「商品ID」「商品名」「価格」の情報が必要です。
  • 価格は税込み価格で管理し、外部からは税抜き価格を取得できるようにします(消費税率は10%固定とします)。
  • 価格を直接変更する`setter`は用意せず、価格改定用のメソッド`revisePrice(newPrice)`を実装します。このメソッドでは、0円未満の価格が設定されないように保護してください。

解答を表示するには、パスワード「oop-design」を入力してください。

解答コード例 (C++):

class Product {
public:
    Product(const std::string& id, const std::string& name, int priceWithTax)
        : id_(id), name_(name), priceWithTax_(0) {
        // コンストラクタでも不正な価格を防ぐ
        if (priceWithTax >= 0) {
            priceWithTax_ = priceWithTax;
        }
    }

    // 税抜き価格を取得する (尋ねるのではなく、計算結果を提供する)
    int getPriceWithoutTax() const {
        return static_cast<int>(priceWithTax_ / 1.1);
    }
    
    // 価格改定を命じる
    void revisePrice(int newPriceWithTax) {
        // 不正な値から内部状態を保護する
        if (newPriceWithTax >= 0) {
            priceWithTax_ = newPriceWithTax;
        }
    }

    // getterは必要に応じて用意するが、setterは安易に作らない
    std::string getId()   const { return id_; }
    std::string getName() const { return name_; }

private:
    std::string id_;
    std::string name_;
    int priceWithTax_; // 内部データは税込みで持つ
};

まとめ:良いクラス設計への道

保守性・再利用性の高いクラスを設計するためには、3つの原則のバランスが重要です。

設計の基本原則

  • カプセル化:内部を隠し、「尋ねるな、命じよ」。安易な`getter`/`setter`を避ける。
  • 高凝集:一つのクラスには、一つの役割だけを持たせる。クラスの責務を明確にする。
  • 疎結合:クラス間の依存関係は、最小限に保つ。単体テストのしやすさを意識する。

常に考えよう

「このクラスの役割は何か?」
「この処理は、本当にこのクラスが知るべき情報か?」
と自問自答することが、より良い設計への第一歩です。