開放閉鎖の原則(Open/Closed Principle)をわかりやすく説明する技術資料


開放閉鎖の原則とは

  • 拡張には開かれている(Open for extension)
    新しい機能を追加できる柔軟性を持つ。
  • 修正には閉じている(Closed for modification)
    既存のコードに影響を与えずに機能を拡張できる。

ゲームを題材としたサンプルコード

背景

プレイヤーが武器を装備し、攻撃を行うシンプルなゲームを作成します。

  • 武器は剣、弓、魔法の杖を持ち、それぞれ異なる攻撃方法を持っています。
  • 新しい武器(例えばハンマー)を追加しても、既存のコードは変更しません。

コード全体

using System;
using System.Collections.Generic;

namespace OpenClosedPrincipleGame
{
    // 1. 武器の共通インターフェース
    public interface IWeapon
    {
        void Attack();
    }

    // 2. 具体的な武器クラス
    public class Sword : IWeapon
    {
        public void Attack()
        {
            Console.WriteLine("剣を振り下ろした!ダメージを与えた!");
        }
    }

    public class Bow : IWeapon
    {
        public void Attack()
        {
            Console.WriteLine("矢を放った!遠距離攻撃成功!");
        }
    }

    public class MagicWand : IWeapon
    {
        public void Attack()
        {
            Console.WriteLine("魔法を唱えた!敵に炎を放った!");
        }
    }

    // 3. プレイヤークラス
    public class Player
    {
        private IWeapon _currentWeapon;

        public void EquipWeapon(IWeapon weapon)
        {
            _currentWeapon = weapon;
            Console.WriteLine("新しい武器を装備しました!");
        }

        public void Attack()
        {
            if (_currentWeapon != null)
            {
                _currentWeapon.Attack();
            }
            else
            {
                Console.WriteLine("武器を装備していません!");
            }
        }
    }

    // 4. 実行用クラス
    class Program
    {
        static void Main(string[] args)
        {
            // プレイヤーの作成
            Player player = new Player();

            // 武器を作成
            IWeapon sword = new Sword();
            IWeapon bow = new Bow();
            IWeapon magicWand = new MagicWand();

            // 武器を装備して攻撃
            player.EquipWeapon(sword);
            player.Attack();

            player.EquipWeapon(bow);
            player.Attack();

            player.EquipWeapon(magicWand);
            player.Attack();
        }
    }
}

コードの特徴

  1. 拡張性の確保
    • 武器の新しい種類(例:ハンマー)を追加する場合は、IWeapon を実装したクラスを作成するだけで良い。
    • 既存の Player クラスや他のクラスを変更する必要がない。
  2. 既存コードの安定性
    • 既存の武器(剣や弓)やプレイヤーのロジックに手を加えないため、バグが入り込むリスクが少ない。
  3. メンテナンスの容易さ
    • 各武器のロジックは独立しており、それぞれのクラスが担当するため、問題が発生しても影響範囲が小さい。

実行結果

新しい武器を装備しました!
剣を振り下ろした!ダメージを与えた!
新しい武器を装備しました!
矢を放った!遠距離攻撃成功!
新しい武器を装備しました!
魔法を唱えた!敵に炎を放った!

開放閉鎖の原則の要点

  • 拡張を簡単に:インターフェース(または抽象クラス)を使うことで、新しい機能を追加する際に影響範囲を限定する。
  • 修正を最小限に:既存コードに依存しない設計により、リファクタリングやバグ修正時のリスクを軽減する。

まとめ

この設計により、新しい武器を簡単に追加できる柔軟性と、既存コードの安定性を両立できます。ゲーム開発をはじめ、さまざまなシステム設計で役立つ重要な原則です。

以下に「開放閉鎖の原則」を破ってしまうコード例と、その問題点を説明します。


開放閉鎖の原則を破る例

この例では、プレイヤーが使用する武器を管理する方法として、Player クラスに直接武器のロジックを書いています。新しい武器を追加するたびに、このクラスを修正する必要があり、原則を破る典型例です。

コード例

using System;

namespace OpenClosedPrincipleBroken
{
    public class Player
    {
        private string _currentWeapon;

        public void EquipWeapon(string weapon)
        {
            _currentWeapon = weapon;
            Console.WriteLine($"{weapon} を装備しました!");
        }

        public void Attack()
        {
            if (_currentWeapon == "Sword")
            {
                Console.WriteLine("剣を振り下ろした!ダメージを与えた!");
            }
            else if (_currentWeapon == "Bow")
            {
                Console.WriteLine("矢を放った!遠距離攻撃成功!");
            }
            else if (_currentWeapon == "MagicWand")
            {
                Console.WriteLine("魔法を唱えた!敵に炎を放った!");
            }
            else
            {
                Console.WriteLine("武器を装備していません!");
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // プレイヤーを作成
            Player player = new Player();

            // 武器を装備して攻撃
            player.EquipWeapon("Sword");
            player.Attack();

            player.EquipWeapon("Bow");
            player.Attack();

            player.EquipWeapon("MagicWand");
            player.Attack();
        }
    }
}

このコードの問題点

  1. 新しい武器を追加するたびにPlayerクラスを変更する必要がある
    • 例えば、「ハンマー」という新しい武器を追加したい場合は、Attack() メソッドに以下のコードを追加する必要があります。
   else if (_currentWeapon == "Hammer")
   {
       Console.WriteLine("ハンマーで強打!大ダメージ!");
   }

このように、コードが新しい武器に依存して拡張しづらくなります。

  1. クラスが武器の詳細ロジックを持ちすぎている
    • Player クラスが「武器の動作」をすべて管理しているため、責務が肥大化しています。これは 単一責任の原則(Single Responsibility Principle) にも反します。
  2. 保守性が低い
    • 武器の種類が増えるたびに Attack() メソッドが肥大化して、コードが読みにくくなります。また、他の部分を壊すリスクが高まります。
  3. 柔軟性が欠ける
    • 新しい武器を追加するたびにコード全体を変更しなければならず、再利用性やモジュール性が損なわれています。

改善例(開放閉鎖の原則を守る設計)

以下は、先ほどの「開放閉鎖の原則を守る例」と同じ内容ですが、この原則を適用したコード例です。

using System;

namespace OpenClosedPrincipleFixed
{
    // 武器の共通インターフェース
    public interface IWeapon
    {
        void Attack();
    }

    // 各武器の実装
    public class Sword : IWeapon
    {
        public void Attack()
        {
            Console.WriteLine("剣を振り下ろした!ダメージを与えた!");
        }
    }

    public class Bow : IWeapon
    {
        public void Attack()
        {
            Console.WriteLine("矢を放った!遠距離攻撃成功!");
        }
    }

    public class MagicWand : IWeapon
    {
        public void Attack()
        {
            Console.WriteLine("魔法を唱えた!敵に炎を放った!");
        }
    }

    // プレイヤークラス
    public class Player
    {
        private IWeapon _currentWeapon;

        public void EquipWeapon(IWeapon weapon)
        {
            _currentWeapon = weapon;
            Console.WriteLine("新しい武器を装備しました!");
        }

        public void Attack()
        {
            if (_currentWeapon != null)
            {
                _currentWeapon.Attack();
            }
            else
            {
                Console.WriteLine("武器を装備していません!");
            }
        }
    }

    // 実行用クラス
    class Program
    {
        static void Main(string[] args)
        {
            Player player = new Player();

            // 武器を装備して攻撃
            player.EquipWeapon(new Sword());
            player.Attack();

            player.EquipWeapon(new Bow());
            player.Attack();

            player.EquipWeapon(new MagicWand());
            player.Attack();
        }
    }
}

比較まとめ

項目原則を破る例原則を守る例
新しい武器の追加Player クラスを変更しなければならない新しいクラスを作るだけで良い
責務の分離Player クラスがすべての武器の攻撃ロジックを持つ各武器クラスが自身の動作を管理
保守性武器が増えるとコードが複雑になり、修正の影響範囲が広がる各武器の動作が独立しているため、修正の影響範囲が限定される
柔軟性と拡張性武器が増えるたびにコード全体を変更する必要があり、リスクが高い新しいクラスを追加するだけで既存コードに影響を与えない

結論

「開放閉鎖の原則」を破るコードは短期的には簡単ですが、長期的な変更や機能拡張が難しくなり、コードが複雑化します。一方、この原則を守る設計では、拡張性と保守性が高まり、新しい機能を簡単に追加できるようになります。