Java
Ruby
Rails

RubyとDependency Injectionについてまとめる

Laravelで開発しててDIコンテナに出くわし、そういえばRailsで開発していてDI利用したことなかったなと思ったので、なぜRuby(Rails)でDIが一般的ではないのかのまとめる。

TL;DR

  • Rubyはインスタンスへの依存性も動的に変更できるから基本的にDI不要
  • ほぼ下記記事参照なので、英語できる人はこっちの記事をどうぞ LEGOs, Play-Doh, and Programming

DI

DI(Dependency Injection)は依存性の注入とよばれるインスタンス間の依存性を解消する技術。外部から依存するインスタンスを注入できるのでテストのときにモックオブジェクトの差し替えなどで便利。またDIフレームワークはDIコンテナにインスタンスの生成と注入を任せることで疎結合・高凝集を実現するテクニックである。
ネイティブアプリ(iOSは知らない)だと一般的に使われている技術でDIフレームワークもいくつかあったりするのでJavaを例にとってみてみる。

Java

public class A {
    // A内部でBインスタンスを生成しており結合度が高い
    public Client client = new B();
}

interface Client {
    String outputSelf();
}

public class B implements Client {
    @Override 
    public String outputSelf() {
        return "B";
    }
}

public class C implements Client {
    @Override 
    public String outputSelf() {
        return "C";
    }
}

これを解決するのがDIで、いくつか種類があるがコンストラクタインジェクションを例にとると以下の様な感じ。
AがBの実体を持たなくなり疎結合になる。

public class A {
    public Client client;
    public void A(Client client) {
        this.client = client;
    }
}

ただ、これだとAに注入するインスタンスの生成を呼び出し側で担うことになり責務が分散するということで、DIコンテナ(ex. dagger2)を使うとこんな感じになる。Activityへの依存性の注入はAを含めすべてDIコンテナが担う。

public class A {
    public Client client;
    @Inject
    public void A(Client client) {
        this.client = client;
    }
}

Ruby

ここまでに説明したようにDIにより具象クラス(インスタンス)ではなくインターフェースに依存させることでオブジェクトの差し替えが容易になる。
では冒頭の結論のようにRubyにおいてDIコンテナのような仕組みが不要とはどういうことかというと、インジェクションを行いたい場合でもRubyが本来持つ言語機能で十分に疎結合・高凝集が達成できるので、DIコンテナを利用することは複雑さを増すだけになりかねない、ということらしい。

Javaの例と同様にコンストラクタインジェクションを行うとすると以下のようになる。

class A 
  def initialize(options={})
    @client_impl = options[:client] || B
  end
  def new_client
    @client_impl.new
  end
end
class B end
class C end

Reflection

プログラムの実行過程でプログラム自身の構造を読み取ったり書き換えたりする技術のこと。この記事でいうとクラスAのメンバーであるclientの実体を外部から動的に書き換える方法ということになる。

Ruby

元記事でRubyとは以下のような言語だと述べられている。

The very Ruby language itself is designed for this: closures, super-simple introspection of objects, runtime modification of existing objects, and the use of modules for extending classes and objects all tend to result in an environment that is simple, malleable, and extensible.

従ってダイナミックにRubyっぽく書くならば、

class A
  def new_client
    client.new
  end
  def client
    B
  end
end

class B end
class C end

生成されたインスタンスであっても変更が容易に行える。

# インスタンスメソッドを動的に変更可能
def A.new.client
  C
end

Java

Javaでもリフレクションはサポートされているので、以下のようにインスタンスのメンバーの差し替えは可能だが、、、

class D {
    public static void main(String args[]){
        try {
            A a_instance = new A();
            Class a = a_instance.getClass();
            Field field = a.getDeclaredField("client");
            System.out.println("before reflection:" + ((Client) field.get(a_instance)).outputSelf());
            field.setAccessible(true);
            field.set(a_instance, new C());
            System.out.println("after reflection:" + ((Client) field.get(a_instance)).outputSelf());

        } catch (Exception e) {}
    }
}

インジェクトした場合に比べると圧倒的にわかりにくいし複雑さが増す。

結論

RubyはJavaのような言語と比べて動的であり一度生成したインスタンスであっても変更が容易であり依存の変更も容易である。よって静的な言語において必要性があったDIコンテナのような仕組みを導入しても複雑性が増してしまうだけなのではないか、ということらしい。