1. 概要
DI(Dependency Injection、依存性注入)はクラス間の結合度を下げ、可読性や保守性を向上できる仕組みですが、逆に開発体験が悪くなってしまう体験をし現在進行系でその改善に取り組んでいるので、なぜそのような経験をしたのかやどのような取り組みを行っているのかについてご紹介したいと思います。
2. 何が辛かったのか
今回記載する内容はFlutterおよびGetXというライブラリを使ったアプリ開発における話です。
GetXでは Get.put(SampleController())
と書くことでSampleControllerクラスのインスタンスをGetXの管理下に登録し、 Get.find<SampleController>()
と書くことで登録したインスタンスを取得できるようになります。
これがGetXにおける基本的なDIの実装方法になります。
次にどういった課題を抱えたのか具体的なコードを用いて説明したいと思います。
以下のコードは、画面に数値とボタンがあり、ボタンを押下すると数値がカウントアップされていくというシンプルな画面を実装したものです。
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桁ごとにカンマを入れます。
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. どのように解決したか
上記のような事象が頻発するようになったため具体的に以下のような実装に変更することにしました。
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でいうと以下のような形です。
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の仕組みを使って実装するということが目的になってしまっていた(手段先行になってしまっていた)ようにも思うので、今後より一層気をつけつつ引き続き改善を続けたいと思います。