让位,Struct:认识 Ruby 的新 Data.define
Source: Dev.to
简介
随着 Ruby 3.2 的引入,Data.define 现在成为在现代 Ruby 应用中处理 DTO(数据传输对象)的首选方式。虽然 Struct 多年来一直是首选,但它有一些怪癖,使其在纯粹的数据传输场景下并不理想。Data 则专门设计为 值对象 工厂。
Data.define ?
Data 是一个类工厂(类似于 Struct),用于定义简单、不可变的对象。
# Define a Data class
Address = Data.define(:city, :zip)
# Initialize it
home = Address.new(city: "Vilnius", zip: "01100")
# Access data
home.city # => "Vilnius"
设计上不可变
Data 与 Struct 最大的区别在于,Data 对象是不可变的——没有 setter 方法。
Point = Data.define(:x, :y)
p1 = Point.new(x: 1, y: 2)
p1.x = 10
# NoMethodError (undefined method `x=' for #)
在 DTO 场景中,不可变性是一个巨大的优势,因为它确保数据在通过应用的不同层时保持一致。
灵活的初始化
Data 开箱即支持位置参数和关键字参数。
Point = Data.define(:x, :y)
# Both work:
p1 = Point.new(1, 2)
p2 = Point.new(x: 1, y: 2)
注意: 使用关键字时会进行校验。传入拼写错误的关键字(例如
z: 3)会抛出ArgumentError。
相等性与模式匹配
如果两个 Data 对象的值相等,则它们被视为相等。
p1 = Point.new(1, 2)
p2 = Point.new(1, 2)
p1 == p2 # => true
p1.eql?(p2) # => true
Data 对象可以完美配合 Ruby 的模式匹配(case/in)。
case p1
in Point(x, y) if x > 0
puts "X is positive: #{x}"
end
添加自定义逻辑
和 Struct 一样,你可以向 Data.define 传入块来添加方法。
Money = Data.define(:amount, :currency) do
def to_s
"#{amount} #{currency}"
end
end
puts Money.new(100, "USD").to_s # => "100 USD"
Data 与 Struct 对比
| 功能 | Data.define (Ruby 3.2+) | Struct |
|---|---|---|
| 可变性 | 不可变(只读) | 可变(读写) |
| 初始化方式 | 位置参数 或 关键字参数 | 位置参数(需要开启关键字支持) |
| 可枚举方法 | 无 | 有(.each、.map 等) |
| 预期用途 | 值对象 / DTO | 轻量级“属性”类 |
| 接口规模 | 最小(简洁) | 较大(继承了许多方法) |
何时仍然使用 Struct
即使 Data 通常更适合 DTO,Struct 在某些场景下仍然有用:
- 需要可变性 – 当对象充当临时的“记事本”,需要频繁更新时。
- 需要可枚举 – 当你想直接对对象的属性调用
.each、.select等方法时。 - 旧版 Ruby – 当你的环境运行 Ruby 3.1 或更早版本,
Data.define不可用时。
Data.define 用于 DTO
在典型的 Rails 或 Dry‑Ruby 堆栈中,DTO 将数据从服务对象传递到视图,或从 API 响应传递到模型。使用 Data.define 有以下优势:
- 防止意外的副作用(没有人可以在请求进行到一半时更改值)。
- 代码更“诚实”——对象仅仅是数据。
- 对关键字参数的内置支持让代码比位置参数的
Struct更易读。
现代 DTO 示例
UserDTO = Data.define(:id, :email, :role)
# In a controller or service
user_data = UserDTO.new(id: user.id, email: user.email, role: user.role)