0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Flutterで実践する型安全な設計:Todoタイトルを値オブジェクトで管理する方法

Posted at

はじめに

Flutterでの型安全なバリデーションを設計したので、共有します。
バリデーションを漏れなくシンプルに実装できます。

目次

  1. 参考コード
  2. 前回の記事
  3. 型安全な設計とは
  4. TodoTitleの実装
  5. TodoTitleの使用方法
  6. Controller層とUsecases層の役割

参考コード

前回の記事

型安全な設計とは

title のようなユーザー入力には、以下のようなバリデーションルールが必要です。

  • 入力必須
  • 文字数制限(1文字以上、50文字以下)
  • 日本語などの形式制限(今回は省略)

これらを毎回 String に対して実装していると

  • 重複コードが増える
  • 実装漏れでバグが発生する

といった問題が起こりがちです。

型を定義すると以下のメリットがあります。

  • 型の時点でルール違反を防げる
  • ルールをまとめられる
  • ルールを再利用できる

Stringのままの場合

Before

@freezed
abstract class TodoModel with _$TodoModel {
  factory TodoModel({
    required String id,
    required String title,
    required bool isDone,
  }) = _TodoModel;

  factory TodoModel.fromJson(Map<String, dynamic> json) =>
      _$TodoModelFromJson(json);
}

専用の型を作成した場合

After

@freezed
abstract class TodoModel with _$TodoModel {
  factory TodoModel({
    required String id,
    required TodoTitle title,
    required bool isDone,
  }) = _TodoModel;

  factory TodoModel.fromJson(Map<String, dynamic> json) =>
      _$TodoModelFromJson(json);
}

TodoTitleを作成

TodoTitleは、バリデーションを内包した値オブジェクトです。
正しいタイトルでなければ、インスタンスを作成できない仕組みになっています。

コードを見る
/domain/todo_title.dart

/// Todoのタイトルを表す値オブジェクト
///
/// タイトルは1文字以上50文字以下である必要があります。
/// 空文字列は許可されません。
@JsonSerializable()
class TodoTitle {
  /// タイトルの最大文字数
  static const int maxLength = 50;

  /// タイトルの最小文字数
  static const int minLength = 1;

  /// タイトルの値
  final String value;

  /// バリデーションルール
  static final TextFieldValidator validator = TextFieldValidator(
    maxLength: maxLength,
    minLength: minLength,
  );

  const TodoTitle._(this.value);

  /// 新しいTodoTitleを作成します。
  ///
  /// [input]が空の場合は[ArgumentError]をスローします。
  /// [input]が[maxLength]を超える場合は[ArgumentError]をスローします。
  /// [input]が[minLength]未満の場合は[ArgumentError]をスローします。
  ///
  /// 例:
  /// ```dart
  /// final title = TodoTitle("新しいタスク"); // OK
  /// final emptyTitle = TodoTitle(""); // ArgumentError: 入力してください
  /// final tooLongTitle = TodoTitle("a" * 51); // ArgumentError: 50文字以内で入力してください
  /// ```
  factory TodoTitle(String input) {
    final error = validator.validate(input);
    if (error != null) throw ArgumentError(error);
    return TodoTitle._(input.trim());
  }

  /// JSONからTodoTitleを作成します。
  ///
  /// [json]は'value'キーを持つMapである必要があります。
  /// 値は文字列である必要があります。
  factory TodoTitle.fromJson(Map<String, dynamic> json) =>
      TodoTitle(json['value'] as String);

  /// TodoTitleをJSONに変換します。
  ///
  /// 戻り値は'value'キーを持つMapです。
  Map<String, dynamic> toJson() => {'value': value};

  @override
  String toString() => value;

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    return other is TodoTitle && other.value == value;
  }

  @override
  int get hashCode => value.hashCode;
}

TodoTitleを使用

以下は、TodoUsecases内でTodoTitleを使って安全にタイトルを扱っている例です。

コードを見る
/application/todo_usecases.dart

class TodoUsecases {
  final TodoRepository repository;

  TodoUsecases(this.repository);

  Future<List<TodoModel>> fetch() => repository.fetchTodos();

  Future<void> add(String rawTitle) async {
    try {
      final title = TodoTitle(rawTitle);

      final todo = TodoModel(
        id: DateTime.now().toIso8601String(),
        title: title,
        isDone: false,
      );
      await repository.createTodo(todo);
    } catch (e) {
      throw Exception('保存できません: ${e is ArgumentError ? e.message : e}');
    }
  }
}

Controller層とUsecases層がある場合

バリデーションはUsecases層で行うようにします。
Controller層は状態管理のみにし、ビジネスロジックを持たないように心がけます。

Controller

  • ユーザー操作を受けてstateを操作
  • ユースケースの呼び出し、結果の受け取りと再描画

Usecases

  • データのバリデーション
  • ドメインルールに基づく変換(copyWith(...)など)
  • Repositoryとのやりとり
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?