🤦

オブジェクト指向の例がわかりにくい。三大要素とは。

2025/02/26に公開

オブジェクト指向がわかりづらい?

オブジェクト指向の説明って、よく「Car クラスを作って~」「Animal クラスを継承して~」といった例が多くていまいちピンとこないですよね。

そこで、もっと身近なWebシステムの例を使えばイメージしやすくなるのでは?と思い、今回はポイントカードシステムを題材にして、オブジェクト指向の三大要素についてまとめてみたいと思います。

そもそもオブジェクト指向とは?

まず、オブジェクト指向とは何かをざっくり説明すると、

「プログラムの各要素を独立したオブジェクトとして設計し、それらを連携させて動作させる設計手法」

です。

オブジェクト指向には、特に重要な3つの概念があります。

  1. 継承
  2. カプセル化
  3. ポリモーフィズム(多態性)

これらを意識することで、コードがよりシンプルで再利用性が高く、拡張しやすくなります。

ポイントカードシステムで学ぶオブジェクト指向の三大要素

今回の例として、よくお店やECサイトで利用されるポイントカードシステムを使います。

  • 購入金額に応じてポイントが貯まる。
  • 貯めたポイントは次回以降の買い物に使える。
  • カードにはスタンダードカードとゴールドカードの2種類があり、ゴールドカードはより高い還元率や特典がついている。

この流れを想定して、以下のサンプルコードで説明していきたいと思います。

1. 継承

継承とは、親クラスのプロパティやメソッドを子クラスに引き継ぐ仕組みです。

まずは、基本となるPointCardクラスです。

abstract class PointCard
{
    protected $point;

    // コンストラクタで初期ポイントを受け取れるようにする
    public function __construct($initialPoint = 0)
    {
        $this->point = $initialPoint;
    }

    // 購入金額からポイントを加算する
    public function purchase($amount)
    {
        $this->addPoint($this->calculatePoint($amount));
    }

    // ポイントを加算
    protected function addPoint($point)
    {
        $this->point += $point;
    }

    // ポイントを使用
    public function usePoint($point)
    {
        // バリデーション:ポイントが足りているかチェック
        if ($point > $this->point) {
            throw new Exception('ポイントが足りません。');
        }

        $this->point -= $point;
    }

    // 現在のポイントを取得
    public function getPoint()
    {
        return $this->point;
    }

    // 抽象メソッド:必ず子クラスで定義する
    abstract protected function calculatePoint($amount);
}
  • __construct() で、インスタンス生成時に初期ポイントを渡す仕組みにしました。これにより、「DBから取得したポイントをセットして生成する」ことも可能になります。
  • purchase() メソッドで購入金額からポイントを計算し、内部の addPoint() で加算しています。
  • usePoint() メソッドでは、バリデーションとして「使用するポイントが現在のポイントより多い場合はエラーにする」仕組みを追加しました。
  • calculatePoint() は子クラスごとに計算方式が異なるので、抽象メソッドとして定義しています。

次に、このPointCardクラスをベースに、スタンダードカードとゴールドカードを継承して作成します。

スタンダードカードのクラス

class StandardCard extends PointCard
{
    protected function calculatePoint($amount)
    {
        return floor($amount * 0.01); // スタンダードは1%還元
    }
}

ゴールドカードのクラス

class GoldCard extends PointCard
{
    protected $specialService = '特典がついています';

    public function getSpecialService()
    {
        return $this->specialService;
    }

    protected function calculatePoint($amount)
    {
        return floor($amount * 0.02); // ゴールドは2%還元
    }
}
  • スタンダードカードは「1%還元」、ゴールドカードは「2%還元」と、子クラスごとにロジックを変えています。

2. カプセル化

カプセル化は、データとその操作を一つにまとめ、外部から直接アクセスできないようにする仕組みです。
この例では、$point プロパティを protected にして直接変更できないようにし、ポイントを加算・使用するときは必ずメソッドを介するようにしています。バリデーションロジック(if ($point > $this->point) { ... })もこのメソッド1か所に書けばOKで、外部から不正にポイントを変更されることを防ぎます。

実際の処理は以下のように呼び出します。

// 例:DBなどからユーザーの現在ポイントが100ptとわかった場合
$initialPoint = 100;

$standard = new StandardCard($initialPoint);
$gold = new GoldCard($initialPoint);

$standard->purchase(1000); // 1000円の購入で +10ポイント
$gold->purchase(1000);     // 1000円の購入で +20ポイント

echo $standard->getPoint(); // 100 + 10 = 110
echo $gold->getPoint();     // 100 + 20 = 120

// ポイントを使う
try {
    $standard->usePoint(5); // 5ポイント使用
    echo $standard->getPoint(); // 110 - 5 = 105
} catch (Exception $e) {
    echo $e->getMessage();
}

外部からは $point を直接操作できず、必ず purchase()usePoint() を経由します。
これにより、ポイント計算のルールやバリデーションが常に正しく処理されるようになります。

3. ポリモーフィズム(多態性)

ポリモーフィズムとは、同じメソッドを呼び出しても、オブジェクトの種類によって異なる動作をする仕組みです。

function displayPoints(PointCard $card, $amount) {
    $card->purchase($amount);
    echo '現在のポイント: ' . $card->getPoint() . "\n";
}

// ゴールドかスタンダードかわからないが、共通のインターフェイスで動作させられる
displayPoints(new StandardCard(), 1000); // 10ポイント
displayPoints(new GoldCard(), 1000);     // 20ポイント

同じ purchase() を呼んでいるのに、渡されたカードの種類(スタンダードかゴールドか)によって付与されるポイントが変わるのがポイントです。
これが
多態性
で、オブジェクト指向の大きな特徴となっています。

実際の使いどころ:ECサイトでのポイント管理

ここまでポイントカードを例にしてきましたが、実際の開発現場でも非常によくあるシチュエーションです。
例えば、ECサイトを想定すると、

  1. ユーザーがログインする
  2. データベースなどからユーザーの現在のポイントを取得する
  3. ユーザーの会員ランク(スタンダード or ゴールド)を判定して、対応するカードクラスのインスタンスを作成
  4. 商品を購入したら、その金額に応じて purchase() メソッドでポイントを加算
  5. 使用したいポイントがあれば、 usePoint() メソッドで差し引いて、残ったポイントをデータベースに更新

といった感じでしょうか。
そして、今回の継承・カプセル化・ポリモーフィズムをうまく使うことで、

  • 追加の会員ランク(シルバーカード、プラチナカードなど)を増やすときに、還元率のメソッドを追加するだけで済む。
  • ポイント使用時のエラー処理(残高不足など)を一元管理できる。
  • それぞれのカード(クラス)で計算方式が変わっても、呼び出し側では同じメソッド(purchase(), usePoint())を使い続けられる。

といったメリットが出てきます。

オブジェクト指向のメリットとデメリット

では、なぜオブジェクト指向が必要なのか? という視点でメリット・デメリットをざっくりまとめておきます。

メリット

  1. 大規模化した際の保守が容易になる
    • メソッドやクラスが役割ごとに分かれており、変更に強い設計がしやすい。
  2. 再利用性が高まり、変更・拡張がしやすい
    • 新機能を追加したり仕様を変えるときも、共通部分は親クラスにまとめておけば重複コードが減る。
  3. チーム開発でも役割分担がしやすい
    • 各クラスやメソッドに責任範囲が明確なので、タスク分解や仕様書の作り方がわかりやすくなる。

デメリット

  1. 小規模・単機能なスクリプトにはややオーバーエンジニアリング
    • 例えば、一時的に使うだけの小さいスクリプトにはクラス設計が大げさに感じられる場合もあります。
  2. 設計を誤ると逆に複雑化しやすい
    • クラスの分け方や抽象度を誤ると、継承関係が不自然になり、保守が大変になることもあります。

まとめ

今回は、ポイントカードシステムを例にして、オブジェクト指向の三大要素である「継承」「カプセル化」「ポリモーフィズム」ついてまとめてみました。

  • 継承 → 共通の機能を親クラスにまとめ、子クラスで再利用&拡張。
  • カプセル化 → データと処理を隠蔽して、不正なアクセスやバグを防ぐ。
  • ポリモーフィズム → 同じメソッドでも、オブジェクトごとに異なる挙動を実現。

これらをうまく活用することで、規模が大きくなっても破綻しない拡張しやすい設計が行えます。

ただ、小規模なプロジェクトなど、開発の目的や規模によっては、あえてオブジェクト指向を導入しないほうがシンプルで効率的な場合もあるので、必要に応じてうまく活用するのが重要になるかと思います!

Discussion