【Flutter】Injectableを使ってみた
Flutterを触っていて、DI用のフレームワークは何があるか調べていたところ、公式ページでは以下のように記載されていた。
Does Flutter come with a dependency injection framework? We don’t ship with an opinionated solution, but there are a variety of packages that offer dependency injection and service location, such as injectable, get_it, and kiwi.
Flutterには付属していないみたいですね。 紹介されているライブラリのStar数を見てみたところ、get_itが一番ポピュラーなのかなという印象を持ちつつも、Injectableが新しいかつコミットも盛んに行われていたため、今回はInjectableを触ってみることにした。
InjectableのGitHubでは以下のように説明されている。
Code Generator for get_it
なるほど...。
Get_itは以下のように説明されています。
Get It - Simple direct Service Locator that allows to decouple the interface from a concrete implementation and to access the concrete implementation from everywhere in your App
DIフレームワークではなく、サービスロケーターみたいですね。
DIとService Locaterパターンの違いについてはこちらを参照すると良さげ。
とりあえず触ってみようと思います。
準備
まずはFlutterプロジェクトを作成
flutter create injectable_sample
そしてpubspec.yamlに以下を追記する。 当たり前ですが、get_itも必要ですね。
dependencies: injectable: "^1.2.0" get_it: "^6.0.0" dev_dependencies: build_runner: "^1.11.5" injectable_generator: "^1.2.0"
ファイルを作成(今回はinjector.dartとした)、トップレベルの関数を定義し、@injectableInit
アノテーションをつける。
そして$initGetit
関数にGetItのインスタンスを渡す。
($initGetIt
関数はInjectableが自動生成してくれる関数です。この関数名は@InjectableInit
に引数initilizerName
に文字列を渡すことで、変更することが可能です)
injector.dart
import 'package:get_it/get_it.dart'; import 'package:injectable/injectable.dart'; final getIt = GetIt.instance; @InjectableInit() void configureDependencies() => $initGetIt(getIt);
このままでは$initGetIt
関数が存在せず、コンパイルエラーになってしまうので、以下のコマンドを実行し、ファイルを生成する。
flutter packages pub run build_runner build
@InjectableInit()
を付与した関数と同じ階層上に、自動生成のファイルが作成されます。
こやつがサービスロケータっぽいですね。
injector.config.dart
// GENERATED CODE - DO NOT MODIFY BY HAND // ************************************************************************** // InjectableConfigGenerator // ************************************************************************** import 'package:get_it/get_it.dart' as _i1; import 'package:injectable/injectable.dart' as _i2; // ignore_for_file: unnecessary_lambdas // ignore_for_file: lines_longer_than_80_chars /// initializes the registration of provided dependencies inside of [GetIt] _i1.GetIt $initGetIt(_i1.GetIt get, {String? environment, _i2.EnvironmentFilter? environmentFilter}) { final gh = _i2.GetItHelper(get, environment, environmentFilter); return get; }M
main関数で先ほど定義して関数を呼び出す。
main.dart
void main() {
configureDependencies();
runApp(MyApp());
}
次に抽象クラスIRepositoryを定義し、Repositoryクラスを作成します。
Repositoryクラスに@Injectable
を指定し、引数asにIRepositoryを指定します。
import 'package:injectable/injectable.dart'; abstract class IRepository { String find(); } @Injectable(as: IRepository) class Repository extends IRepository { @override String find() => "repository"; }
ここでまたbuildを実行し、ファイルを自動生成します。
flutter packages pub run build_runner build
自動生成されたファイルの中身が以下のように変わっており、Repositoryオブジェクトが登録されているのが分かります。
// GENERATED CODE - DO NOT MODIFY BY HAND // ************************************************************************** // InjectableConfigGenerator // ************************************************************************** import 'package:get_it/get_it.dart' as _i1; import 'package:injectable/injectable.dart' as _i2; import 'repository.dart' as _i3; // ignore_for_file: unnecessary_lambdas // ignore_for_file: lines_longer_than_80_chars /// initializes the registration of provided dependencies inside of [GetIt] _i1.GetIt $initGetIt(_i1.GetIt get, {String? environment, _i2.EnvironmentFilter? environmentFilter}) { final gh = _i2.GetItHelper(get, environment, environmentFilter); gh.factory<_i3.IRepository>(() => _i3.Repository()); return get; }
main.dart関数から呼び出してみる。
main.dart
void main() { configureDependencies(); runApp(MyApp()); } class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { final text = getIt.get<IRepository>().find(); return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: Scaffold(body: Center(child: Text(text),)) ); } }
Repositoryと表示されています。 良さそうですね。
環境毎に利用するオブジェクトを変化させる
環境毎に、利用するオブジェクトを変えることをやってみたいと思います。 今回はdev環境の時に、MockRepositoryを利用するようにしましょう。
まずは、最初に作成したinjector.dartで定義した関数で環境名を受け取れるようにし、$initGetIt
のenvironmentに渡します。
injector.dart
import 'package:get_it/get_it.dart'; import 'package:injectable/injectable.dart'; import 'injector.config.dart'; final getIt = GetIt.instance; @InjectableInit() void configureDependencies(String env) => $initGetIt(getIt, environment: env);
先ほど作成したRepositoryには@prod
を、 MockRepositoryに@dev
を指定します。
(@prod
と@dev
はInjectableが用意してくれているアノテーションです。オリジナルの環境を定義してアノテーションで指定することももちろん可能です。)
repository.dart
import 'package:injectable/injectable.dart'; abstract class IRepository { String find(); } @prod @Injectable(as: IRepository) class Repository extends IRepository { @override String find() => "repository"; } @dev @Injectable(as: IRepository) class MockRepository extends IRepository { @override String find() => "mockRepository"; }
buildを実行し、ファイルを自動生成。
flutter packages pub run build_runner build
環境毎に利用するRepositoryが指定されているのが分かります。
injector.config.dart
// GENERATED CODE - DO NOT MODIFY BY HAND // ************************************************************************** // InjectableConfigGenerator // ************************************************************************** import 'package:get_it/get_it.dart' as _i1; import 'package:injectable/injectable.dart' as _i2; import 'repository.dart' as _i3; const String _prod = 'prod'; const String _dev = 'dev'; // ignore_for_file: unnecessary_lambdas // ignore_for_file: lines_longer_than_80_chars /// initializes the registration of provided dependencies inside of [GetIt] _i1.GetIt $initGetIt(_i1.GetIt get, {String? environment, _i2.EnvironmentFilter? environmentFilter}) { final gh = _i2.GetItHelper(get, environment, environmentFilter); gh.factory<_i3.IRepository>(() => _i3.Repository(), registerFor: {_prod}); gh.factory<_i3.IRepository>(() => _i3.MockRepository(), registerFor: {_dev}); return get; }
main関数で呼び出していた関数に、環境情報を渡します。
main.dart
void main() {
configureDependencies(Environment.dev);
runApp(MyApp());
}
これで画面を見てみると、mockRepositoryと表示されています。うまくできたようです。
Injectableを今回使ってみましたが、一先ず少ない記述量で利用するオブジェクトのを定義できるので便利だなと感じた(小並感)。 この記事を書いていて、Service LocatorパターンとDIが自分の中で混ざってしまっていると感じていたので、整理せねばと思いました
擬似クラスの「:first-child」と「:first-of-type」について
何かと便利な擬似クラス。
その中でも:first-child
は多用しまくると思うが、たまにスタイルが当たらず、一瞬「あ、、、え、、、」となるので自分の戒めのために書き記しておく。
「あ、、、え、、、」となるのは以下のような時である。
各div要素の一番最初のpタグにのみstyleを当てたい場合、 p:first-child
で行けそうな気がするが実際には当たらない。
<div> <p>選択される</p> <p>選択されない</p> </div> <div> <h5>選択されない</h5> <p>選択されない</p> </div>
p:first-child { color: red; }
解決するには:first-of-type
を利用する。
<div> <p>選択される</p> <p>選択されない</p> </div> <div> <h5>選択されない</h5> <p>選択される</p> </div>
p:first-of-type { color: red; }
MDN Web Docsでは、
兄弟要素のグループの中で最初の要素を表します。
兄弟要素のグループの中でその種類の最初の要素を表します。
と記載されているが、以上の挙動から、
p:first-child
は「兄弟要素の中でpタグが最初である場合」
p:first-of-type
は「兄弟要素の中の最初のpタグに対して」
と言い換えることができるのではと思います。
Selenideのドキュメントを読んでみて
状態を確認する
要素の状態を確認するメソッド。要素がその状態になってくれるまで待ってくれる(デフォルト4秒)ので、非同期で変化する要素も良い感じに確認できる。
`$`("target").should(exist) `$`("target").shouldNot(exist) `$`("target").shouldBe(visible) `$`("target").shouldNotBe(visible) `$`("target").shouldHave(text("text")) `$`("target").shouldNotHave(text("text"))
上記のメソッドは振る舞いとしては同じであり、Selenideの公式サイトでは以下のように説明されており、英語の文章として好ましいものを選択すると良さげ。
We recommend to choose the convenient alias so the line of code can be easily read like a common english phrase
https://selenide.org/documentation.html
existとvisible
existは要素が存在しているか、visibleは要素が表示されているかを確認できる。
存在しているかの確認なので、styleでvisibility: hidden
や、display: none
が指定されていてもexistは通過する。(visibleは通過しない)
visibility: hidden
<div class="target" style="visibility: hidden;">Hidden</div>
`$`(".target").should(exist) // pass `$`(".target").shouldBe(visible) // not pass
display: none
<div class="target" style="display: none;">None</div>
`$`(".target").should(exist) // pass `$`(".target").shouldBe(visible) // not pass
imgの確認
Condition.image
はimg要素が画像のロードに成功しているかを見てくれる。
<img class="exist-image" src="dog.png"/> <img class="non-exist-image" src="non-image"/>
`$`(".exist-image").shouldBe(image) // pass `$`(".non-exist-image").shouldBe(image) // not pass
styleの確認
`$`("target").shouldHave(cssValue("color","red"))
textとexactText
Condition.text()
は文字列の部分一致、Condition.exactText()
は文字列の完全一致を見てくれる。
基本はexactTextの方を利用すると良さそう。
<p>ログインに成功しました</p>
`$`("p").shouldHave(text("ログイン")) // pass `$`("p").shouldHave(exactText("ログインに成功しました")) // pass `$`("p").shouldHave(exactText("ログイン")) // not pass
複数の要素を一度にテストする
<ul> <li>aaa</li> <li>bbb</li> <li>ccc</li> </ul>
`$$`("ul li").shouldHave(texts("aaa", "bbb", "ccc"))
By セレクター
By
を利用すると、cssSelectorでなく、 HTML要素の文字列や、name、id等で検索することができる。
`$`(byText("送信")).click() `$`(byName("password")).setValue("inputPassword")
ドラッグアンドドロップ
`$`("from").dragAndDropTo("to")
ドメイン駆動設計のインターホンを押した
最近思うこと
DDD、ドメイン駆動設計。
カンファレンスのセッションや、書籍等でドメイン駆動設計をテーマにしているものが、数年前と比べて明らかに増えてきていると最近感じます。
Twitterのタイムラインにも毎日のように「DDD」という文字が流れてきます。
DDDのセッションを何回か聞いたことがあるのですが、
私の理解は「業務に詳しい人と協力し合いながら、現実世界をソースコードに反映させていく開発手法」という非常にふわふわとした理解でした。
そんなふわふわな自分に不安を覚え、エリック・エヴァンスのドメイン駆動開発(以下 DDD本)と実践ドメイン駆動設計(以下 IDDD本)を手に取ることにしました。
ドメイン駆動設計
ドメイン駆動設計とは何なのか。
DDD本の冒頭で以下のように述べられています。
先進的なソフトウェア開発者は、少なくともここ20年の間、ドメインモデリングと設計が重大なテーマであると認識してきた。 しかし、何をする必要があって、それをどのようにすべきかについては、驚くほどわずかしか書かれていない。 だが、明確に体系化されてはいないものの、オブジェクトコミュニティの底流として、ある哲学が出現してきている。 私はその哲学を、「ドメイン駆動設計」と呼んでいる
どうやら、ドメインモデリングという言葉が重要なキーワードという匂いがします。
IDDD本では以下のように述べられています。
DDDと呼ばれるソフトウェア開発手法がある。より高品質のソフトウェアモデルを設計する手助けとなる手法だ。 設計した結果が、そのソフトウェアの動作を明確に表すようにできる。 DDDでは、ドメインエキスパートとソフトウェア開発者が力を合わせて、業務のエキスパートのメンタルモデルを反映したソフトウェアを開発する。 これは決して「現実世界」をそのままモデリングすることではない。現状をそのままモデリングするのではなく、業務により役立つモデルを作る。
こちらでも「モデリング」というワードが出てきており、DDD本と同様に重要なキーワードという匂いがします。
ここで、何となく意味はわかるんだけど、具体的に説明してと言われると息苦しくなる「ドメイン」と「モデリング」という言葉について整理しようと思います。
ドメイン
ドメインという言葉を単純に調べてみると、「範囲、領域」という意味が出てきます。
DDD本では
すベてのソフトウェアプログラムは、それを使用するユーザの何らかの活動や関心と関係がある。ユーザがプログラムを適用することの対象領域が、ソフトウェアのでメインである。
IDDD本では
ドメインとは、広い意味でいうと、組織が行う事業やそれを取り巻く世界のことだ。
と、説明されている。
上記の説明から、ドメイン駆動設計におけるドメインとは「ソフトウェアが解決しようとしている問題領域」かなと理解しました。
モデリング
モデリングとはなんでしょう。
我らがwikipedia様を見てみました。
モデリング(英: modeling)は、広義の意味での模型(モデル)を組み立てる事を言う
モデルを作成することと一先ず理解することにしました。
では、モデルとはなんなのでしょうか。。。
調べてみると、
問題とする事象(対象や諸関係)を模倣し、類比・単純化したもの。また、事象の構造を抽象して論理的に形式化したもの。
DDD本を読んでみると、
選び抜かれてシンプルにされ、意図的に組み立てられた知識の表現形式
('・_・`)
なんだか難しい表現が並んでいると感じました。
何かしっくり来ないな〜、と思いながら調べていると「DDDのモデリングとは何なのか、 そしてどうコードに落とすのか」資料 / Q&A - little hands' labというスライドに出会いました。
こちらに記載されていた、
問題解決のために、物事の特定の側面を抽象化したもの
という表現が私には一番しっくりきた表現でした。
抽象化でググってみると
「思考における手法のひとつで、対象から注目すべき要素を重点的に抜き出して他は捨て去る方法である。」
と出てきます。
私の今の所の理解では「モデルとは」と問われた時の回答は
問題解決のために、物事の側面で必要な物だけを抜き出したもの
となりました。
モデルが私の中で上記の表現になったため、ドメインモデルは
ソフトウェアが解決しようしている問題領域内の物事の側面で、必要な物だけを抜き出したもの
といったことになるでしょうか。
DDD本やIDDD本ではドメインエキスパートと呼ばれる、ドメインについて一番詳しい人と話を重ねあいながらドメインモデルを作り上げる必要があると述べられています。
ドメインモデルを共に作成し、それを実装に反映させていく。
ドメインモデルと実装を密に結びつけることが重要になります。
ドメインモデルが変更されたら実装が変わる。
この辺りから、私が当初ざっくり理解していたことがなんとなくですが、鮮明になってきたような気がします。
ドメインモデル
では、ドメインモデルを表現するにはどうすれば良いでしょうか。
DDD本ではドメインモデルを表現する3パターンが紹介されています。(IDDD本では、戦術的モデリングツールと題して紹介されている)
- エンティティ
- 値オブジェクト
- サービス
ドメインモデルを表現するパターンを明確に区分けすることで、ドメインモデルの意味が鮮明になり、実際に実装できるドメインモデルが作成できるようになります。
それでは、それぞれのパターンを見てみたいと思います。
エンティティ
同一性によって定義されるオブジェクトはエンティティと呼ばれます。
同一性の定義を調べてみました。
Aが異なった状況においても常に同じものであり,同じものとして認められるとき,A は自己自身と同一である。このとき A=A において同一性が成立しているという
よく同一性の例で目にするのが人間や会社かなと思います。
5歳の人間が25歳になっても、別の個人としては扱われません。身長が伸びても別の個人としては扱われません。 つまり、姿や形が変わっても、変化前と同じであるため、同一性によって定義されるオブジェクトと言えるでしょう。
エンティティは各オブジェクトを識別する手段を定義する必要があり、識別するための属性は一意なものでなければいけません。ここが値オブジェクトとは異なる点です。
値オブジェクト
同一性を持つものはエンティティと呼ぶと述べましたが、同一性を持たないオブジェクトは値オブジェクトと呼ばれます。 そのオブジェクトが何であるかが重要であり、どれなのか、誰なのかは問わないオブジェクトです。
ある概念が値であるかどうかを判断するときには、その概念が以下の特性を持っているかどうかを見極める必要があるとIDDD本では述べられています。
サービス
ドメインにおける重要なプロセスや変換処理がエンティティや値オブジェクトの自然な責務ではない場合、その操作はサービスとして定義します。
集合に対する優先度の判定、パスワードの暗号化、オブジェクトの重複確認等、エンティティや値オブジェクトに振る舞いとして持たせてしまうと不自然になってしまうことがあります。
不自然な振る舞いをエンティティや値オブジェクトに持たせてしまうと、オブジェクトの概念が明確でなくなってしまいます。
では、サービスとしてエンティティや値オブジェクトに振る舞いを持たせるべきでないと判断する条件はあるでしょうか。
IDDD本では以下の3つが挙げられています。
まとめ
まだまだDDDの入り口の門を開けたところであり、抜け落ちている所ばかりだと思いますが、記事を書く前の自分よりかはDDDに対しての理解が進んだと思うことにします。
淡々とドメインモデルを表現するパターンを書いてしまったので、次はコードを交えて記事を書きたいな...