🌏
Dart3でのResult型について
はじめに
例外処理についてDartではtry-catch
を使用します。
基本的にどの言語も、try-catch
を用いることが多いように感じますが、
SwiftやKotlinを使用した人には馴染み深いResult型が存在します。
Result型は例外処理を明示的/宣言的に定義できるため
用途によってはより堅牢な例外処理となり得ます。
Dartでも、以前からabstractを用いたResult型の定義などは存在していましたが、
今回はDart3から登場したsealed class
を用いたResult型の定義と、使用用途について記載していきます。
概要
Resultの定義
result.dart
/// sealed classに準拠したResultクラスを生成
sealed class Result<S, E extends Exception> {
const Result();
}
/// Resultクラスに準拠したSuccessクラス
final class Success<S, E extends Exception> extends Result<S, E> {
const Success(this.value);
final S value;
}
/// Resultクラスに準拠したFailureクラス
final class Failure<S, E extends Exception> extends Result<S, E> {
const Failure(this.exception);
final E exception;
}
説明
- まず、
sealed class
を用いて、Result側を定義- genericには、dynamicなSuccess / Exceptionに準拠したFailureを指定しています。
-
Success class
では、genericで指定している、第一引数の値をconstructorとして指定 -
Failure class
では、genericで指定している、第二引数の値をconstructorとして指定
Model
Code
test.dart
class Test with _$Test {
factory Test({
required int id,
required String name,
required int age,
}) = _Test;
factory Test.failure() => Test(
id: 0,
name: '',
age: 0,
);
Client
Code
rest_client.dart
(baseUrl: "https://xxx/test")
abstract class RestClient {
factory RestClient(Dio dio, {String baseUrl}) = _RestClient;
("/test")
Future<Test> getTest();
}
Repository
repository.dart
Future<Result<Test, Exception>> getTest() async {
final dio = Dio(); // Provide a dio instance
final client = RestClient(dio);
try {
final response = await client.getTasks().then((it) => logger.i(it));
return Success(response);
} on Exception catch (e) {
return Failure(e);
}
}
説明
- Result型を使用するためには、例外の発生源を
try-catch
で処理する必要があります。- 基本的には、外部通信などを実施する低レイヤー部分で
try-catch
を実施する想定です。 - architectureに応じて
try-catch
を実施する箇所は選定して下さい
- 基本的には、外部通信などを実施する低レイヤー部分で
- 例外が発生しない場合は、
Success class
を用いて、responseを返却 - 例外が発生した場合は、
Failure class
を用いてExceptionを返却- 内容によっては、様々な例外が発生するため、ハンドリングに関してはプロジェクトごとに書き換えてください
Notifier
notifier.dart
Future<void> fetchTest() async {
final result = await repository.getTest();
final value = switch (result) {
Success(value: final value) => value,
Failure() => Test.failure(),
};
state = state.copyWith(test: value);
}
説明
- 状態を保持しているNotifier(Riverpodを想定しています。)内では、Repositoryで取得した値を用いてswitch式でTestの値を取得し、自身のstateを更新しています。
- 記載の通り、宣言的に例外処理が記述できるため、可読性が向上します。
まとめ
ご覧の通り、Result型を用いると、例外処理が宣言的に定義でき可読性が向上しています。
ただ、例外が起こり得る非同期処理が連続で発生した場合は、別途対処が必要になって来てしまいます。
ネストが深くなり、可読性が低下してしまうため、非同期処理を別関数で定義したりなどして対処していただければと思います。
参考文献のように、関数型なプログラミング定義も実施可能とのことなので、私も試してみたいと思います。
参考
Discussion