はじめに
SOLID原則とは、オブジェクト指向設計において、保守性や拡張性を高めるための5つの基本原則になります。
- 単一責任の原則(Single Responsibility Principle: SRP)
- 開放/閉鎖の原則(Open/Closed Principle: OCP)
- リスコフの置換原則(Liskov Substitution Principle: LSP)
- インターフェース分離の原則(Interface Segregation Principle: ISP)
- 依存関係逆転の原則(Dependency Inversion Principle: DIP)
1. 単一責任の原則(SRP)
~クラスは1つの責務(役割)のみを持つべき~
悪い例
public class Player
{
public void Move() {
Console.WriteLine("プレイヤーが移動しました。");
}
public void Attack() {
Console.WriteLine("プレイヤーが攻撃しました。");
}
public void SavePlayerData() {
Console.WriteLine("プレイヤーデータを保存しました。");
}
}
このクラスは移動・攻撃・データの保存という異なる責務を持っており、責務が混在しています。
良い例
public class Player
{
public void Move() {
Console.WriteLine("プレイヤーが移動しました。");
}
public void Attack() {
Console.WriteLine("プレイヤーが攻撃しました。");
}
}
public class PlayerDataSaver
{
public void Save(Player player) {
Console.WriteLine("プレイヤーデータを保存しました。");
}
}
責務を分けることで、変更に強い設計になります。
2. 開放/閉鎖の原則(OCP)
~クラスは拡張に対して開いており、修正に対して閉じているべき~
悪い例
public class Enemy
{
public void Attack(string type)
{
if (type == "Goblin")
Console.WriteLine("ゴブリンが噛みついた!");
else if (type == "Dragon")
Console.WriteLine("ドラゴンが火を吹いた!");
}
}
敵の種類が増えるたびに、既存コード(Attack
メソッド)を修正する必要があます。
良い例
public interface IEnemy
{
void Attack();
}
public class Goblin : IEnemy
{
public void Attack() => Console.WriteLine("ゴブリンが噛みついた!");
}
public class Dragon : IEnemy
{
public void Attack() => Console.WriteLine("ドラゴンが火を吹いた!");
}
public class EnemyAttacker
{
public void AttackEnemy(IEnemy enemy)
{
enemy.Attack();
}
}
新しい敵を追加する際に、既存のコードを変更せずに拡張できます。
3. リスコフの置換原則(LSP)
~派生クラスは基底クラスの代替として機能すべき~
悪い例
public class Character
{
public virtual void Attack() {
Console.WriteLine("キャラクターが攻撃!");
}
}
public class Healer : Character
{
public override void Attack() {
throw new NotImplementedException("ヒーラーは攻撃できません。");
}
}
Character
型として扱うと、Healer
クラスで例外が発生します。派生クラスが基底クラスの動作を破壊しています。
良い例
public interface ICharacter
{
void PerformAction();
}
public class Warrior : ICharacter
{
public void PerformAction() => Console.WriteLine("戦士が攻撃!");
}
public class Healer : ICharacter
{
public void PerformAction() => Console.WriteLine("ヒーラーが回復!");
}
キャラクターの行動を PerformAction
に統一し、期待通りの振る舞いを保証します。
4. インターフェース分離の原則(ISP)
~クライアントは使用しないメソッドに依存してはならない~
悪い例
public interface ICharacter
{
void Attack();
void Heal();
}
public class Warrior : ICharacter
{
public void Attack() => Console.WriteLine("戦士が攻撃!");
public void Heal() => throw new NotImplementedException();
}
戦士は回復できないにも関わらず、無関係なメソッド(Heal
メソッド)の実装を強制されてしまっています。
良い例
public interface IAttacker
{
void Attack();
}
public interface IHealer
{
void Heal();
}
public class Warrior : IAttacker
{
public void Attack() => Console.WriteLine("戦士が攻撃!");
}
public class Priest : IHealer
{
public void Heal() => Console.WriteLine("僧侶が回復!");
}
不必要なメソッドの強制を避け、適切に分離できます。
5. 依存関係逆転の原則(DIP)
~高レベルモジュールは低レベルモジュールに依存してはならない~
悪い例
public class Sword
{
public void Use() => Console.WriteLine("剣を振った!");
}
public class Player
{
private Sword _weapon = new Sword();
public void Attack() => _weapon.Use();
}
Player
クラスは 具体的な実装(Sword
)に直接依存しており、拡張性が低くなっています。
良い例
public interface IWeapon
{
void Use();
}
public class Sword : IWeapon
{
public void Use() => Console.WriteLine("剣を振った!");
}
public class Player
{
private readonly IWeapon _weapon;
public Player(IWeapon weapon) => _weapon = weapon;
public void Attack() => _weapon.Use();
}
IWeapon
インターフェースを導入することで、拡張性が向上します。