おふくろさまより愛をこめて

mmmpa ふとしです。誠実なプログラミングを心がけたい。

依存性の逆転について

現代のプログラミングでは Plugin という仕組みや Polymorphism が一般的になっているため + 動的型付け言語を使用してきたため、依存性の逆転 が具体的になにを解決しているのかよくわかっていませんでした。なにしろ雰囲気でプログラミングをやってきたので。

現在 Clean Architecture を少しずつ読んでいます。読みながら極端に悪い依存性を持つ状態をイメージし、そこに依存性の逆転が持ち込まれると何が解決されるのかを見ることによってようやく理解できました。

以下は自分が理解するために書いたものです。

実行処理の上流の module が下流の module の実装に依存する状態

実行処理の流れがそのまま依存の流れになっている (依存先の詳細を知らなければならない) 状態です。A -> B -> C という順に処理が呼び出される場合、A は B に依存し、B は C に依存するという構造です。

例では Processor は上流からの呼び出しに従い任意の device を使って readwrite を行います。

極端に悪い例

MicTabletTerminal が公開しているメソッド名が統一されていないため、使う device によって処理を切り替える必要があります。「依存先の詳細を知らなければならない」状態の悪さがわかると思います。

import Mic from './Mic';
import Tablet from './Tablet';
import Terminal from './Terminal';

export default class Processor {
  device: Mic | Tablet | Terminal;

  constructor (device: Mic | Tablet | Terminal) {
    this.device = device;
  }

  read (): string {
    switch (this.device.constructor) {
    case Mic:
      return (this.device as Mic).listen();
    case Tablet:
      return (this.device as Tablet).write();
    case Terminal:
      return (this.device as Terminal).in();
    default:
      return '';
    }
  }

  write (s: string) {
    switch (this.device.constructor) {
    case Mic:
      (this.device as Mic).say(s);
      return;
    case Tablet:
      (this.device as Tablet).display(s);
      return;
    case Terminal:
      (this.device as Terminal).out(s);
      return;
    default:
      //
    }
  }
}

Plugin 化された例

メソッド名を統一することで随分すっきりしたコードになりました。

import Mic from './Mic';
import Tablet from './Tablet';
import Terminal from './Terminal';

export default class Processor {
  device: Mic | Tablet | Terminal;

  constructor (device: Mic | Tablet | Terminal) {
    this.device = device;
  }

  read (): string {
    return this.device.in();
  }

  write (s: string) {
    this.device.out(s);
  }
}

しかし、未だに下流の module に依存している状態です。MicTabletTerminal のいずれかが変更されると、Processor のコンパイルしなおしが必要になってしまいます。

依存性の逆転

そこで MicTabletTerminalProcessor 用の Interface IDevice に依存するように変更します。つまり、下流の module が上流の module の実装に依存するように、処理の流れとは逆にするということです。

export default interface IDevice {
  out (s: string): void
  in (): string
}
import IDevice from './IDevice';

export default class Processor {
  device: IDevice

  constructor (device: IDevice) {
    this.device = device;
  }

  // snip
}
import IDevice from './IDevice';

export default class Mic implements IDevice {
  out (s: string): void {
  }

  in (): string {
  }
}

(他の Device は省略)

これですべての module が独立してコンパイルできるようになりました。また何か新しい Device が導入されたとしても、その Device は IDevice を実装すれば Processor にはなんの変更もせず使用することができます。

Architecture の観点から

この依存性の逆転はソースコードの依存性を一方向性を解決し、依存の方向をどちらへも向けることを可能にします。

Clean Architecture では、システム/ビジネスのポリシーやルールに基づいて、Open-Close の原則の方向を決めます。

そこでこのソースコードレベルの依存性によらない自由な依存の方向が必要になります。

まとめ

実際、Ruby などの動的型付け言語では plugin 化の段階ですでに依存性の逆転は達成されています。Duck Typing を満たす限りどんな Device も Processor で使用できるからです。

依存性の逆転について、粗結合に優れるコードを書くための概念としては把握していました。しかし今回、極端に悪い例と実際に逆転を意識しないと実現できないコードを知ることにより、具体的に何を解決していたかを知ることができました。