paild tech blog

法人カードのクラウド型発行・管理サービスpaild

Rust の DI を考える — Part 1: DI とは何だったか

paild 社でお手伝いをしている yuki です。みなさんは Rust で DI をしようと思った際に困ったことはありませんか?この連載では、他のプログラミング言語で利用される DI パターンを参照しながら、Rust でそれを実装するためにはどのような工夫が必要かまでを検討します。中には Rust での実装が難しいパターンも出てくるかもしれません。その際は、なぜ難しいのかまでを検証します。


そこそこの規模のソフトウェアを実装するにあたって、ソフトウェアエンジニアが共通して利用する手法がいくつかあると思います。その中でも DI (Dependency Injection; 依存オブジェクト注入) は最もポピュラーな手法の一つであり、保守運用まできちんと耐えうるソフトウェアの設計をしたいとなったときに、まず真っ先に候補に上がる手法でしょう。

Rust ではこの DI をどのように行えばよいのでしょうか。このシリーズでは、DI についてまず何であったかをおさらいし、Rust 以外のプログラミング言語で DI がどのように扱われているかを確認します。[*1]そして最後に、これらのプログラミング言語で紹介した手法を Rust で当てはめるとどのように実装できる可能性があるかを紹介します。

DI の是非についてはさまざま議論があるようです。Ruby などの柔軟な動的型付き言語では、そもそも DI などの手法を利用せずとも柔軟に疎結合なコードを書くことができます。こうした文脈では DI は不要、という意見もあります。

しかし、今回筆者は Rust における DI を前提します。Rust のような静的型付き言語では、依然として DI は有効な手法であると考えています。ただし、Java の文脈に登場するような多機能な DI はさほど必要はないと考えており、そうした議論については適宜補足を入れる想定です。[*2]

Dependency Injection (DI) とは何か

Dependency Injection (以降、DI) という言葉自体は、Wikipedia などを辿ると Martin Fowler 氏が最初に提唱し始めたようです。[*3]Martin Fowler 氏の書いた「Inversion of Control and the Dependency Injection pattern」[*4]という記事により、一般に用語が普及し始めたようです。

記事中にもある通り、元々は Inversion of Control という用語で DI 相当の概念が議論されていました。しかしこの「Inversion of Control」という名称がわかりにくいなどの理由で、有識者同士で適切な用語を議論しあったようです。その結果生まれたのが「Dependency Injection」という用語でした。

DI とは何かを知るにあたり、DI がまず何をするものなのかを説明します。次に、DI を構成する概念の説明をします。最後にDI をはじめとする技術的な概念は、どのような問題を解決し、利用者にどのようなメリットをもたらすのかを知るべきでしょう。

DI は何をするのか?

DI が最終的に何をするのかという点については、下記2つから説明できると筆者は考えています。

  1. コンポーネントを外側から注入し、コンポーネント同士を疎結合にする。
  2. 依存関係を自動で解決し、コンポーネント管理の手間を少なくする。

DI を導入することで、コンポーネント同士を疎結合にすることができます。コンポーネントというのは詳しくは後ほど説明しますが、オブジェクトならオブジェクト、クラスならクラスと考えてください。DI ではこのコンポーネントを外側から注入するようにし、コンポーネント同士を疎結合にすることができます。また、DI を導入すると、コンポーネント間の依存関係をある程度自動で解決できるようになります。これにより情報の一元管理の実現や依存関係の管理コストの低減を期待できます。

もっともシンプルな DI

ユーザーに関する情報を操作するための簡単な Java コードを通じて簡単に例を示します。今回は2つのコンポーネントと1つのデータを用意します。

まずユーザーは次のようなデータ構造をしているものとします。

package com.github.yuk1ty.bootdi.users;

public record User(String userId, String userName, boolean effective) {}

次に、このユーザーを操作する UserServiceUserRepository を用意します。これらを「コンポーネント」と呼びます。

まず UserRepository の実装は次のようになっているものとします。

package com.github.yuk1ty.bootdi.users;

import java.util.Optional;

public class UserRepository {

    public UserRepository() {
        // コンストラクタの内部処理を何か用意するかもしれない。
    }

    public Optional<User> findUser(String userId) {
        return Optional.empty();
    }

    public void updateUser(User user) {
        // ユーザーのデータの更新処理を書く。たとえばデータベースへの接続を行い、UPDATE 文を書くなど。
    }
}

次に、UserService は次のようになっているものとします。この際、UserServicedeactivate というメソッドの中で UserRepository のメソッドを呼び出しします。したがって UserService 側に、何かしら UserRepository の情報が保持されている必要があります。その情報の保持のために、コンストラクタの引数として UserRepository を渡しています。このように UserRepositoryUserService の外から渡されて(注入されて)います。

package com.github.yuk1ty.bootdi.users;

public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void deactivate(String userId) {
        var user = this.userRepository.findUser(userId);
        user.ifPresent(u -> {
            var updated = new User(u.userId(), u.userName(), false);
            this.userRepository.updateUser(updated);
        });
    }
}

DI を利用しておくと、使う側(UserService)は、使いたいオブジェクト(UserRepository)がどのような依存解決をされたのかをあまり気にする必要はありません。なぜならすでに生成済みのコンポーネントを渡すだけでよくなるためです。コンストラクタにある使いたいコンポーネントをそのまま呼び出すのみです。

呼び出す際には、まず UserRepositoryインスタンスを生成し、UserServiceインスタンスを生成します。UserServiceインスタンスを生成するタイミングで先に作っておいた UserRepository の値をコンストラクタに入れます。

public class Application {
    public static void main(String[] args) {
        var userRepository = new UserRepository();
        var userService = new UserService(userRepository);
        userService.deactivate("user-a");
    }
}

DI コンテナを利用する例

また、DI を利用する際には DI コンテナと呼ばれる機能を利用できる場合があります。DI コンテナというのはさしあたっては、自動で依存関係を解消してくれるブラックボックスのようなものと考えてください。コンポーネントの数が増え、依存関係を整理する対象が増えると必然的にコンポーネント生成のために依存を渡す管理コストが膨らみます。DI コンテナを利用すると、こうした依存関係をプログラマが手で整理することはなく、マクロやリフレクションなどの機能により自動的に解決した状態にすることができます。これにより大幅に手間が軽減されます。

Java には Spring Framework というフレームワークがあるのですが、この Spring Framework は DI コンテナをサポートしています。これを用いて、先ほどの UserServiceUserRepository のコードを DI コンテナで書き換えてみます。

UserRepository の実装を書き換えてみたものです。前段として @Component というアノテーションを付与すると、そのコンポーネントは DI される(あるいはする)対象として使用できるようになります。

package com.github.yuk1ty.bootdi.users.spring;

import com.github.yuk1ty.bootdi.users.User;
import org.springframework.stereotype.Component;

import java.util.Optional;

@Component
public class UserRepository {

    public UserRepository() {
        // コンストラクタの内部処理を何か用意するかもしれない。
    }

    public Optional<User> findUser(String userId) {
        return Optional.empty();
    }

    public void updateUser(User user) {
        // ユーザーのデータの更新処理を書く。たとえばデータベースへの接続を行い、UPDATE 文を書くなど。
    }
}

下記は UserService です。詳しくは解説しませんが、@Autowired というアノテーションをコンストラクタに付与すると、インスタンス生成済みの UserRepository が自動的に渡ってくるようになります。

package com.github.yuk1ty.bootdi.users.spring;

import com.github.yuk1ty.bootdi.users.User;
import com.github.yuk1ty.bootdi.users.spring.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    private final UserRepository userRepository;

    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void deactivate(String userId) {
        var user = this.userRepository.findUser(userId);
        user.ifPresent(u -> {
            var updated = new User(u.userId(), u.userName(), false);
            this.userRepository.updateUser(updated);
        });
    }
}

DI コンテナの恩恵は、それらのコンポーネントを使う側を見るとわかりやすいので UserService を使う側を確認してみましょう。UserController という適当なクラスを作ってみることにします。このコードからもわかるように、インスタンス生成に関する知識を一切コードに書くことなく裏で自動で解決済みのインスタンスが渡ってきます。あとはフィールドの userService をよしなに使用して処理を実装していくだけです。

package com.github.yuk1ty.bootdi.users.spring;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/users")
public class UserController {

    // もうここには DI コンテナで解決済みのインスタンスが来ている。
    private UserService userService;
}

実はこのコントローラー自体も裏で自動的に依存関係が解決された状態で生成済みとなるので、最終的にはメイン関数は下記のように Spring Boot のアプリケーションの起動に関する記述のみで済むようになります。

package com.github.yuk1ty.bootdi;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

若干黒魔術感があるといえばありますが、先ほどの DI コンテナを用いない例と比較すると一切インスタンス生成に関するコードを記述しなくて済んでいることわかります。これは DI コンテナが裏でそのような依存解決を自動で行っているためです。メリットとしては、たとえば UserService のコンストラクタの引数にさらに依存するリポジトリが増えた場合に、コンストラクタの引数とフィールドを追加するだけで済むようになることです。インスタンス生成のコードはどうしても芋づる式に修正が必要になりがちですが、DI コンテナがあればその芋づる式の修正を抑止できます。

ただし、DI コンテナは必ずしも利用する必要はないと筆者は考えています。『Dependency Injection Principles, Practices, and Patterns』では DI コンテナを使わない DI を「Pure DI」と呼んでいます。[*5]筆者の経験の限りでは、アプリケーションの規模が小さいうちは Pure DI を用いる傾向にあります。連載の後半で紹介しようと思っていますが、代わりに簡単な生成器を作って生成する行為そのものをそこにまとめておく傾向にあるように思います。

DI の形式

DI は最近ではほとんどのケースでコンストラクタインジェクションを利用するようになっているかと思われます。コンストラクタインジェクションは、先ほどサンプルコードで説明したやり方です。依存先のコンポーネントに利用したいコンポーネントをコンストラクタ経由で渡すことを言います。コンストラクタインジェクションは、インスタンス生成時にどのコンポーネントを利用するかを決定し、インスタンス生成後は変更を加えません。これにより、生成されたインスタンスをイミュータブルに保ちやすくなるというメリットを享受できます。

実はその他にもいくつか手法があります。Martin Fowler の元記事では「セッターインジェクション」と「インタフェースインジェクション」が紹介されています。先ほど紹介した書籍『Dependency Injection Principles, Practices, and Patterns』では、「プロパティインジェクション」なる手法も紹介されています。

その他にも筆者の知る限りでは、Scala 界隈で見かける Cake Pattern と呼ばれる技法や、関数型プログラミングの文脈で見かける Reader モナドを用いた DI などが思い浮かびます。これらは DI コンテナは用いないものの、少ない手間で注入されるコンポーネント間の依存関係をうまく解決できる手法です。

DI という手法自体はさまざまなプログラミング言語で表現可能です。DI コンテナを利用するかどうかはプログラミング言語に依るように思われます。

Dependency Inversion Principle (DIP)

DI と同時にペアで説明されることが多い概念(テクニック)として「Dependency Inversion Principle」というものがあります。この概念は DI として内包的に説明されている場合もあるようですが、今回は分けて考えます。[*6]

Dependency Inversion Principle とは何か

さて、ここまでは「Dependency Injection」について紹介してきました。紛らわしいですが、同じ「DI」という頭文字で「Dependency Inversion」と呼ばれるルールが存在します。

Dependency Inversion (Principle) は「依存性逆転の原則」と表現されます。DIP と略されることが多いかもしれません。DIP は DI の際にインタフェースを利用して依存コンポーネントを注入します。

先ほどの DI のコードでは、コンストラクタを経由して具体的なクラスを注入していました。たとえばこのクラスに、さらに「インメモリでキャッシュとして使いたい」という機能をもつ新しい亜種が誕生したとして、そのコンポーネントを注入したいとなったとします。すると、これまで具体的なクラスを利用してコンポーネントを注入していたため、コンストラクタの型を書き換える必要が出てきます。

// package com.github.yuk1ty.bootdi.users;
// 
// import java.util.Optional;
// 
// public class UserRepositoryOnMemory {
// 
//     public UserRepositoryOnMemory() {
//         // コンストラクタの内部処理を何か用意するかもしれない。
//     }
// 
//     public Optional<User> findUser(String userId) {
//         return Optional.empty();
//     }
// 
//     public void updateUser(User user) {
//         // ユーザーのデータの更新処理を書く。たとえばデータベースへの接続を行い、UPDATE 文を書くなど。
//     }
// }

package com.github.yuk1ty.bootdi.users;

public class UserService {

// 先ほどまでは下記を利用していた。
//     private final UserRepository userRepository;

    private final UserRepositoryOnMemory userRepository;

    public UserService(UserRepositoryOnMemory userRepository) {
        this.userRepository = userRepository;
    }

    public void deactivate(String userId) {
        var user = this.userRepository.findUser(userId);
        user.ifPresent(u -> {
            var updated = new User(u.userId(), u.userName(), false);
            this.userRepository.updateUser(updated);
        });
}

DIP は、このコンストラクタの型をインタフェースに差し替えます。インタフェースに差し替えることで、コンストラクタの型は変更の必要がなくなります。

まず、UserRepository というインターフェースを用意します。

package com.github.yuk1ty.bootdi.users;

import java.util.Optional;

public interface UserRepository {

    Optional<User> findUser(String userId);

    void updateUser(User user);
}

次に、UserRepositoryOnMemory を実装します。その際、UserRepository インタフェースを implements しておきます。

package com.github.yuk1ty.bootdi.users;

import java.util.Optional;

public class UserRepositoryOnMemory implements UserRepository {

    public UserRepositoryOnMemory() {
        // コンストラクタの内部処理を何か用意するかもしれない。
    }

    public Optional<User> findUser(String userId) {
        return Optional.empty();
    }

    public void updateUser(User user) {
        // ユーザーのデータの更新処理を書く。
        // ここの更新処理はオンメモリの情報(たとえばハッシュマップなど)の書き換えが想定される。
    }
}

これで UserRepositoryOnMemory 型は UserRepository 型の具象実装ということになります。これを利用すると、先ほどのように UserService がもつ UserRepository 型のシグネチャを逐一変更することなく、具象実装側のみを差し替えて対応するだけで済むようになります。その差し替え自体は使う側 UserService の外で行われるようになります。したがってより疎結合な実装になるというわけです。

package com.github.yuk1ty.bootdi.users;

public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void deactivate(String userId) {
        var user = this.userRepository.findUser(userId);
        user.ifPresent(u -> {
            var updated = new User(u.userId(), u.userName(), false);
            this.userRepository.updateUser(updated);
        });
    }
}

DI や DIP を導入するメリット・デメリットについて

メリット

さて、DI ならびに DIP を導入するメリットについてです。これは『Dependency Injection Principles, Practices, and Patterns』での議論がよく整理されており参考になるので援用したいと思います。

DI ならびに DIP には次のようなメリットがあると言われています。書籍内では DIP に関する議論と DI に関する議論が一緒くたにされてメリットが紹介されている点に注意は必要ですが、

  1. コードの再利用や拡張がしやすくなる。
  2. チームによる並列開発がしやすくなる。
  3. クラスの責務が明確になり、メンテナンス性が向上する。
  4. ユニットテストがしやすくなる。

といったメリットが挙げられます。

DI を利用することによりコンポーネント同士が責務できちんとわかれて疎結合になるため、たとえば同じようなコンポーネントを並列開発している際に diff の衝突が起きにくくなるなどのメリットがあります。また、インスタンスの具体的な実装は後から外部より注入することになるため、たとえばユニットテスト時によく利用されるモックを利用しやすくなるといった副次的なメリットもあります。

デメリット

まず DI では外部よりコンポーネントを注入するわけですが、そのコンポーネントを生成するにはさらに外部よりコンポーネントを注入する必要があるかもしれません。強いていうなら、コンポーネントインスタンス生成分だけ管理コストが膨らむことになります。DI コンテナを利用できる場合もありますが、DI コンテナを利用すればたとえば XML ファイルのような設定に関する管理コストも増えます。この辺りは DI 解決したい問題がどのくらい手軽かつ本質的に解決できるようになるかと、DI を導入することによる管理コスト増大分とのトレードオフかもしれません。[*7]

DIP についていえば、具象実装が1パターンしかない部分に対して DIP を適用するのは少々オーバーエンジニアリングかもしれません。具象実装が複数パターン考えられればメリットは大きいのですが、1パターンしかない場合、インターフェースの定義と具象実装の定義の2つが発生することになります。こうしたコンポーネントが大量に発生すると、単に管理コストが増大するばかりで抽象化によるメリットを享受しにくいです。

この言説に対するよくある反論としては、「モックの差し込みのために、実装本体の具象実装が1つしかなかったとしてもインタフェースを切るのだ」というものが考えられます。が、この主張の正当性はプログラミング言語の提供する言語機能に完全に依ります。具体的には、具象実装に対してリフレクションやマクロなどのいわゆるメタプログラミングの機構を利用して、外部から実装を差し込みできるようになっているプログラミング言語です。こうしたケースではそもそもインタフェースに相当する機能を用いて実装を分ける必要がなくなります。もちろん利用するプログラミング言語による話なのですべてに対応できる正解はありませんが、こうした機能の利用を検討することで無駄な DIP を避けられるようになります。要するに、モックという目的に対して DIP は必要条件ではないということです。

まとめと次回予告

今回の記事では、DI や DIP について簡単なコードを交えながら復習をしました。次回の記事では、では Rust を用いた DI としてはどのような実装手法が考えられうるのかという話題について議論していきます。

*1:ちなみに筆者は JavaScala などの JVM のエコシステムでの開発経験が長かった都合でこれらを題材とします。実際 DI という用語の最初の導入者である Martin Fowler も Java を使ってこの概念を説明する記事を世に出しているため、説明対象として Java を使用するのは間違いではないはずです。

*2:まず、Matz による Java の DI の検証が日本語記事で読める有用な議論だと思います。続いて、DHH も同様に DI は Ruby においては不要で、Ruby の範疇で解決できる手法を採れば十分なのではないかという提案をしています。筆者も、DI はどちらかというと静的型付き言語の都合をふんだんに含む手法で、すべてのプログラミング言語パラダイムにおいて必ずしも有効な手法ではないと考えています。

*3:https://en.wikipedia.org/wiki/Dependency_injection

*4:https://martinfowler.com/articles/injection.html

*5:https://www.manning.com/books/dependency-injection-principles-practices-patterns あるいは https://blog.ploeh.dk/2014/06/10/pure-di/

*6:この2つの概念の定義を明確にわけている原典があるわけではないので、これまでの DI の議論の歴史にきちんと沿っているか判断が難しいですが、Introduction to Dependency Injection 「DI」の整理とそのメリット - Speaker Deck の議論が個人的にはしっくりきたためです。今回はここから援用して、DI と DIP は別のものとして分けて考えてみることにしています。

*7:DI 以前は Singleton などを利用して管理していた歴史もあったようですが、マルチプロセスが主流な今の時代では Singleton はあまり意味をなさないというのもあり、DI はコンポーネント管理のデファクトになっていると考えられます。DI 以外のより優れた代替手段は筆者の知る限りでは今のところなく、おそらく導入しないという選択肢はほぼないかと考えています。