深入理解 Dart Freezed:从 OO SOLID 的视角
在 Dart 和 Flutter 的开发中,freezed
是一个不可或缺的库。它能为我们自动生成不可变(Immutable)数据类的模板代码,将我们从繁琐的 copyWith
、==
操作符重载、hashCode
以及 toString
的编写中解放出来。
然而,当我们第一次接触 freezed
生成的代码时,常常会感到困惑。一个简单的类,为什么会生成如此复杂的层级结构?
// 我们写的简单代码
class User with _$User {
const factory User({
required String name,
required int age,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
仅仅是这段代码,就会牵扯出 _$User
(一个 mixin)、_User
(一个私有类)、_$UserFromJson
(一个函数)以及最终的 _$UserImpl
(另一个我们看不见的实现类)。这背后究竟是怎样的设计哲学?最起码来说,直接让目标类型 User
继承一个完整的实现的子类 _$UserImpl
,岂不是更简单?
一、剖析生成代码:揭开 Freezed 的神秘面纱
要理解 freezed
的设计哲学,最好的方法就是直面它生成的代码。许多困惑都源于我们不清楚 part
文件背后到底发生了什么。
让我们以一个表示网络请求结果的 Result
联合类型(Union Type)为例,这是 freezed
最强大的功能之一。
第一步:我们手写的文件 (result.dart
)
import 'package:freezed_annotation/freezed_annotation.dart';
part 'result.freezed.dart';
part 'result.g.dart';
class Result<T> with _$Result<T> {
const factory Result.success({required T data}) = _Success<T>;
const factory Result.failure({required String message}) = _Failure<T>;
factory Result.fromJson(Map<String, dynamic> json) => _$ResultFromJson(json);
}
我们在这里定义了意图:一个名为 Result
的类型,它有两个状态 success
和 failure
。我们还希望它能从 JSON 转换而来。
第二步:Freezed 生成了什么?
当我们运行构建命令后,freezed
和 json_serializable
会生成 result.freezed.dart
和 result.g.dart
。让我们来看看其中的关键部分(已简化):
-
result.freezed.dart
-mixin _$Result<T>
mixin _$Result<T> { // 提供了 when/map 等方法的"签名" // 它们的实现只是简单地抛出异常 TResult when<TResult extends Object?>({ required TResult Function(T data) success, required TResult Function(String message) failure, }) => throw _privateConstructorUsedError; Map<String, dynamic> toJson() => throw _privateConstructorUsedError; }
这是一个行为契约。你通过
with _$Result<T>
将它应用到你的类上。它并不包含真正的逻辑,但它向 Dart 编译器保证了when
、toJson
等方法的存在,让你可以在代码中安全地调用它们。$
前缀提醒我们:这是我需要消费的、由工具生成的契约。 -
result.freezed.dart
- 子类的"抽象"与"实现" 对于success
分支,freezed
生成了两个类:// 1. 开发者定义的"抽象标签" abstract class _Success<T> implements Result<T> { const factory _Success({required final T data}) = _$SuccessImpl<T>; factory _Success.fromJson(Map<String, dynamic> json) = _$SuccessImpl<T>.fromJson; // ... } // 2. 工具生成的"具体实现" () class _$SuccessImpl<T> implements _Success<T> { const _$SuccessImpl({required this.data}); final T data; // ... 此处包含 hashCode, ==, copyWith, toString 的真正实现 ... Map<String, dynamic> toJson() { return _$$SuccessImplToJson(this); // 调用 .g.dart 中的函数 } }
这里揭示了设计的核心:
- 你在
factory
中指定的_Success
,被freezed
生成为一个abstract
类。它的作用是作为一座桥梁,明确声明_Success
是Result
的一个子类型,并将构造任务转发给真正的实现类_$SuccessImpl
。 _$SuccessImpl
才是真正的"幕后黑手",它包含了所有属性和方法的具体实现。
- 你在
-
result.freezed.dart
和result.g.dart
-fromJson
的实现// in result.freezed.dart Result<T> _$ResultFromJson<T>(Map<String, dynamic> json) { // 这是一个"分派中心" switch (json['runtimeType']) { case 'success': return _Success<T>.fromJson(json); case 'failure': return _Failure<T>.fromJson(json); default: throw CheckedFromJsonException(...); } } // in result.g.dart // 这是真正干活的序列化/反序列化函数 Map<String, dynamic> _$$SuccessImplToJson<T>(...) { ... } _$SuccessImpl<T> _$$SuccessImplFromJson<T>(...) { ... }
这里我们看到了
fromJson
的全貌:- 你调用的
_$ResultFromJson
是一个顶层函数,它通过检查 JSON 中的runtimeType
字段来决定具体调用哪个子类的fromJson
。 - 而每个子类的
fromJson
(如_Success.fromJson
)最终会调用.g.dart
文件中由json_serializable
生成的_$$SuccessImplFromJson
函数来完成真正的转换工作。
- 你调用的
第三步:用"四层模型"重新理解
现在,我们可以用一个清晰的模型来总结我们所看到的一切:
- API 定义层 (
Result<T>
): 你手写的类,是公开的 API。 - 生成行为契约层 (
_$Result<T>
):mixin
,提供方法签名。 - 开发者抽象定义层 (
_Success<T>
): 你在factory
中定义的稳定钩子,作为连接你和工具的桥梁。 - 工具实现层 (
_$SuccessImpl<T>
): 包含所有模板代码的具体实现类。
这个模型解释了我们最初的问题:
- 我们需要
_Success
这个由我们命名的中间层,是为了实践依赖倒置,让我们的高层 API 不直接依赖于工具生成的低层实现细节_$SuccessImpl
。 - 我们需要
_$ResultFromJson
这个分派中心,是为了在运行时能够动态地、类型安全地根据 JSON 内容创建出正确的子类型实例。
二、freezed
与 SOLID 原则的完美合奏
现在,让我们从 SOLID 的视角,来审视这个四层模型为何如此设计。
D - 依赖倒置原则 (Dependency Inversion Principle)
这是理解 freezed
设计核心的关键。该原则指出:高层模块不应依赖于低层模块,二者都应依赖于抽象。
-
错误的方式:如果我们的 factory 直接指向实现类,会怎样?
const factory Result.success({required T data}) = _$SuccessImpl<T>;
在这种情况下,你的Result
类(高层模块)直接依赖了_$SuccessImpl
(一个由工具生成的、不稳定的低层实现细节)。如果freezed
未来改变其内部命名规则,你的代码就会崩溃。 -
freezed
的正确方式:const factory Result.success({required T data}) = _Success<T>;
在这里,Result
类(高层模块)依赖于你定义的_Success
(一个稳定的抽象)。同时,freezed
生成的_$SuccessImpl
(低层模块)也去实现这个_Success
抽象。如此一来,高层和低层都依赖于你亲手定义的、稳定的抽象
_Success
。这便是依赖倒置的精髓!你的factory
声明不再是"消费一个实现细节",而是"定义一个抽象契约",并将实现任务委托给代码生成工具。
O - 开闭原则 (Open/Closed Principle)
freezed
的联合类型是开闭原则的典范。
- 对扩展开放:你可以随时为
Result
添加新的状态,比如loading
状态:const factory Result.loading() = _Loading<T>;
- 对修改关闭:增加这个新状态,你完全不需要修改
Result
基类或任何已有的_Success
、_Failure
的实现。你只需在fromJson
的分派中心和when
方法中处理新的情况即可,而这些都是由工具自动完成的。
L - 里氏替换原则 (Liskov Substitution Principle)
任何一个 Result
的子类型实例(如 _Success
或 _Failure
)都可以被其父类型 Result
的引用所替代,而程序的行为不会出错。这正是 freezed
强大的 when
方法能够类型安全地工作的基石。
void processResult(Result<int> result) {
// 无论传入的是 Success还是Failure,程序都能正确处理
String message = result.when(
success: (data) => 'Success! Data is $data',
failure: (message) => 'Failed with error: $message',
loading: () => 'Loading...', // 如果我们增加了 loading 状态
);
print(message);
}
S - 单一职责原则 (Single Responsibility Principle)
freezed
的结构清晰地划分了职责:
- 你的手写文件:负责定义业务意图和 API 结构。
- 生成的
Impl
类:负责实现与业务无关的模板代码(==
,hashCode
,copyWith
等)。 - 生成的
.g.dart
文件:负责具体的 JSON 序列化/反序列化逻辑。 - 生成的
.freezed.dart
文件:负责连接以上所有部分,并提供when
/map
等联合类型特有的功能。
每个部分各司其职,互不干扰。
三、结论
初看 freezed
,我们可能会被其生成的代码层级所迷惑。但通过 SOLID 的棱镜去审视,便会发现这并非过度设计,而是一个将软件工程原则内化于心的、极其精巧的架构。
它通过依赖倒置,将你的手写代码(API 定义)与工具代码(实现细节)完美解耦;通过引入 $
和 _
的命名约定,清晰地划分了开发者和工具的职责边界。
下一次,当你写下 const factory ... = _MyType;
时,请记住,你不仅是在定义一个构造函数,更是在实践一种优雅的软件设计思想:定义抽象,而非依赖实现。这正是 freezed
教给我们的、超越代码生成本身的宝贵一课。
四、后记:殊途同归——从Freezed看FP与OO的深层一致性
函数式编程(FP)与面向对象编程(OO)的核心思想,在表面上似乎截然相反。FP 强调"数据"与"行为"的分离,而 OO 则强调将它们"封装"在一起。但当我们深入探索,会发现这两种范式在追求高质量软件的道路上,最终会走向同一个目的地。
OO 的发展并非止步于封装。为了驾驭大型软件的复杂性,SOLID 原则应运而生,其中的"依赖倒置原则"恰恰引导开发者走向一种更高级的分离:行为契约与具体实现的分离。我们依赖的"接口"(Interface),本质上就是一个不包含具体实现的"行为契约";而实现接口的类,则是承担具体工作的"执行者"。
此时,OO 的思想已经从"将数据和行为捆绑",升华为"将稳定的契约与易变的实现解耦"。这与 FP 将不变的数据结构与可组合的函数行为分离开来的思想,在哲学层面形成了深刻的共鸣。
freezed
的精妙之处,就在于它将这种原本深藏于高级 OO 设计理论中的思想,以一种极其直观甚至带有强制性的方式,呈现在了我们面前:
-
我们手写的代码 (
@freezed class...
):这不再是一个传统的类,而是一个数据契约的声明。它只定义"需要什么"(what),描述了数据的结构与意图,这与在 FP 中定义一个不可变的数据类型如出一辙。 -
工具生成的代码 (
.freezed.dart
,.g.dart
):这是纯粹的行为实现。它负责所有繁杂、机械的"如何做"(how),比如copyWith
、toJson
等。
通过 freezed
,我们被迫将"契约"与"实现"分置于两个世界——手写代码与生成代码。这让我们清晰地看到,无论是 FP 还是成熟的 OO 设计,其管理复杂性的终极武器都是一样的:守护住稳定的抽象核心,将易变的实现细节隔离出去。这或许就是 freezed
这个小工具,带给我们的最大启示。