💡

Flutterで依存性の注入(DI)を実装する前提知識

2024/10/07に公開

はじめに

自分自身はWebエンジニア出身でFlutterでの開発経験はほぼ皆無なのですが、個人的に最近Flutterの勉強を始めています。

今回はFlutterにおいて依存性の注入(Dependency Injection)を実装する前に抑えておくべき基礎をまとめてみました。Flutterに限らない基礎を理解することを目的にしたので、Flutterにおける実装手法の整理はまた別の記事にしたいと思います。

依存性の注入(Dependency Injection)とは

コンピュータプログラムのデザインパターンの一つ。オブジェクトが依存する他のオブジェクトを外部から提供するようにする仕組みで、オブジェクト同士の依存関係が管理しやすくなり、コードの再利用性やテストのしやすさが向上します。

なぜ依存性の注入(Dependency Injection)が必要か

以前の記事で書いた通り、アプリが中規模〜大規模になってくると複数のレイヤーに関心を分離し、アプリを作っていく必要があります。
https://zenn.dev/masarufuruya/articles/dbf343822ef15b

そのようになった時に依存性の注入を行えるようなコードにしておくと、各レイヤー同士の関係性をコードで明確に表すことができるようになり、他のエンジニアがコードを読んだ時の可読性が上がります。

また同じオブジェクトを異なるコンテキストで再利用がしやすくなるのと、
依存関係をモックやスタブに置き換えることでテストを書くことも容易になります。

依存性注入の方法

依存性の注入は外部から依存関係のあるオブジェクトを渡すパターンであり、外部から渡す方法にはコンストラクタ・セッターメソッド・フィールドの3つがあります。また直接ではなくインタフェースを渡すケースもあります。

フィールド注入はProviderやRiverpodを使った方法の時に説明したいので今回は割愛しました。

  • コンストラクタ注入: 依存オブジェクトをコンストラクタの引数として渡す
  • セッター注入: セッターメソッドを通じて依存オブジェクトを注入
  • インターフェース注入: インターフェースを通じて依存オブジェクトを注入

コンストラクタ注入

DIの方法としては一番簡単で、「依存先のオブジェクトを自ら生成せずに、コンストラクタの引数としてもらう」ようにします。Dartで簡単なサンプルを書くと以下のようになります。わかりやすくコンストラクタやインスタンスの初期化は省略形を使わずに書いています。

class Logger {
  void log(String message) {
    print(message);
  }
}

class UserManager {
  Logger _logger;

  // コンストラクタ
  UserManager(Logger logger) {
    this._logger = logger;
  }

  void createUser(String name) {
    _logger.log('Creating user: $name');
  }
}

void main() {
  Logger logger = new Logger();
  // Loggerクラスの依存性を注入
  UserManager userManager = new UserManager(logger);
  userManager.createUser('Alice');
}

セッター注入

次はコンストラクタではなく、セッターメソッドを通じて注入する方法です。
セッター注入は、コンストラクタ注入に比べるとDIを行うためのメソッド呼び出しを忘れることによるバグの発生に繋がりやすいため、DIを強制することができるコンストラクタ注入の方がオススメです。

class Logger {
  void log(String message) {
    print(message);
  }
}

class UserManager {
  Logger? _logger;

  // セッターメソッド
  void setLogger(Logger logger) {
    this._logger = logger;
  }

  void createUser(String name) {
    if (_logger == null) {
      throw StateError('Logger has not been set');
    }
    _logger!.log('Creating user: $name');
  }
}

void main() {
  Logger logger = new Logger();
  UserManager userManager = new UserManager();

  // セッター注入
  userManager.setLogger(logger);

  userManager.createUser('Alice');
}

インターフェース注入

次は依存性を提供するためのインターフェースを定義し、そのインターフェースを実装するクラスを通じて依存性を注入する方法です。

インタフェースに依存するようになるので実装の仕方が変わった時の拡張がしやすくなります。
ただ追加のインターフェースが必要になるため、コードが若干複雑になります。小中規模なアプリケーションの場合は過剰な設計になりうるので、基本的には一番シンプルなコンストラクタ注入でDIを行うのがオススメになるかと思います。

// ロガーのインターフェース
abstract class LoggerInterface {
  void log(String message);
}

// ロガーの実装
class ConsoleLogger implements LoggerInterface {
  
  void log(String message) {
    print(message);
  }
}

// 依存性注入のためのインターフェース
abstract class LoggerInjector {
  void injectLogger(LoggerInterface logger);
}

class UserManager implements LoggerInjector {
  LoggerInterface? _logger;

  
  void injectLogger(LoggerInterface logger) {
    _logger = logger;
  }

  void createUser(String name) {
    if (_logger == null) {
      throw StateError('Logger has not been injected');
    }
    _logger!.log('Creating user: $name');
  }
}

void main() {
  LoggerInterface logger = ConsoleLogger();
  UserManager userManager = UserManager();

  // インターフェース注入
  userManager.injectLogger(logger);

  userManager.createUser('Alice');
}

さいごに

DI(Dependency Injection)はメンテナンスやテストをしやすくするために、オブジェクトが依存する他のオブジェクトを外部から提供するようにするというシンプルな考え方です。

DIの実装方法がフレームワークによって異なったり、DIコンテナという概念がここに加わってくるので、複雑に感じてしまうのかもしれません。

DIでやりたいことは別にそんなに大したことではないので、シンプルな設計になるように心がけたいですね。Flutterにおける実際のDIの実装方法はまた別の記事で書いていきたいと思います。

Discussion