はじめに
皆さん、Freezedを使ってますでしょうか?
簡単な記述で、Jsonのシリアライズ・デシリアライズや、オブジェクト同士の比較のオーバライドメソッドなどを生成してくれるFreezedは大変便利です。
特に便利に感じるというか、必須だと思うのが単体テストを書こうと思ったときです。
Stringとかintのプリミティブ型を多用せず、オブジェクト指向に基づき、丁寧にクラス化を行っていると、
比較するオブジェクトに「==」と「hashCode」が必要になってくるので、
それをいちいち実装するのは結構手間になってきます。
また不変オブジェクトとして生成できるので、値オブジェクトにも使えるというのも利点かと思います。
本記事は値オブジェクトにFreezedを活用する際、コンストラクタで引数の値のバリデートを行う場合、どうすればいいかというプラクティスになります。
経緯として、値オブジェクトを表現する上で、初期化時に不正な値のチェックを行いたい(行わないと生焼けオブジェクトになる)という場面がありましたが、実装方法がわからなかったので調査したので、他にも困っている人がいるかと思い、アウトプットすることにしました。
対象読者
・Freezedを使ってコンストラクタで初期値のバリデーションを行いたい方
目次
概要
方法としてはシンプルで、コンストラクタにバリデーションを追加します。
一応、これだけでオブジェクト初期化時にバリデーションを行えて、不正な値だった場合、例外を投げるといった処理が可能となります。
しかし、上記の方法には一点だけ問題があります。
それは、自動生成されるcopyWith
メソッド内でバリデーションを実装したコンストラクタが呼ばれないことです。
つまり、copyWith
を使うと不正な値が混入する可能性があるということです。
この問題に対処するべく、copyWith
は自動ではなく、自分で実装するという方法をとりました。
(もっといい方法あれば教えてください...。)
サンプルコード
ユーザーの声明を表現するUserName
という値オブジェクトをFreezedで作成するというケースでサンプルコードを載せておきます。
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user_name.freezed.dart';
// 大文字で@Freezedとすると、後ろに()書きで不要なものを削除できる
// 初期化する場合はcopywithを生成しないように指定する
// Freezedで自動生成された「copyWith」メソッドでは
// バリデーションを実装したコンストラクタ(サンプルコードでいうと「factory UserName」の部分)が呼び出されないため、
// copyWithは自分で実装する
@Freezed(copyWith: false)
abstract class UserName implements _$UserName{
UserName._();
factory UserName._internal({
required String firstName,
required String lastName,
}) = _UserName;
factory UserName({
required String firstName,
required String lastName,
}){
if(firstName.isEmpty){
throw Exception("firstName is Empty.");
}
if(lastName.isEmpty){
throw Exception("lastName is Empty.");
}
return UserName._internal(
firstName : firstName,
lastName : lastName,
);
}
// バリデーションを実装したコンストラクタを呼び出すようにcopyWithを実装
UserName copyWith({String? firstName, String? lastName}){
final copiedFirstName = firstName ?? this.firstName;
final copiedLastName = lastName ?? this.lastName;
return UserName(firstName: copiedFirstName, lastName: copiedLastName);
}
}