依存関係を切断しよう

 処理が別のライブラリの処理を呼び出す場合、処理はそのライブラリに依存しています。

 処理には入力と出力が必要です。入力データ型や出力データ型が基本型でなく、特別に作られたものだったなら、処理はそういったデータ型に依存することになります。コードで書くとこんな感じになります。

OutputDataType 処理(InputDataType input){ ... }

 処理と入力データ型、出力データ型のように密接な関係にある場合、依存関係を分断することは不適切です。それらはむしろ一つの塊として考えてください。

 これから処理やデータ型をひとつにまとめたものを「パッケージ」と呼ぶことにします。パッケージの内部にはそれぞれ密接に関係した型がまとめられています。問題にすべきはパッケージの間の依存関係で、依存性をできるだけ小さくすることで処理の再利用性を高め、複雑に依存しあった処理を解きほぐし理解しやすくすることができます。

依存性を注入しよう

 一例としてこのようなコマンドを解析することを考えてみましょう。

makevideo -b 600 source.avi dest.mp4

 makevideoコマンドは動画ファイルを作るコマンドで、-bの後に数字を入れると動画のビットレートを設定することができます。-bの他にも様々なオプションがあるものとします。

 説明のためにC#によく似たコードを使っていきます。

 コマンドラインの文字列を解析するために、とりあえずAnalyzeというメソッドに投げることにします。

//argsにはコマンドラインの文字列が入っています
void Main(string[] args)
{
 Analyze(args);
  ...
}

 Analyzeの中には、それがオプションであるかを判定し、オプションによってswitch文で分岐する、オプションじゃなければムービーを作成する、そんな処理を書きました。

void Analyze(string[] args)
{
  for(int i = 0; i < args.Length; ++i)
  {
    string optionStr;
    if(IsOption(args[i], out optionStr))
    {
      switch(optionStr)
      {
         case "b":
           ...//ビットレートを設定
           break;

         case ...
      }
    }
    else
    {
       CreateVideo(...);
    }
  }
}

 この処理は、「-bの場合ビットレートを設定する」というようなソフト個別の事情に基づいているので、他のソフトで再利用することが出来ません。また、CreateVideoというメソッドを呼びだしていますが、このメソッドはソフト独自のライブラリに含まれるメソッドです。ソフト独自の動画作成メソッドに依存することにより、動画作成以外の用途での使用ができなくなっています。

 余計なパッケージに依存するから悪いのです。こういった依存性は切断しましょう。

 オプションのデータだけを解析して返し、それを使って何をするかは呼び出し元に任せることにします。

//-b 600の場合、OptionStr="b", Arg="600" が入る。
class OptionArg{ public string OptionStr; public string Arg; }

//オプションはOptionsに、変換元、変換先ファイル名はFileNamesに入る。
class CommandLineArgs{ public OptionArg[] Options; public string[] Args; }

 このようなデータ型を作り、Analyzeを呼び出すと、{"b","600"}というように整理された文字列が返ってくるようにします。

void Main(string[] args)
{
 CommandLineArgs cl = Analyze(args);

  foreach(OptionArg option in cl.Options)
  {
    switch(option.OptionStr)
    {
      case "b":
        //ビットレートを設定する
    }
  }
  ...
}

 さて、Analyzeメソッドはコマンドラインの整理だけをするようになり、ソフト個別の事情から自由になったので、他のソフトからも再利用可能になりました。多少マシになった感はありますが、switch文の煩雑さなど見るに、余計な処理を書いているように見えます。

 なぜこのように煩雑になってしまっているのかというと、Analyzeメソッドがソフトの個別の事情を教えてもらっていないからです。仕方なく一般的な文字列だけで結果を返しているので、文字列をオプションに変換する煩雑な処理を書く必要が出てきます。

 再利用可能になるように、ライブラリは個別のソフトの事情に依存しないように書く必要がありますが、、ライブラリを使うときには、個別のソフトの事情を教えてやることで余計な処理を書かずにシンプルなコードにすることができます。あとから依存性を注入してやれば良いのです。

 まずOptionData型というのを作ります。

class OptionData{
 public int Bitrate;
  ...(などなどいろいろなオプションが並ぶ)
}

 ビットレートやなんかを保持しておく型です。AnalyzeメソッドにOptionData型のことを教えて、適切に扱えるようにする必要があります。AnalyzeメソッドにOptionData型への依存性を注入するのです。

 たとえば、-b 600といったデータがあったら、OptionData型のBitrateメンバに600を入れれば良い、ということを教えていきます。

OptionData data = new OptionData();

Analyze(
  Case("b", arg => data.Bitrate = int.Parse(arg)),
  Case(...),...
);

 「arg => data.Bitrate = int.Parse(arg)」

 これはラムダ式です。argにはオプションの引数が入ります。たとえば-b 600の場合は"600"という文字列が入ります。それをint.Parseで整数に変換して、Bitrateに代入しています。

 ラムダ式がまだ導入されていない言語の場合、このように書けば大丈夫です。

interface OptionAction{
  void Action(string arg);
}

class BitrateAction : OptionAction
{
  OptionData Data;

  public BitrateAction(OptionData data)
  {
    Data = data;
  }

  public void Action(string arg)
  {
    Data.Bitrate = int.Parse(arg);
  }
}

...

Analyze(
  Case("b", new BitrateAction()),
  ...

 ただラムダ式が導入されていない言語の場合、冗長で見通しの悪いコードになってしまいがちなので、依存性の注入はやらない方がベターな場合も多いです。

 さらに、CreateVideoメソッドへの依存性も注入してやります。

OptionData data = new OptionData();

Analyze(
   Case("b", arg => data.Bitrate = int.Parse(arg)),
   ...
   args => CreateVideo(args, data)
)