[読書会]freezed 2.5.7 翻訳及び補足(Motivation)

freezedは、データクラス、ユニオン(合併型)、およびクローニング用のコードジェネレーターです。
これはDartプログラミング言語におけるモデル定義を簡単にするために設計されています。

Note(注釈)
現在、Freezedのマクロを使用した初期プレビュー版が利用可能です。
詳細については、GitHubのリンク https://github.com/rrousselGit/freezed/tree/macros を参照してください。

本ブログの(上記https://pub.dev/packages/freezedの)翻訳および解説の目次頁(構成上のトップページ)は次のURLになります。

本頁は、上記のうちの、「Motivation」の章の翻訳および解説頁になります。

1.Motivation(きっかけ)

Dart言語は素晴らしいですが、モデル定義するのには、次の様な煩雑な作業が伴います。

  1. コンストラクタおよびプロパティの定義
  2. (toString、== 演算子、hashCode の)オーバーライド
  3. (オブジェクトをクローンするための) copyWith メソッドの実装
  4. シリアライゼーションとデシリアライゼーションの処理

これらを全て行うと、数百行に渡るコードが必要になり、エラーが発生し易くなり、モデルの可読性にも悪影響を及ぼします。

Freezedを利用することで上記の殆どを自動で実装できます。
これによりモデル定義に集中でき、冗長なコードを書く手間を省き、より効率的に開発を進めることが可能になります。

freezed利用時のコード(これだけで済む、という意味)
@freezed
class Person with _$Person {
  const factory Person({
    required String firstName,
    required String lastName,
    required int age,
  }) = _Person;

  factory Person.fromJson(Map<String, Object?> json) 
    => _$PersonFromJson(json);
}
freezedを使用しない場合のコード(こんなに書く必要があります、という意味)
//(1)`@immutable`アノテーションは、このクラスが不変であることを示す。
//(オブジェクトが作成された後はその状態が変更されないことを意味する。
@immutable
class Person {
  //(2)コンストラクタ:必要な情報を受け取り、Person オブジェクトを初期化する。
  //(required キーワードは、各パラメータが必須であることを示す)
  const Person({
    required this.firstName,
    required this.lastName,
    required this.age,
  });

  //(3)ファクトリコンストラクタ(JSONからPersonオブジェクトを作成)
  //(JSON形式のデータを受け取り、新しいPersonオブジェクトを返す)
  factory Person.fromJson(Map<String, Object?> json) {
    return Person(
      //JSONのキー'firstName'から値を取得し、Stringにキャストします。
      firstName: json['firstName'] as String,
      //JSONのキー'lastName'から値を取得し、Stringにキャストします。 
      lastName: json['lastName'] as String, 
      //JSONのキー'age'から値を取得し、intにキャストします。
      age: json['age'] as int, 
    );
  }

  //(4)フィールド定義(Person オブジェクトの各データを保持する変数)
  final String firstName;
  final String lastName;
  final int age;

  //(5)copyWithメソッド
  //(既存Personオブジェクトの一部フィールドを変更し、新オブジェクト作成)
  Person copyWith({
    String? firstName,
    String? lastName,
    int? age,
  }) {
    return Person(
      //新しいfirstNameが提供されなければ、現在の値を使用します。
      firstName: firstName ?? this.firstName,
      //新しいlastNameが提供されなければ、現在の値を使用します。 
      lastName: lastName ?? this.lastName, 
      //新しいageが提供されなければ、現在の値を使用します。
      age: age ?? this.age, 
    );
  }

  //(6)toJSONメソッド:PersonオブジェクトをJSON形式に変換します。
  Map<String, Object?> toJson() {
    return {
      'firstName': firstName,
      'lastName': lastName,
      'age': age,
    };
  }

  //(7)toStringメソッド:Personオブジェクトを文字列形式で表現します。
  @override
  String toString() {
    return 'Person(firstName: $firstName, lastName: $lastName, age: $age)';
  }

  //(8)== 演算子のオーバーライド
  //(二つのPersonオブジェクトが等しいかどうかを判断します)
  @override
  bool operator ==(Object other) {
    return other is Person &&
      runtimeType == other.runtimeType &&
      firstName == other.firstName &&
      lastName == other.lastName &&
      age == other.age;
  }

  //(9)hashCodeのオーバーライド(オブジェクトのハッシュコードを生成)
  @override
  int get hashCode => Object.hash(
    runtimeType,
    firstName,
    lastName,
    age,
  );
}
参考) @freezed による toString メソッドのオーバーライドの具体的なメリット

toString メソッドのオーバーライドは、デバッグやログ出力時に非常に便利です。
@freezed が提供する toString メソッドは、クラス名と全フィールドの値を含むわかりやすい形式でオブジェクトを表示します。
これにより、オブジェクトの状態を簡単に確認できます。

便利になった使用例:print(person); と書くだけで…(以下の出力結果になる)
void main() {
  final person = Person(firstName: 'John', lastName: 'Doe', age: 30);
  print(person);
}
出力結果:
Person(firstName: John, lastName: Doe, age: 30)

もし@freezedによるtoString()オーバーライドが無かったら、上記出力結果を得る為には、以下の様に書く必要があります。

もし@freezedによるtoString()オーバーライドが無かった場合の出力用コード:
void main() {
  final person = Person(firstName: 'John', lastName: 'Doe', age: 30);
  print('firstName: ${person.firstName}, lastName: ${person.lastName}, age: ${person.age}');
}

しかも、上記の様に、書く場合ですら、もし@freezedによる自動生成が無かったとしたら、クラス作成コードについても、次の違いがあるわけです。

@freezedを利用する場合:クラス定義
@freezed
class Person with _$Person {
  const factory Person({
    required String firstName,
    required String lastName,
    required int age,
  }) = _Person;
}
@freezedを利用しない場合:クラス定義
class Person {
  final String firstName;
  final String lastName;
  final int age;

  Person({
    required this.firstName,
    required this.lastName,
    required this.age,
  });
}

※しかも、上記では、シリアライズ・デシリアライズについての記述は省略していますので、@freezedを利用しない場合は、これについても、煩雑なコーディングが必要になります。

上記の様に、@freezed が提供する toString メソッドのオーバーライドは、手動でフィールドの値を表示するコードを書く必要がなくなるため、とても便利です。@freezed を使わない場合、人間は以下のように手動で toString メソッドをオーバーライドするか、個別にフィールドの値を表示するコードを書く必要があるわけです。

参考) @freezed による == 演算子のオーバーライドの具体的なメリット

① オブジェクトの内容比較:
 == 演算子をオーバーライドすることで、オブジェクトのメモリアドレスではなく、オブジェクトの内容(フィールドの値)が等しいかどうかを比較することができます。これにより、同じデータを持つ別々のオブジェクトが等しいと判断されます。

② コレクション操作の一貫性:
 Dart では、コレクション(リスト、セット、マップなど)に含まれる要素の等価性を判断する際に == 演算子を使用します。== 演算子をオーバーライドすることで、コレクション内でのオブジェクトの検索、削除、重複チェックなどが正しく機能するようになります。

③ テストの簡略化:
 単体テストや統合テストでオブジェクトの等価性をチェックする際に、== 演算子のオーバーライドが役立ちます。オブジェクトが正しく比較されるため、テストコードがシンプルかつ明確になります。

以下、両者による判定結果です。

(1)オーバーライド前(デフォルトの == 演算子)
デフォルトの == 演算子はオブジェクトのメモリアドレスを比較するため、同じ内容を持つ別のインスタンスは等しくないと判断されます。

デフォルトの==演算子:(同じ内容でも異なるインスタンスだとfalseになってしまう)
class Person {
  final String firstName;
  final String lastName;
  final int age;

  Person({
    required this.firstName,
    required this.lastName,
    required this.age,
  });
}

void main() {
  final person1 = Person(firstName: 'John', lastName: 'Doe', age: 30);
  final person2 = Person(firstName: 'John', lastName: 'Doe', age: 30);

  //デフォルトの==演算子:
  print(person1 == person2); // false
}

(2)オーバーライド後(freezed によるオーバーライド)
@freezed を使用することで、== 演算子がオーバーライドされ、オブジェクトの内容が比較されます。

@freezedによりオーバーライドされた==演算子:(同じ内容ならtrueになる)
import 'package:freezed_annotation/freezed_annotation.dart';

part 'person.freezed.dart';

@freezed
class Person with _$Person {
  const factory Person({
    required String firstName,
    required String lastName,
    required int age,
  }) = _Person;
}

void main() {
  final person1 = Person(firstName: 'John', lastName: 'Doe', age: 30);
  final person2 = Person(firstName: 'John', lastName: 'Doe', age: 30);
  //@freezedによりオーバーライドされた==演算子:
  print(person1 == person2); // true
}

コードの詳細
オーバーライドされた == 演算子は、以下の条件をチェックします。

  • 比較対象が Person 型であるかどうか。
  • オブジェクトのランタイムタイプが一致しているかどうか。
  • 各フィールド(firstName、lastName、age)の値が等しいかどうか。

このように、== 演算子をオーバーライドすることで、オブジェクトの内容を正しく比較できるようになります。これにより、コレクション操作やテストの一貫性が向上し、コードの品質が向上します。@freezed を使用すると、このオーバーライドが自動的に行われるため、開発者は手動で実装する必要がなく、手間を省くことができます。

参考) @freezed による hashCodeのオーバーライドの具体的なメリット

hashCode のオーバーライドには、オブジェクトのハッシュコードを生成するためのメソッドが提供されます。
これにはいくつかの具体的なメリットがあります。

① コレクションの効率的な操作:
 Dart のコレクション(セットやマップ)では、要素の追加、削除、検索の際にハッシュコードを使用します。
 hashCode をオーバーライドすることで、コレクション内でのオブジェクトの操作が効率的かつ正確に行われます。

② オブジェクトの同一性の保証:
 hashCode は == 演算子と連動して動作します。
 == 演算子がオーバーライドされている場合、そのオブジェクトが等しいと判断されるときに hashCode も同じ値を返す必要があります。これにより、オブジェクトの同一性が保証されます。

以下、両者による判定結果です。

(1)オーバーライド前(デフォルトの hashCode)
デフォルトの hashCode は、オブジェクトのメモリアドレスに基づいて生成されるため、同じ内容を持つ別のインスタンスは異なるハッシュコードを持ちます。

デフォルトのhashCode:(同じ内容でも異なるインスタンスだとfalseになってしまう)
class Person {
  final String firstName;
  final String lastName;
  final int age;

  Person({
    required this.firstName,
    required this.lastName,
    required this.age,
  });
}

void main() {
  final person1 = Person(firstName: 'John', lastName: 'Doe', age: 30);
  final person2 = Person(firstName: 'John', lastName: 'Doe', age: 30);

  //デフォルトのhashCode:
  print(person1.hashCode); // 異なるハッシュコード
  print(person2.hashCode); // 異なるハッシュコード
}

(2)オーバーライド後(freezed によるオーバーライド)
@freezed を使用することで、hashCode がオーバーライドされ、オブジェクトの内容に基づいたハッシュコードが生成されます。

@freezedによりオーバーライドされたhashCode:(同じ内容ならtrueになる)
import 'package:freezed_annotation/freezed_annotation.dart';

part 'person.freezed.dart';

@freezed
class Person with _$Person {
  const factory Person({
    required String firstName,
    required String lastName,
    required int age,
  }) = _Person;
}

void main() {
  final person1 = Person(firstName: 'John', lastName: 'Doe', age: 30);
  final person2 = Person(firstName: 'John', lastName: 'Doe', age: 30);

  //@freezedによりオーバーライドされたhashCode:
  print(person1.hashCode); // 同じハッシュコード
  print(person2.hashCode); // 同じハッシュコード
}

コードの詳細
オーバーライドされた hashCode は、以下のフィールドを基にハッシュコードを生成します。

  • オブジェクトのランタイムタイプ(runtimeType)
  • 各フィールド(firstName、lastName、age)

この様に、hashCode をオーバーライドすることで、オブジェクトのハッシュコードがその内容に基づいて生成され、== 演算子と整合性が取れるようになります。これにより、コレクション内でのオブジェクトの操作が効率的かつ正確に行われ、オブジェクトの同一性が保証されます。@freezed を使用すると、この hashCode のオーバーライドが自動的に行われるため、開発者は手動で実装する必要がなく、手間を省くことができます。

(続きの記事はこちら)

コメントを残す