Kodokon kodokon.com

データをモデル化する:クラス、JSON、不変性

fromJson、toJson、copyWith、そして信頼できる値の等価性を使って、JSONを不変なDartのクラスへと変換しましょう。

9 分 · 3 問

このレッスンを Kodokon で開く

このレベルでは、本当のテーマはもはやクラスの構文ではなく、外の世界とあなたのコードとの境界です。APIのレスポンスがMap<String, dynamic>のまま持ち回されている限り、フィールドを読むたびに実行時の賭けをしていることになります。プロフェッショナルの原則は、JSONを受け取ったその瞬間に型付きのオブジェクトへ変換し、それ以降はそのオブジェクトだけを持ち回すことです。これを不変性、つまりfinalフィールドとconstコンストラクタと組み合わせれば、まるごと一群のバグを取り除けます。共有されたオブジェクトを誰かが裏であなたに無断で変更することはもうできなくなりますし、同一のconstインスタンス二つはコンパイラによって正規化されます。

DART
class User {
  const User({
    required this.id,
    required this.name,
    this.email,
  });

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: (json['id'] as num).toInt(),
      name: json['name'] as String,
      email: json['email'] as String?,
    );
  }

  final int id;
  final String name;
  final String? email;
}
防御的なJSONパースを備えた不変のモデル。

三つの選択について、はっきり説明しておく価値があります。まず(json['id'] as num).toInt()について。多くのAPIは、バックエンドによって整数を3とシリアライズしたり3.0とシリアライズしたりします。intへの直接のキャストはdoubleに対してクラッシュします。次にas Stringは、フィールドが欠けていれば明確なTypeErrorとともにすぐに失敗します。これは意図的なものです。パースの時点で早く失敗するほうが、nullが伝播して三画面あとで爆発するよりましです。最後にas String?は、そのフィールドが本当に省略可能であることを型のなかに明記します。

DART
class User {
  const User({required this.id, this.email});

  final int id;
  final String? email;

  Map<String, dynamic> toJson() => {
        'id': id,
        'email': email,
      };

  User copyWith({int? id, String? email}) {
    return User(
      id: id ?? this.id,
      email: email ?? this.email,
    );
  }
}
二つのフィールドに絞り込んだモデルでのtoJsonとcopyWith。

不変性は値の等価性を必要とします。デフォルトでは、Dartは参照を比較します。フィールドごとに同一な二つのUserは「異なる」とみなされてしまいます。これは状態を比較するその瞬間に、テストのなかで、Providerのselectのなかで、リストの検索のなかで、具体的な問題になります。==hashCodeは必ず一緒に再定義しましょう。

DART
class Point {
  const Point(this.x, this.y);

  final int x;
  final int y;

  @override
  bool operator ==(Object other) =>
      other is Point && other.x == x && other.y == y;

  @override
  int get hashCode => Object.hash(x, y);
}
値の等価性:==とhashCodeは対でやってくる。

理解度チェック

このレッスンの要点をしっかり覚えているか確認しましょう。

  1. json['id'] as intではなく(json['id'] as num).toInt()と書くのはなぜですか?
    • as intは実行時にas numより遅いから。
    • バックエンドが整数を3.0とシリアライズすることがあり、intへの直接のキャストはdoubleに対して失敗するから。
    • json['id']は常に文字列だから。
  2. 典型的なcopyWithの実装で、user.copyWith(email: null)は何を生み出しますか?
    • emailフィールドは要求どおりnullにリセットされる。
    • 実行時に例外が投げられる。
    • オブジェクトは現在のemailを保つ。null ?? this.emailは既存の値へフォールバックする。
    • strictモードではコードがコンパイルされない。
  3. 不変のモデルで==hashCodeを再定義するのはなぜですか?
    • Dartはデフォルトで参照を比較するから。そうしなければ、フィールドごとに同一な二つのオブジェクトは「異なる」ものになってしまう。
    • constインスタンスの生成を速くするため。
    • フィールドがfinalと宣言された時点で必須になるから。