保守性の高いクラスを設計するための、3つの基本的な考え方を学びます。
優れたクラス設計は、以下の3つの概念を理解し、バランスを取ることから始まります。
オブジェクトの内部データや実装の詳細を、利用者から隠蔽することです。
利用者は、公開された安全なインターフェース(publicなメンバ関数)だけを通してオブジェクトを操作します。これにより、クラスの内部をブラックボックス化でき、利用者は複雑な実装を意識する必要がなくなります。

「メンバ変数をprivateにする」ことだけがカプセル化ではありません。
メンバ変数に直接アクセスするための `getter` や `setter` を安易に用意してしまうと、カプセル化は崩壊します。 なぜなら、クラスの内部構造を外部に漏らしてしまい、クラスの外でロジックが組まれる原因となるからです。
良いカプセル化を実践するための重要な指針です。オブジェクトからデータを取得して(尋ねて)外部で処理するのではなく、処理そのものをオブジェクトに「命令」します。
プレイヤーの移動処理を、クラスの外側で行っている例です。`Player`クラスの内部データ(位置や速度)を一つ一つ「尋ねて」計算しています。
// プレイヤーの移動処理 (悪い例)
Vector2 position = player.getPosition(); // 尋ねる
position += player.getVelocity(); // 尋ねる
player.setPosition(position); // 設定する
これでは、移動ロジックが変更されるたびに、このコードも修正する必要があり、`Player`クラスがカプセル化されている意味がありません。
移動するという「振る舞い」そのものを`Player`クラスに担当させます。利用者は「動け」と命じるだけです。
// プレイヤーの移動処理 (良い例)
player.move(); // 「動け」と命じる
移動の具体的な計算方法は`Player`クラスの中に隠蔽されています。将来、移動方法(例:ジャンプ、ダッシュ)が変更されても、修正は`Player`クラスの内部だけで完結し、この呼び出しコードを変更する必要はありません。
「尋ねるな、命じよ」の原則に最も合致するコードはどれですか?(HPを10回復する処理)
クラスがどれだけ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_;
};
これにより、各クラスの役割が明確になり、それぞれが凝集度の高い状態になります。
以下のうち、最も凝集度が低いと考えられるクラスはどれですか?
あるクラスが、ほかのクラスとどれだけ密接に関連しているかを示す尺度です。
クラスは、なるべくお互いに影響しあわないほうが良い関係と言えます。

疎結合なクラスほど良いとされています。なぜなら、以下の利点があるからです。
「このクラスの単体テストは簡単にできるだろうか?」と考えてみることは、結合度の良し悪しを判断する良い方法です。もしテストのために多くの他のクラスの準備が必要なら、それは密結合のサインかもしれません。
クラスAがクラスBの非公開メンバ(private変数など)を直接参照している場合、この2つのクラスの関係を何と呼びますか?
以下の凝集度が低い`UserProfile`クラスがあります。これを、凝集度が高い複数のクラスに分割するリファクタリングを考えてみましょう。(解答例を参考に、どのようなクラスに分割できるかイメージしてください)
class UserProfile {
private:
// 基本情報
std::string userId_;
std::string userName_;
// 住所情報
std::string postalCode_;
std::string address_;
// ログイン情報
std::string passwordHash_;
Date lastLoginTime_;
};
class User {
private:
UserId id_;
UserName name_;
LoginInfo loginInfo_;
Address address_;
};
// それぞれの役割に特化したクラス
class Address { /* ... */ };
class LoginInfo { /* ... */ };
オンラインショップの「商品(Product)」クラスを設計してください。以下の要件を満たし、カプセル化と高い凝集度を意識してください。
解答を表示するには、パスワード「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つの原則のバランスが重要です。
「このクラスの役割は何か?」
「この処理は、本当にこのクラスが知るべき情報か?」
と自問自答することが、より良い設計への第一歩です。