watnowアドベントカレンダー2日目
僕は、flutterを使ってハッカソンに出てアプリを制作したり
バイトをしたりしている情報系2回、watnow所属の学生です。
といっても、flutter使用経験1年未満なので
まだまだ知識不足の初心者です。
加えて記事の投稿は今回が初めてです(笑)。
なので温かい気持ちで読んでいただけたら幸いです🙏
間違い、不適切な点等ありましたら
お手柔らかに指摘していただけたら助かります。
今回の記事の背景
もともとはriverpodで状態管理とDB処理の両方を行なっていた箇所を
リポジトリパターンやDIを用いてリファクタリングしました。
具体的には、状態管理、DB処理、リポジトリへの依存性を
抽象クラスを用いて分離しました!
今回はその内容に加えて勉強も兼ねながら
DDDについても簡単にまとめていきたいと思います。
DDD(ドメイン駆動設計)について
ドメインとは専門領域といったビジネスルールのことを指しています。
ビジネスルールは今回はアプリの規則、方針、制約、ロジック
などが該当します。
DDDはビジネスの内容を理解しそれをアプリの設計に
反映させるためのアプローチです。
以下はDDD設計のプロセスの例です
1.ドメインモデルリング
ドメインモデリングとは、アプリ内で使用される
ドメインの関係について明らかすることです。
こちらの記事にて詳しく書かれています!
2.異なるビジネス領域を分離
以下はファイル構成の例です。applicationフォルダを見てもらうと、
異なるビジネス領域ごとにファイルを設置し、そのファイル内で
notifierやusecaseを制作して分離していることがわかります。
lib
└── domain
├── application
│ ├── answer
│ │ └── notifier
│ ├── my_quest
│ │ └── notifier
│ ├── pick_image
│ │ └── notifier
│ ├── quest_list
│ │ ├── notifier
│ │ └── usecase
│ └── select_prefecture
│ ├── notifier
│ └── usecase
3.集約の設計
集約によってデータ同士にずれや矛盾、重複部分などがないようにしていきます。
こちらの記事の後半にてDDDの集約の設計手法について詳しく書かれています!
4.リポジトの使用
データ保持のため、リポジトリパターンを使用します。
次の段落で詳しく書いていきます。
5.ビジネスロジック(ドメイン層)とUI(アプリケーション層)を分離
ドメイン層とアプリケーション層の間の依存性を管理するために
依存性の注入(DI)パターンが使用されます。
リポジトリパターンの説明とメリットについて
以下別記事から抜粋したリポジトリパターンの説明です
(https://qiita.com/karayok/items/d7740ab2bd0adbab2e06)
リポジトリパターンとはビジネスロジックとデータ操作のロジックを分離し、
データ操作を抽象化したレイヤに任せるデザインパターンのことです。
リポジトリパターンでは、DBの操作や外部APIによるデータ取得等の
データソースへのアクセス部分は Repositoryインターフェースから
完全に隠蔽されます。そのためアプリケーションはデータソースがDBや
外部APIであるか意識することなく、データ操作を行うことができます。
加えて、repository内の記述を変更することで、
別のDBやAPIを取り扱った実装に切り替えやすいため、
依存性が下がるといったメリットもあります。
もともとのコード
今回取り扱っているのは相棒のモンスター選択機能のコードです。
下記のようにStateNotifier内の関数で、SupabaseClient取得して、
DBにも書き込んで、盛りだくさんな感じになっていました。
final monchoiceNotifierProvider =
StateNotifierProvider<MonchoiceNotifier, MonchoiceNotifierState>((ref) {
final client = ref.watch(supabaseClientProvider);
return MonchoiceNotifier(client,ref);
});
class MonchoiceNotifier extends StateNotifier<MonchoiceNotifierState> {
MonchoiceNotifier(this.client,this.ref)
: super(
MonchoiceNotifierState(
currentUserId: client.auth.currentUser?.id,
),
);
final SupabaseClient client;
final Ref ref;
Future<void> addMonster(int selectedPet) async {
final userId = client.auth.currentUser?.id;
try {
await client.from('monsters').insert({
'baseMonster': userId,
'experience': 0,
'monName': 'デフォルト',
});
} catch (e) {
debugPrint(e.toString());
}
}
リファクタリングしていきます
簡単に説明すると、先ほどのmon_choice_notifier.dartでの記述を
3つのファイルに分割しています。
api_repository.dartという抽象クラスを一旦挟んでから、
DBなどの具体的な処理内容はsupabase_api_repository_impl.dart
というサブクラスに記述しています。
そしてmon_choice_notifier.dartでは
抽象クラスで宣言した関数を呼び出す形で記述しています。
ディレクトリの構成は以下のようになっています。
lib
├── domain
│ ├── application
│ │ ├── notifier
│ │ │ └─mon_choice_notifier.dart
│ └── repositories
│ └─api_repository.dart
├── foundation
│ └── supabase_client_provider.dart
├── infrastructure
│ ├── data
│ │ └── supabase_api_repository_impl.dart
└── presentation
└── screen
└─ auth
└─ select_pet_screen.dart
それぞれのファイルを具体的に見ていきます。
- riverpod generatorを使い、notifier内ではapiRepositoryProviderのメソッドを呼び出して処理を書きます
@riverpod
class MonchoiceNotifier extends _$MonchoiceNotifier {
//モンスターのデータを取得する
@override
Future<MonChoiceData?> build() {
final repository = ref.read(apiRepositoryProvider);
return repository.getBaseMonster();
}
//notifier内では処理の内容を具体的には書かない
Future<void> addMonster(int selectedPet)
async {
try {
final repository = ref.read(apiRepositoryProvider);
await repository.addMonster(selectedPet);
} catch (e) {
debugPrint(e.toString());
}
}
}
- ApiRepositoryとして抽象クラス内に関数を記述します
final apiRepositoryProvider = Provider<ApiRepository>((ref) {
//SupabaseClientをDIする
final client = ref.read(supabaseClientProvider);
//ここを書き換えると、別の実装に差し替えられる
return SupabaseApiRepositoryImpl(client);
});
//抽象クラスを用意して具体的な処理は記述せずに関数を定義する
abstract class ApiRepository {
//選択したモンスターを登録する
Future<void> addMonster(int selectedPet);
}
- 先ほどの抽象クラスをインターフェイスとして実装します
class SupabaseApiRepositoryImpl implements ApiRepository {
SupabaseApiRepositoryImpl(this.supabaseClient);
final SupabaseClient supabaseClient;
//先ほど抽象クラスに記述した関数をoverrideして具体的なDB処理を記述
@override
Future<void> addMonster(int selectedPet) async {
final userId = supabaseClient.auth.currentUser?.id;
try {
await supabaseClient.from('monsters').insert({
'baseMonster': "$selectedPet",
'userId': userId,
'experience': 0,
'monName': 'デフォルト',
});
} catch (e) {
rethrow;
}
}
- 参考までに、notifierによって状態管理が実際に行われている箇所
//notifierで、選択したモンスターデータを更新
CustomButton(
text: '次へ',
onPressed: () async{
await ref
.read(monchoiceNotifierProvider.notifier)
.addMonster(selectedPet.value);
await context.pushRoute(const CompletionPetRoute());
},
),
記述の工程が増えて手間は増しますが、
ルールを理解すると、変更が加えやすくなります!!
この記事を読んでくれた方の役に立てれば嬉しいです。
今年を振り返って
今年の春からwatnowに参加して
たくさんの経験を得ることができました✨
flutterと出会ったことが大きかったです🎉
関西ビギナーズハッカソンvol2で「四徹」として最優秀賞をもらえたこと
キャラバンハッカソンで他大学の仲間と長期的な開発に携われたこと
実務アルバイトをスタートしたこと
watnow秋プロジェクトに参画していること
watnowの運営に加わったこと
などなど、flutterがたくさんの縁を繋いでくれて、確実に成長できました❗️
関わってくれた皆さん本当にお世話になりました。
本当にありがとうございました!!!!!!!!
来年からもよろしくお願いします〜
参考にさせていただいた記事