依存性逆転の原則(Dependency Inversion Principle), the D in SOLID

SOLIDの一つである、依存性逆転の原則(Dependency Inversion Principle, DIP)は一体どういうものか?色々調査してみた。今までもはっきりわかったと言えないがある程度納得したと思う。

まず、依存性の注入(DI)は依存性逆転の原則(DIP)ではない

よく言われてる依存性の注入(Dependency Injection, DI)と依存性逆転の原則は、全く別のものである。

あるクラスBの中には、別のクラスAを使う場合、BはAを依存している

class A {
    public A(int p) {
        // ...
    }
}
class B {
    public B() {
        A a = new A(1);
        // ...
    }

    public static void main(String[] args) {
        B b = new B();
    }
}

DIていうのは、その依存対象を直接扱う代わりに、外から注入することです。

class B {
    public B(A a) {
        // ...
    }

    public static void main(String[] args) {
        A a = new A(1);
        B b = new B(a); // Bの依存を注入する
    }
}

SpringとかのフレームワークはDIを利用して依存関係を管理して注入してくれる。

class B {
    @Autowired
    public B(A a) {
        // ...
    }
}

その次、Programming to an Interfaceを理解する必要ある

この原則は、下記の感じだと思う

List<String> todos1 = new ArrayList<>(); // 良い
ArrayList<String> todos2 = new ArrayList<>(); // 良くない

Map<String, String> routes1 = new HashMap<>(); // 良い
HashMap<String, String> routes2 = new HashMap<>(); // 良くない

特定の実現を依存せず、Interfaceを依存する、でいうことです。 下記のようなソース、よく書いた、、

class Something {
    private ArrayList<String> data;
    public Something(ArrayList<String> d) {
        this.data = d;
    }
}

Somethingクラスを使うクライアント側、ArrayListしか選択肢がないから、LinkedListを使いたい場合、ケンカになっちゃう。 Programming to an interfaceを従うと、

class Something {
    private List<String> data;
    public Something(List<String> d) {
        this.data = d;
    }
}

クライアント側好きなList実現を自由に使える

ようやく依存性逆転の原則を理解してみよう

明確な定義というと1

  • High-level modules should not depend on low-level modules. Both should depend on abstractions.
  • Abstractions should not depend on details. Details should depend on abstractions.
  • 上位レベルのモジュールは下位レベルのモジュールに依存すべきではない。両方とも抽象(abstractions)に依存すべきである。
  • 抽象は詳細に依存してはならない。詳細が抽象に依存すべきである。

以下の例を考えてみよう。 あるTextクラスを操作できるエディタークラスを実装する

package foo.bar.file;

class Text {
    private String content;
    private int size;
    public Text(String filename) {
        // ...
    }

    public int size() {

    }

    public String getContent() {

    }

    public void setContent() {

    }
}
package foo.bar.editor;

class Editor {
    private Text target;
    private Text temp;

    public open(Text text) {
        
    }
    public void new(Text text) {
        
    }

    public Text duplicate() {

    }

    public void save() {
        this.target.setContent(this.temp.getContent());
    }

    public void update(String newContent) {
        this.temp.setContent(newContent);
    }

    public static void main(Stirng[] args) {
        Text text = new Text("to/somewhere/bah.txt");
        Editor editor = new Editor(text);
    }
}

ここまでは上位レベル(Editor)が下位レベル(Text)を依存しています。Textの定義が変わった場合、Editorが使えなくなる可能性も高いです。

ではEditorの定義を修正しようか。まず、Editorが使えるファイルを定義する。

package foo.bar.editor;

interface File {

    public void init(String filePath);

    public int size();

    public String getContent();

    public void setContent(String content);
}

Editorはこのインタフェースを依存して実装する。

package foo.bar.editor;

class Editor {
    private File target;
    private File temp;

    public open(File file) {
        
    }

    public void new(File file) {
        
    }

    public File duplicate() {

    }

    public int size() {

    }

    public void save() {
        this.target.setContent(this.temp.getContent());
    }

    public void update(String newContent) {
        this.temp.setContent(newContent);
    }

    public static void main(Stirng[] args) {
        File text = new Text("to/somewhere/bah.txt");
        Editor editor = new Editor(text);
    }
}
package foo.bar.file;

import foo.bar.editor.File;

class Text implements File {
    private String content;
    private int size;
    public Text(String filename) {
        // ...
    }

    @Overrided
    public int size() {

    }

    @Overrided
    public String getContent() {

    }

    @Overrided
    public void setContent(String contents) {

    }
}

こうすると、Editorは下位レベルのText実装を依存せず、自分が必要なファイル定義をコントロールしている。そして、下位レベルの実装が上位のインタフェース定義を従わないといけないので、下位レベルが上位を依存するように、依存関係が逆転になった。

上記の例で、Interface定義とEditorが同じパッケージにいるので、上位レベルが抽象定義を所有していると言える。実は上位レベル、下位レベルと抽象定義は全部異なるパッケージに所属し、それぞれ隔離されるケースも普通です。

現実の例 java.Util.Scannerのコンストラクタの2つ:

https://docs.oracle.com/javase/jp/8/docs/api/java/util/Scanner.html

// java.nio.file.Path
Scanner(Path source)
// java.lang.Readable
Scanner(Readable source)

参考

The Dependency Inversion Principle in Java

初学者でも10分で理解できる依存性逆転の原則(Dependency inversion principle)

comments powered by Disqus