依存性逆転の原則(Dependency Inversion Principle)
SOLID原則のひとつ。
上位モジュールは下位モジュールに依存してはいけない。どちらのモジュールも抽象に依存すべき。
抽象は実装の詳細に依存してはならない。実装の詳細が抽象に依存すべき。
internal class BClz
{
internal string GetGreet() => "こんにちは";
}
public class AClz
{
public void Greet()
{
var b = new BClz(); // Bに依存
System.Console.WriteLine(b.GetGreet());
}
}
上記のコードは以下の図のように、AClzがBClzに依存している。
逆転とあるので、やりたいことは以下の図。
これを実現するためには、
- BClzのインターフェイスIBClzを容易する。
- IBClzを継承してBClzを実装する。
- AClzはIBClzに依存させる。
実装方法は以下の方法で行う。
依存性の注入(Dependency Injection)
簡潔に
外部からインスタンスを注入すること。
以下のクラスAはクラスBに依存している。
- クラスA:依存している
- クラスB:依存されている
class A { private B B; }
class B {}
クラスAのメンバ変数のクラスBを外部から注入する。
コード例解説
依存性の注入はSOLID原則の依存性逆転の原則を実装するために必要な概念。
依存している側で依存されている側のインスタンスは生成せず、外部から渡す。
このとき、依存している側はインターフェイスで受け取る。
以下のコードについて、
interface IBoxing
{
public int Jab();
}
class Sparring : IBoxing
{
public int Jab() => 60;
}
class Fighting : IBoxing
{
public int Jab() => 100;
}
class TitleMatch
{
private IBoxing IBoxing;
TitleMatch()
{
this.IBoxing = new Fighting(); // ★
}
public int Attack() => this.IBoxing.Jab();
}
上記の★の部分は依存している側でインスタンスを生成してしまっている。
タイトルマッチに向けてのファイトキャンプでスパーリングをしたい場合、★を修正する必要がある。
ファイトキャンプ中はnew Sparring();に変えたが、ファイトキャンプ終了時にnew Fighting();に戻さなかった場合、 60%の力でタイトルマッチに望むことになる。
そこで、外部からIBoxingを注入する。
class TitleMatch
{
private IBoxing IBoxing;
TitleMatch(IBoxing iBoxing)
{
this.IBoxing = iBoxing; // ★
}
public int Attack() => this.IBoxing.Jab();
}
インターフェイス分離の原則(Interface Segregation Principle)
SOLID原則のひとつ。
クライアントが使用しないメソッドへの依存を、強制すべきではない。
インターフェイスの具象クラスでは、メソッドの実装が強制される。
つまり、具象クラスで不要なメソッドの作成が強制されないように、小さなインターフェイスを作る必要がある。
悪い例
格闘について考える。
public interface IFighting
{
public void Punch();
public void Kick();
public void TakeDown();
public void Choke();
}
ボクシング、キックボクシング、レスリング、柔術などは格闘の一環のため、IFightingの具象クラスとした場合。
ボクシングにKick()、TakeDown()、Choke()
キックボクシングにTakeDown()、Choke()
レスリングにPunch()、Kick()、Choke()
柔術にPunch()、Kick()
など不要なメソッドの実装を強制してしまう。
改善案
クライアント(ボクシング、キックボクシング、レスリング、柔術)が使う単位でインターフェイスを分離する。
public interface IHandStriking
{
public void Punch();
}
public interface IFootStriking
{
public void Kick();
}
public interface IThrowing
{
public void TakeDown();
}
public interface IGrappling
{
public void Choke();
}
public class Boxing : IHandStriking{}
public class KickBoxing : IHandStriking, IFootStriking{}
public class Wrestling : IThrowing{}
public class JiuJitsu : IThrowing, IGrappling{}
型の入出力規格の互換性
型の制限の強弱
「強い・弱い」、「狭める・広げる」等が使われる。
以下の3つのクラスをもとに、型の制限の強弱を考える。
class A {}
class B : A {}
class C : B {}
それぞれを別ファイルにあると考えると分かりやすい。
- AはBの存在もCの存在も知らない。
- BはAの存在を知っているが、Cの存在は知らない。
- CはAの存在もBの存在も知っている。
このため、以下のような代入が結果となる。
B a = new A(); // NG(Bは知らないので、AがBのように振舞えない)
C a = new A(); // NG(Cも知らないので、AはCのように振舞えない)
A b = new B(); // OK(Aを知っているので、BはAのように振舞える)
C b = new B(); // NG(Cを知らないので、BはCのように振舞えない)
A c = new C(); // OK(Aを知っているので、CはAのように振舞える)
B c = new C(); // OK(Bを知っているので、CはBのように振舞える)
以上のことを考えると
- Aに対してBやCは型の制限が強い
- Cに対してAやBは型の制限が弱い
共変性(Covariance)
最初に指定された型よりも強い派生型を使用できるようにする。
派生型を返す処理も扱えるようにする。
反変性(Contravariance)
最初に指定された型よりも一般的(弱い派生)型を使用できるようにする。
基底型を受け取る処理も扱えるようにする。
不変性(Invariance)
最初に指定された型のみを使用できるようにする。
契約による設計(Design By Contract)
プログラムコードの中にプログラムが満たすべき仕様について記述を盛り込むことで設計の安全性を高める。
仕様の記述を盛り込む
C#ではAssertを使用する。
System.Diagnostics.Debug.Assert(//条件)
条件にはtrueになる条件を記述する。
AssertはDebugビルド時は処理されるが、それ以外では処理されない。
契約とは、コードの利用条件が満たされることによって成立する。
条件は満たすべきタイミングと主体によって、以下の3種類に分けられる。
- 事前条件(Precondition)
サブルーチンの開始前に保証すべき性質。
主体は呼ぶ側
Assertについては、一般的にサブルーチンの最初で引数チェックを記述する。
- 事後条件(Postcondition)
サブルーチンが終了時に保証すべき性質。
主体は呼ばれた側
Assertについては、一般的にサブルーチンのreturn前に戻り値のチェックを記述する。
- 不変条件(Invariant)
オブジェクトがその外部に公開しているすべての操作の開始時と終了時に保証すべき、オブジェクトごとに共通した性質。
主体は呼ぶ側と呼ばれた側の両方
リスコフの置換原則(Liskov Substitution Principle)
継承関係が正しいかの判断基準。
派生型(サブクラス)は、その基底型(スーパークラス)と置換可能でなければならない。
基底型の変数に派生型の値を代入しても支障をきたさないこと。
サブタイプはスーパータイプのオブジェクトのすべての振る舞いと、更に別の何かを備えたもの。
インターフェイスルール
- 引数の数は同一
- 引数は反変性
- 戻り値は共変性
- 例外は同一または、そのサブタイプ
プロパティルール
- 不変条件
スーパータイプでプロパティAの値がxを超えない場合、サブタイプでもこの条件を保持する必要がある。
- 制約
スーパータイプから新規追加またはオーバーライドされたメソッドは、スーパータイプで許可されていない方法でプロパティの値を変更してはならない。
開放閉鎖の原則(Open/Closed Principle)
SOLID原則のひとつ。
開放(Open) | 拡張に対して開いている。 |
---|---|
閉鎖(Closed) | 修正に対して閉じている。 |
新しく機能を追加するとき、既存のコードを変更せずにあたらしいコードを追加するだけで済むようにする。
格闘家を例にとる
一般的に格闘技には階級がある。
選手は規定の体重以内で計量をパスしなければならない。
public enum WeightClass
{
Heavy,
Light
}
public class Fighter
{
public WeightClass WeightClass;
public string Name { get; private set; }
public double Weight { get; set; }
public Fighter(string name, WeightClass weightClass)
{
this.Name = name;
this.WeightClass = weightClass;
}
public bool WeightIn()
{
switch (this.WeightClass)
{
case WeightClass.Light:
return this.Weight <= 70.3;
default:
return this.Weight <= 120.2;
}
}
}
階級を増やす
競技人口の増加により、2階級のみでは競技性が損なわれるため、HeavyとLightの間にMiddleを増やしたい。
WeightClassにMiddleを増やすのは容易(拡張に対して開いている)だが、 WeightInに修正が発生(閉じていない)する。
解決策
格闘家を基底クラスとして、階級ごとに派生クラスを作成する。
public enum WeightClass
{
Heavy,
Middle,
Light
}
public abstract class Fighter
{
public WeightClass WeightClass;
public string Name { get; private set; }
public double Weight { get; set; }
public Fighter(string name, WeightClass weightClass)
{
this.Name = name;
this.WeightClass = weightClass;
}
public abstract bool WeightIn();
}
public class HeavyWeightFighter : Fighter
{
private static readonly double LIMIT = 120.2;
public HeavyWeightFighter(string name, WeightClass weightClass) : base(name, weightClass) { }
public override bool WeightIn() => this.Weight <= LIMIT;
}
public class MiddleWeightFighter : Fighter
{
private static readonly double LIMIT = 83.9;
public MiddleWeightFighter(string name, WeightClass weightClass) : base(name, weightClass) { }
public override bool WeightIn() => this.Weight <= LIMIT;
}
public class LightWeightFighter : Fighter
{
private static readonly double LIMIT = 70.3;
public LightWeightFighter(string name, WeightClass weightClass) : base(name, weightClass) { }
public override bool WeightIn() => this.Weight <= LIMIT;
}