🌏

Dart3でのResult型について

2023/06/20に公開

はじめに

例外処理について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型を用いると、例外処理が宣言的に定義でき可読性が向上しています。
ただ、例外が起こり得る非同期処理が連続で発生した場合は、別途対処が必要になって来てしまいます。
ネストが深くなり、可読性が低下してしまうため、非同期処理を別関数で定義したりなどして対処していただければと思います。
参考文献のように、関数型なプログラミング定義も実施可能とのことなので、私も試してみたいと思います。

参考

https://codewithandrea.com/articles/flutter-exception-handling-try-catch-result-type/

Discussion