YOUTRUSTアプリを支えるデータ処理の技術

どうも、株式会社YOUTRUSTのアプリ開発のリードエンジニアをやっているashdikこと朝日(YOUTRUST / X)です。

僕は結構海外ドラマを観るのが好きでして。
「LOST」「24」「BOSCH」「THE MENTALIST」辺りが好きです。
そしてここ最近は「THE BLACKLIST」と言うのにハマっていて、
シーズン8の展開が面白すぎて興奮しております。
ちなみに、ご存知かもですが海外ドラマは一話あたりが40分ととても長いです。 それが20話くらい集まって1シーズンになります。

...シーズン8まで追いついてくれる人が一人でもいると嬉しいです笑

⚓️ 概要

第一部は、こちらの記事でYOUTRUSTアプリのレイヤー構成についてざっと説明しました。

tech.youtrust.co.jp

第二部は、レイヤー構成の記事に登場する図のうち、ApiClient 及び Request を説明しました。

tech.youtrust.co.jp

第三部は、レイヤー構成の記事に登場する図のうち、Store を説明しました。

tech.youtrust.co.jp

今回は、第四部と称して、 Facade について触れていこうと思います。

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 の責務は「データの整合性を保つこと」です。
また、基本的に FacadexxxProvider を直接触ることはなく全て 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 がシンプルになる

    • これにより、コードの読み手にも今何の処理を読んでいるのか分かりやすい
      • FriendRequestFacade は、つながりリクエストに関する Facadeなんだな という頭でコードリーディング出来る
        • メソッドも fetch , delete などシンプルにすることができる
      • FriendFacade は、つながりリクエスト以外のことも書いているので頭もコードもこんがらがりやすい
        • メソッドも fetchRequests , deleteRequests などと情報量が多くなる
  • 責務がはっきりしている

    • 呼び出す側のコードを見た時に、どのような概念に依存しているかが分かりやすい
      • friend_request_facade.dartimport しているということはつながりリクエストに関する処理をするファイルなんだろうな、など
    • 仮につながりリクエストの概念を消すとなった場合に FriendRequestFacade を消すだけでOK

という様なメリットがあるのかなと思っています。

🎥 最後に

いかがでしたでしょうか? YOUTRUSTアプリ、レイヤー構成シリーズもこれで第4弾。

だんだん、作り方がわかってきたのではないでしょうか?

残すは ViewModel と要望があれば Screen を紹介して終わりになるのかなと思います。

エンジニア募集中っ!

  • このシリーズを読んで、実際に触ってみたい!と思っていただけた方
  • 純粋にYOUTRUSTに興味がある方

YOUTRUSTでは、エンジニアを募集しています!
ぜひ、下のリンクよりご応募お待ちしておりまっす!

herp.careers