依存性の切断の一般的手法
class Aがclass BのメソッドMに依存しているとします。AからBへの依存関係を切断する一般的手法をコードで示します。
状況はこのように表せます。
class A { public void Hoge() { ... new B().M(); ... } } class B { public void M(){...} }
引数がなく戻り値がない関数型を{ void => void }のように表すことにします。int, stringを引数に取りdoubleを返す関数型なら{ int, string => double }です。
AからBへの依存性を切断するとこうなります。
class A { //引数がなく戻り値もない関数を受け取る public void Hoge({ void => void } m) { ... m(); ... } } class B { public void M(){...} } void Main() { A a = new A(); //B.Mを与える a.Hoge(() => new B().M()); }
AからBを直接呼び出さず、Aの呼び出し側からBへの依存性を注入してやることでAからBへの依存性を切断することができます。
ただし、この切断が必要になるのは、AからB.M()以外を呼び出す可能性がある場合です。class Aの目的上、どう考えてもB.M()以外を呼び出すことがないなら、関数型にしてラムダ式でB.Mを与えてやる必要もありません。そういう場合は、後述する「依存性の囲い込み」を行ってください。
B.M()以外を呼び出すならば、AからBへの依存性を切断することでより汎用的に使えるようになり、再利用性が高まります。
依存性の循環とカプセル化
B.M()以外を呼び出すことがないような、汎用性が必要ない場合でも、経験上依存性を切断したほうがいいこともあります。それは依存性が循環する場合です。
class AがBのメソッドMBに依存し、class BがAのMAに依存するとします。AとBがお互いに依存しあい、依存性が循環しています。
class A { public B B; public void MA(){ ... } public void Hoge(){ ... B.MB(); ...} } class B { public A A; public void MB(){ ... } public void Hoge(){ ... A.MA(); ... } } void Main() { var a = new A(); var b = new B(); a.B = b; b.A = a; a.Hoge(); b.Hoge(); }
クラスA,Bがお互いにお互いをフィールドとして持ち合う形になっています。こういう場合はコンストラクタで初期化することが困難です。
無理やり書こうとすると、こんなふうになります。
var a = new A(new B(a));
まだ初期化されていないaをBのコンストラクタに突っ込もうとしてコンパイルエラーを引き起こしてしまいます。なのでpublicにして後で代入していますが、これは必要のないメンバの露出でありカプセル化が損なわれています。
また、AはB.MB()に、BはA.MA()に依存していますが、A.Hoge()、B.Hoge()には依存していません。なのにAはBをフィールドに所持しているので、B.Hoge()にアクセスすることができます。Bも同じです。
クラスは一般的に大きくなりすぎ、クラスのインスタンスを渡すと必要のないメンバへのアクセス権まで渡してしまうことが多くあります。この場合はHogeメソッドが余計なので、Hogeメソッドを別のクラスに移すことで対応が可能かもしれません。しかし、そんな場当たり的な対処をする必要は本来ありません。必要なメンバへのアクセス権を必要なだけ注入してやれば良いのです。
class A { public void MA(){...} public void Hoge({void => void} m){ ... m(); ... } } class B { public void MB(){ ... } public void Hoge({void => void} m){ ... m(); ... } } void Main() { var a = new A(); var b = new B(); a.Hoge(() => b.MB()); b.Hoge(() => a.MA()); }
AとBの依存性を切断し、余計なメンバへのアクセスも遮断することができました。一般に、必要なメソッドがある場合は、関数型にして外部から入れてもらうことで依存性が切断できます。
今回は必要ないですが、フィールドに持つ場合も書いておきます。
class A { {void => void} M; public A({void => void} m){ M = m; } public void MA(){ ... } public void Hoge(){ ... M(); ... } } class B { {void => void} M; public B({void => void} m){ M = m; } public void MB(){ ... } public void Hoge(){ ... M(); ... } } void Main() { B b = null; var a = new A(() => b.MB()); b = new B(() => a.MA()); a.Hoge(); b.Hoge(); }
循環しあって初期化できなかったものが、ラムダ式の魔法を使うことで初期化可能になりました。
B b = null; var a = new A(() => b.MB());
「() => b.MB()」このラムダ式が実行されるのは、a.Hoge()を実行した時です。つまり
b = new B(() => a.MA());
この初期化が行われた後になりますから、ヌルポインターエクセプションは起こりません。