依存性(3)

概念整理(2)

 ソフトがその目的を達成するためには、必要な処理を正しい順番で過不足なく記述する必要があります。オブジェクト指向言語でそれを記述すると、処理の順序はオブジェクトの木に置き換えられます。

 オブジェクトは依存するオブジェクトのメソッドを呼び出します。前回循環する依存関係を無条件で排除したので、依存関係は循環せず、オブジェクトは複数のオブジェクトに依存し、依存するオブジェクトもまた複数のオブジェクトに依存する、という木構造の形をとります。

 依存性の木はどのように構成されるべきでしょうか。

依存性の囲い込み

 class Bに依存するclass Aがあったとします。BにはMainメソッドから呼び出されるメソッドと、Aから呼び出されるメソッドがあります。

class B
{
  public void FromA(){...}
  public void FromMain(){...}
}

class A
{
  B B;

  public A(B b){ B = b; }

  public void Hoge(){ ... B.FromA(); ... }
}

void Main()
{
  B b = new B();
  A a = new A(b);

  a.Hoge();
  b.FromMain();
}

 MainがA,Bに依存し、AはBに依存しています。この設計で問題なのは、Bが複数箇所に現れていることです。

Main-A-B
    -B

 Bが2箇所に存在していると、木の構造が複雑になり、Bに対して誰がアクセスする責任を持つのかわかりにくくなります。複数ヶ所からいじられれば意図した状態を保つことも難しくなります。

 よって依存性の木においてオブジェクトの重複は禁止する必要があります。なので出来る形は2つです。AからBへの依存性を切断してMainにBを持たせるか、AがBを囲い込んでMainからBへの依存性を無くすかです。

 今回は依存性を囲い込みます。依存性を切断するか囲い込むかは、依存先のメソッドを別のメソッドに置き換える必要の有無で判別します。

class A
{
  B B;

  public A(){ B = new B(); }

  public void Hoge(){ ... B.FromA(); ... }

  //FromMainだけを露出させる。
  public void FromMain(){ B.FromMain(); }
}

void Main()
{
  A a = new A();

  a.Hoge();
  ...
  a.FromMain();
}

 Aが依存性を囲い込み、MainからBへの依存性をなくすことができました。(注)

 ところで話が飛びますが、コンストラクタでnewするのは良くない設計です。

public A(){ B = new B(); }

 外部から入れるようにするだけで汎用性が増し、依存性がコンストラクタに現れるので外部からも分かりやすく表現できます。

public A(B b){ B = b; }

 呼び出し側はこうなります。

void Main()
{
  A a = new A(new B());

  ...
}

 MainからBのコンストラクタを使っていて、Bにいくらか依存してしまっていますが、実は初期化の時にはどうしても依存性が出てきてしまいます。

 なので依存性の木のルールは初期化時には適用できません。初期化時には好きなだけnewを呼び出してコンストラクタに入れてください。それが終わったら触らないようにしましょう。

 ちなみに、どうしてもこのオブジェクトの状態はこれでなければダメだ、というのがあるならコンストラクタでnewし、外部には触らせないようにしてください。

public A(){ B = new B(3.1415926535); /*どうしてもこれで初期化しなければダメ*/ }

依存性の木

 classがなぜそのメンバを持っているのか、そのclassがなぜ作られたのか、という問いにはいろいろな答えがありえます。状態をprivateフィールドに隠蔽してコントロールするためにclassが必要になったり、多態性のために必要になったり(ラムダ式の登場以降ほとんど必要なくなりましたが)、クラスが大きくなりすぎて見通しが悪いので単にソースファイルを分割するためにクラスが必要になったりします。classは恣意的に、適当に構成されるものです。

 依存性の木によってメソッドの呼び出し順序はある程度規定されますが、メソッドの呼び出し順序のためだけにクラスが構成されているわけではないので、依存性の木からはみ出した順序での呼び出しが必要になることもあります。Bのメソッド、FromMain()はAからはみ出しています。こういったハミ出しは大きいより小さいほうが良いですが、それよりも状態を隠蔽する方がずっと重要なので、無理にハミ出さないようにする必要はありません。

 大きくはみ出す例を見てみます。依存性の木がこのように構成されているとします。

Main-A-B
    -C-D

 MainからA-Bという木とC-Dという木が生えています。

 DからBのメソッド、FromDを呼び出したいとします。

class A
{
  B B;

  public A(B b){ B = b; }

  ...
}

class B
{
  public void FromD(){...}
  ...
}

class C
{
  D D;

  public C(D d){ D = d; }

  public void Hoge(){ ... D.Hoge(); ...}
}

class D
{
  public void Hoge(){ /* B.FromDを呼び出したい */ }
}

void Main()
{
  A a = new A(new B());
  C c = new C(new D());

  c.Hoge();
  ...
}

 一番悪いのは生のBを渡すことです。AとDの2箇所にBが繋がってしまいます。DがBのパブリックメンバ全てへのアクセス権を得てしまう上に木の構造も複雑化します。

class D
{
  B B;

  public D(B b){ B = b; }

  public void Hoge(){ ... B.FromD(); ... }
}

void Main()
{
  B b = new B();
  A a = new A(b);

  //生のbをDに渡す
  C c = new C(new D(b));

  c.Hoge();
  ...
}

 一番良いのは、メソッドの引数としてfromDを渡す形です。

class D
{
  public void Hoge({void => void} fromD){ ... fromD() ... }
}

class C
{
  D D;

  public C(D d){ D = d; }

  public void Hoge({void => void} fromD){ D.Hoge(fromD); }
}

void Main()
{
  A a = new A(new B());
  C c = new C(new D());

  c.Hoge(() => a.fromD());
}

 この場合はB.FromD()メソッドがAにはみ出し、C、Dと流れています。依存性の木を伝って長い距離をはみ出していく様子が処理を辿るとわかります。

 ただし、呼び出し側でfromDを引数として与えられない場合も考えられます。GUIを用いたイベント駆動のアプリケーションでは、始点がMainではなく様々な場所から始まりうるので、たとえば始点がCになったときfromDメソッドを取得できません。

 そういった場合には、Dにフィールドとして持たせる必要があります。

class D
{
  {void => void} FromD;

  public D({void => void} fromD){ FromD = fromD; }

  public void Hoge(){ ... FromD(); ... }
}

 まず悪い設計を書いておきます。

void Main()
{
  B b = new B();
  A a = new A(b);
  C c = new C(new D(() => b.fromD());
}

 b.fromD()を直接Dのコンストラクタに渡しています。こうすると、FromDをAにはみ出させる必要も、Cを経由して渡す必要もありません。

 この場合の問題は、Dの初期化のためにBのインスタンスが必要になることです。この例ではMain関数ですべてのオブジェクトを初期化するようになっていますが、オブジェクトが増えてくればそのうち破綻し、分割する必要が出てきます。自然に分割すると、自分の子のオブジェクトの初期化に責任を持つ形に分割されるでしょう。

 その際に、Aまではみ出させてCのファクトリーメソッド等に流しこむ必要性が出てきます。

void Main()
{
  A a = CreateA();
  C c = CreateC(() => a.FromD());
}

 木に沿って親の方向にはみ出させ、子の方向に流しこむようにしておくことで、そのメソッドがどこで使われているのかわかりやすくなります。