どうも、株式会社YOUTRUSTのアプリ開発のリードエンジニアをやっているashdikこと朝日(YOUTRUST / X)です。
僕は結構海外ドラマを観るのが好きでして。
「LOST」「24」「BOSCH」「THE MENTALIST」辺りが好きです。
そしてここ最近は「THE BLACKLIST」と言うのにハマっていて、
シーズン8の展開が面白すぎて興奮しております。
ちなみに、ご存知かもですが海外ドラマは一話あたりが40分ととても長いです。
それが20話くらい集まって1シーズンになります。
...シーズン8まで追いついてくれる人が一人でもいると嬉しいです笑
⚓️ 概要
第一部は、こちらの記事でYOUTRUSTアプリのレイヤー構成についてざっと説明しました。
第二部は、レイヤー構成の記事に登場する図のうち、ApiClient
及び Request
を説明しました。
第三部は、レイヤー構成の記事に登場する図のうち、Store
を説明しました。
今回は、第四部と称して、 Facade
について触れていこうと思います。
💻 筆者環境 (執筆時)
name | version |
---|---|
Flutter | 3.10.6 |
Dart | 3.0.6 |
flutter_hooks | 0.20.0 |
hooks_riverpod | 2.3.6 |
📓 大前提(お詫び)
(前回に引き続き)
riverpod
黎明期の頃から使っており、 FutureProvider
は使われておらず StateNotfierProvider
も最近導入し始めたところです。
その辺の構成の参考にしようとしている方には、もしかしたらお役に立てないかもしれません。
ですが、2年半くらい今の構成で作り続けており、非常に作りやすいと感じています。
また、新しくジョインしていただいた方々にもコードが読みやすいと好評いただいているので
一つのパターンとしてありなのではないかなと個人的には思っています。
それではそんな言い訳をしたところで笑、本編の解説に入りたいと思います。
Facade
弊社のレイヤー構成における、Facade
の責務は「データの整合性を保つこと」です。
また、基本的に Facade
が xxxProvider
を直接触ることはなく全て Store
を介します。
概要
僕が Facade
について説明する時に良く使う例が商品の購入です。
商品を購入すると、以下のことが起こります。
- 所持金の低下
- 所持アイテムの増加
- 店側在庫の減少
これを必要最低限のインタフェースで公開し、実行するのが Facade
になります。
利用者側目線で言えば、Facade
を呼べばデータ周りのあれこれを意識せずよしなにしてくれるというわけです。
言い方を変えると、データ周りの不整合が起きた場合は Facade
の責任となります。
実装イメージ
※概要を掴んでいただくためのものなのでエラーハンドリングなどは除きます。
class ShopFacade { static Future<void> purchase({ requried Reader read, required String productId, requried int amount, }) async { // 購入リクエストの送信 ... // 所持金の低下 WalletStore.reduce( read: read, price: price, ); // 所持アイテムの増加 ItemStore.add( read: read, productId: productId, amount: amount, ); // 店側在庫の減少 InventoryStore.decrease( read: read, productId: productId, amount: amount, ); } }
大枠イメージいただけましたでしょうか?
Todoでの例
実装
class TodoFacade { static Future<void> fetchAll({ required Reader read, }) async { final allTodos = await read(apiClientLocator).sendRequest( TodoListFetchRequest(), ); TodoStore.replace( read: read, todos: allTodos, ); } }
利用側
await TodoFacade.fetchAll( read: ref.read, });
まだ、Reader
使ってんのかよという指摘に関してはもっともなので、謹んでお受けいたします笑
これにより、前回説明した _todosStateProvider
が更新され、それを watch
しているUI側も更新されるという流れです。
フィルタをセットしてみる
今度は、良くあるTodoのフィルタをセットしてみましょう。
Storeなど
enum TodoFilterType { all, completed, incomplete, expired, } final _filterStateProvider = StateProvider( (ref) => TodoFilterType.all, ); final filteredTodoProvider = Provider( (ref) { final filter = ref.watch(_filterStateProvider); final allTodos = ref.watch(_todosStateProvider); return switch (filter) { TodoFilterType.all => allTodos, TodoFilterType.completed => [...allTodos.map((e) => e.isCompleted)], TodoFilterType.incomplete => [...allTodos.map((e) => !e.isCompleted)], TodoFilterType.expired => [...allTodos.map((e) => e.isExpired)], }; }, ); class TodoStore { ... static void setFilter({ required Reader read, required TodoFilterType filterType, }) { read(_filterStateProvider.notifier).update((_) => filterType); } }
class TodoFacade { ... static void setFilter({ required Reader read, required TodoFilterType filterType, }) { TodoStore.setFilter(read: read, filterType: filterType); } }
Store
に依存するのは基本的には Facade
だけなので、
こんな感じでただ受け渡すだけの時もあります。
これにより、filteredTodoProvider
はフィルタされた結果を返すようになります。
粒度
最後に、Facade
の粒度について書いていこうと思います。
僕自身のイメージとしては、一つの Facade
にごちゃごちゃっとまとめるというよりは
割と細かめに作るのが良いように思っています。
つながりリクエスト一覧を取得する、という処理を書く際に
FriendFacade.fetchRequests
vs FriendRequestFacade.fetch
が思い浮かぶのですが後者で作るイメージです。
各種
Facade
がシンプルになる責務がはっきりしている
という様なメリットがあるのかなと思っています。
🎥 最後に
いかがでしたでしょうか? YOUTRUSTアプリ、レイヤー構成シリーズもこれで第4弾。
だんだん、作り方がわかってきたのではないでしょうか?
残すは ViewModel
と要望があれば Screen
を紹介して終わりになるのかなと思います。
エンジニア募集中っ!
- このシリーズを読んで、実際に触ってみたい!と思っていただけた方
- 純粋にYOUTRUSTに興味がある方
YOUTRUSTでは、エンジニアを募集しています!
ぜひ、下のリンクよりご応募お待ちしておりまっす!