DIをベースにしたアプリ開発でDIの使い方に苦労した話

# 1. 概要
DI（Dependency Injection、依存性注入）はクラス間の結合度を下げ、可読性や保守性を向上できる仕組みですが、逆に開発体験が悪くなってしまう体験をし現在進行系でその改善に取り組んでいるので、なぜそのような経験をしたのかやどのような取り組みを行っているのかについてご紹介したいと思います。

# 2. 何が辛かったのか
今回記載する内容はFlutterおよびGetXというライブラリを使ったアプリ開発における話です。
GetXでは `Get.put(SampleController())` と書くことでSampleControllerクラスのインスタンスをGetXの管理下に登録し、 `Get.find<SampleController>()` と書くことで登録したインスタンスを取得できるようになります。
これがGetXにおける基本的なDIの実装方法になります。

次にどういった課題を抱えたのか具体的なコードを用いて説明したいと思います。
以下のコードは、画面に数値とボタンがあり、ボタンを押下すると数値がカウントアップされていくというシンプルな画面を実装したものです。

```dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';

class SampleController {
  final RxInt count = 0.obs;
  void onPressedButton() => count.value = count.value + 1;
}

class SamplePage extends StatelessWidget {
  const SamplePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Sample Page'),
      ),
      body: Center(
        child: Column(
          children: [
            Text(Get.find<SampleController>().count.value.toString()),
            ElevatedButton(
              onPressed: Get.find<SampleController>().onPressedButton,
              child: const Text('Count Up'),
            ),
          ],
        ),
      ),
    );
  }
}

```

数値や数値のカウントアップの処理をSampleControllerが担っており、 `Get.find` でクラスのインスタンスを取得して使っています。そのため、SamplePageを表示する前に `Get.put` でSampleControllerのインスタンスを登録しておく必要があります。

次に、数値がただの数値ではなく通貨の円でかつ、ボタンを押下すると1000ずつカウントアップされるようになったとします。通貨の円なので3桁ごとにカンマを入れます。
```dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';

class NumberHelper {
  String formatNumToJpYen(int num) {
    final NumberFormat formatter = NumberFormat('#,###', 'ja_JP');
    return formatter.format(num);
  }
}

class SampleController {
  final RxInt price = 0.obs;
  String get formattedPrice {
    final formatted = Get.find<NumberHelper>().formatNumToJpYen(price.value);
    return formatted;
  }

  void onPressedButton() => price.value = price.value + 1000;
}

class SampleWidget extends StatelessWidget {
  const SampleWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Sample Page'),
      ),
      body: Center(
        child: Column(
          children: [
            Text(Get.find<SampleController>().formattedPrice),
            ElevatedButton(
              onPressed: Get.find<SampleController>().onPressedButton,
              child: const Text('Count Up'),
            ),
          ],
        ),
      ),
    );
  }
}
```

> 通貨の円なので3桁ごとにカンマを入れます。

この処理はSampleWidgetでのみ使われる処理ではないので、NumberHelperという共通クラスのメソッドとして定義しました。上のように実装することでSampleWidgetはSampleControllerに、SampleControllerはNumberHelperに依存する形になります。
これがまさに苦労したポイントで、 **「SampleWidgetを使用するのにSampleControllerおよびNumberHelperのインスタンスを依存管理下に登録しておく必要がある(Get.putする必要がある)」** ということが、SampleWidgetをぱっと見ただけでは分からないのです。これによって `You need to call Get.put(XX) or Get.lazyPut(XX)` というエラーを何度も見る羽目になりました。。

# 3. どのように解決したか
上記のような事象が頻発するようになったため具体的に以下のような実装に変更することにしました。
```dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';

class NumberHelper {
  String formatNumToJpYen(int num) {
    final NumberFormat formatter = NumberFormat('#,###', 'ja_JP');
    return formatter.format(num);
  }
}

class SampleController {
  SampleController(this.numberHelper);

  final NumberHelper numberHelper;
  final RxInt price = 0.obs;

  String get formattedPrice {
    final formatted = numberHelper.formatNumToJpYen(price.value);
    return formatted;
  }

  void onPressedButton() => price.value = price.value + 1000;
}

class SampleWidget extends StatelessWidget {
  const SampleWidget({
    required this.controller,
    super.key,
  });

  final SampleController controller;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Sample Page'),
      ),
      body: Center(
        child: Column(
          children: [
            Text(controller.formattedPrice),
            ElevatedButton(
              onPressed: controller.onPressedButton,
              child: const Text('Count Up'),
            ),
          ],
        ),
      ),
    );
  }
}
```

クラスの内部で `Get.find()` するのをやめて、代わりにコンストラクタの引数でインスタンスを渡す形に変更しています。
このようにすることで、事前に `Get.put` で依存管理下にインスタンスを登録する必要がなくなり、クラスを作成する際にそのクラスがどのクラスに依存しているのかわかりやすくなりました（コンストラクタでインスタンスを渡す必要があるため）。
また、そもそもHelperクラスのようなインスタンスを作成する必要がないクラス（状態を持たないようなクラス）については、静的なメソッドで構成するように変更し、そもそも依存管理をしなくて済むように変更しました。
例で示したNumberHelperでいうと以下のような形です。
```dart
class NumberHelper {
  static String formatNumToJpYen(int num) {
    final NumberFormat formatter = NumberFormat('#,###', 'ja_JP');
    return formatter.format(num);
  }
}

class SampleController {
  final RxInt price = 0.obs;
  String get formattedPrice {
    final formatted = NumberHelper.formatNumToJpYen(price.value);
    return formatted;
  }
}
```

# 4. まとめ
DIはクラス同士の結合度を下げるための仕組みですが、そもそも依存関係が把握しづらい状態にあると逆に実装がし辛くなってしまうということを学びました。また、便利な仕組みであるが故にどうして使う必要があるのかといった目的意識のところが曖昧になってしまっていたようにも思います。
まだ改善中にはなるので今回の方針変更がベストかはまだわかっていませんが、気づかぬ内にDIの仕組みを使って実装するということが目的になってしまっていた（手段先行になってしまっていた）ようにも思うので、今後より一層気をつけつつ引き続き改善を続けたいと思います。