開放閉鎖の原則(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();
}
}
}
コードの特徴
- 拡張性の確保
- 武器の新しい種類(例:ハンマー)を追加する場合は、
IWeapon
を実装したクラスを作成するだけで良い。 - 既存の
Player
クラスや他のクラスを変更する必要がない。
- 武器の新しい種類(例:ハンマー)を追加する場合は、
- 既存コードの安定性
- 既存の武器(剣や弓)やプレイヤーのロジックに手を加えないため、バグが入り込むリスクが少ない。
- メンテナンスの容易さ
- 各武器のロジックは独立しており、それぞれのクラスが担当するため、問題が発生しても影響範囲が小さい。
実行結果
新しい武器を装備しました!
剣を振り下ろした!ダメージを与えた!
新しい武器を装備しました!
矢を放った!遠距離攻撃成功!
新しい武器を装備しました!
魔法を唱えた!敵に炎を放った!
開放閉鎖の原則の要点
- 拡張を簡単に:インターフェース(または抽象クラス)を使うことで、新しい機能を追加する際に影響範囲を限定する。
- 修正を最小限に:既存コードに依存しない設計により、リファクタリングやバグ修正時のリスクを軽減する。
まとめ
この設計により、新しい武器を簡単に追加できる柔軟性と、既存コードの安定性を両立できます。ゲーム開発をはじめ、さまざまなシステム設計で役立つ重要な原則です。
以下に「開放閉鎖の原則」を破ってしまうコード例と、その問題点を説明します。
開放閉鎖の原則を破る例
この例では、プレイヤーが使用する武器を管理する方法として、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();
}
}
}
このコードの問題点
- 新しい武器を追加するたびに
Player
クラスを変更する必要がある- 例えば、「ハンマー」という新しい武器を追加したい場合は、
Attack()
メソッドに以下のコードを追加する必要があります。
- 例えば、「ハンマー」という新しい武器を追加したい場合は、
else if (_currentWeapon == "Hammer")
{
Console.WriteLine("ハンマーで強打!大ダメージ!");
}
このように、コードが新しい武器に依存して拡張しづらくなります。
- クラスが武器の詳細ロジックを持ちすぎている
Player
クラスが「武器の動作」をすべて管理しているため、責務が肥大化しています。これは 単一責任の原則(Single Responsibility Principle) にも反します。
- 保守性が低い
- 武器の種類が増えるたびに
Attack()
メソッドが肥大化して、コードが読みにくくなります。また、他の部分を壊すリスクが高まります。
- 武器の種類が増えるたびに
- 柔軟性が欠ける
- 新しい武器を追加するたびにコード全体を変更しなければならず、再利用性やモジュール性が損なわれています。
改善例(開放閉鎖の原則を守る設計)
以下は、先ほどの「開放閉鎖の原則を守る例」と同じ内容ですが、この原則を適用したコード例です。
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 クラスがすべての武器の攻撃ロジックを持つ | 各武器クラスが自身の動作を管理 |
保守性 | 武器が増えるとコードが複雑になり、修正の影響範囲が広がる | 各武器の動作が独立しているため、修正の影響範囲が限定される |
柔軟性と拡張性 | 武器が増えるたびにコード全体を変更する必要があり、リスクが高い | 新しいクラスを追加するだけで既存コードに影響を与えない |
結論
「開放閉鎖の原則」を破るコードは短期的には簡単ですが、長期的な変更や機能拡張が難しくなり、コードが複雑化します。一方、この原則を守る設計では、拡張性と保守性が高まり、新しい機能を簡単に追加できるようになります。
ディスカッション
コメント一覧
まだ、コメントがありません