目次
- SOLID原則を勉強する その1~単一責任の原則 (SRP)~
- SOLID原則を勉強する その2~オープン・クローズド原則(OCP)~
- SOLID原則を勉強する その3~リスコフの置換原則(LSP)~
- SOLID原則を勉強する その4~インターフェース分離の原則(ISP)~
- SOLID原則を勉強する その5~依存性逆転の原則(DIP)~ ←いまここ
前置き
書籍を読んだり、ググったりして、自分に分かりやすいようにまとめた記事です。
より詳しく知りたい方は、下記の参考文献を読んでみてください。
参考文献
Clean Architecture 達人に学ぶソフトウェアの構造と設計 | Amazon
Adaptive Code ~ C#実践開発手法 | Amazon
C#の設計の基本【SOLID原則】まとめ
Unity開発で使える設計の話+Zenjectの紹介
C# で SOLID の原則に違反する危険性
依存性逆転の原則(DIP)
-
上位モジュールが下位モジュールに依存してはいけない
- 上位モジュールも下位モジュールも抽象に依存すべき
- 上記モジュール = 相手を使う側のクラス
- 下位モジュール = 上位から使われるクラス
- 抽象は詳細に依存してはならない
- 詳細が抽象に依存すべき
- 上位モジュールも下位モジュールも抽象に依存すべき
- 依存性注入(Dependency injection)にも話が繋がる - 依存性注入 = オブジェクトを外から注入する - コンストラクタで外からインスタンスを受け取る - クラス内ではインターフェースしか操作しない - Unityだと Zenject という依存性注入を管理してくれるフレームワークがある - 依存性注入と依存性逆転の原則とは別の話なので注意
- 変化しやすい具象クラスを参照しない - 抽象クラス、インターフェースを参照するべき - Abstract Factory パターンを使う - 変化しやすい具象クラスを継承しない - 継承は一種の依存関係なので、気をつけて使うことが賢明 - 具象関数をオーバーライドしない - 具象関数はソースコードの依存関係を要求することが多い = 依存関係を排除できない
コード例
Unity開発で使える設計の話+Zenjectの紹介で紹介されているコードを参考にしていますが、クラス名などを変更しています。
before
SlashAttack.cs
using UnityEngine;
namespace Before.DIP.Attack
{
public class SlashAttack
{
public void ApplyDamage()
{
Debug.Log("斬撃攻撃でダメージを与える");
}
}
}
Player.cs
using Before.DIP.Attack;
namespace Before.DIP.Player
{
public class Player
{
public void Attack()
{
var attack = new SlashAttack();
attack.ApplyDamage();
}
}
}
ダメなところ
- 下位モジュールである
Attack
パッケージが変更されると、上位モジュールのPlayer
の変更が必要になる- シグネチャ(メソッド名、引数)が変更されたら、
Player
を変更しなければいけない -
Attack
の種類が増えたら、処理の分岐を追加しなければいけない
- シグネチャ(メソッド名、引数)が変更されたら、
After
- インターフェースを
Player
側に定義する- そのインターフェースを使うことで、矢印の向きを逆にする
-
Player
は下位モジュールのAttack
を知らない -
SlashAttack
はPlayer
側で定義したインターフェースを実装する
Player.cs
namespace After.DIP.Player
{
public class Player
{
IAttack _attack;
public Player(IAttack attack)
{
_attack = attack;
}
public void Attack()
{
_attack.ApplyDamage();
}
}
}
IAttack.cs
namespace After.DIP.Player
{
public interface IAttack
{
void ApplyDamage();
}
}
SlashAttack.cs
using After.DIP.Player;
using UnityEngine;
namespace After.DIP.Attack
{
public class SlashAttack : IAttack
{
public void ApplyDamage()
{
Debug.Log("斬撃攻撃でダメージを与える");
}
}
}
良いところ
-
SlashAttack
が変更されても、Player
を変更せずにすむ -
SlashAttack
から別のクラスに置き換えるときも簡単に可能- コンストラクタに渡す
IAttack
を実装したオブジェクトを変更するだけ
- コンストラクタに渡す
void Start()
{
var attack = new SlashAttack(); // ここを変えるだけ
var player = new Player(attack);
player.Attack();
}
新しい攻撃 MagicAttack
を追加しても。 Player
に影響がない
終わりに
もし、変なところがあったらぜひ教えてください。
他の設計原則についても、いずれまとめる予定です。