让位,Struct:认识 Ruby 的新 Data.define

发布: (2026年1月15日 GMT+8 04:50)
4 min read
原文: Dev.to

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"

设计上不可变

DataStruct 最大的区别在于,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)
Back to Blog

相关文章

阅读更多 »

Go 中优雅的领域驱动设计对象

❓ 你如何在 Go 中定义你的领域对象?Go 并不是典型的面向对象语言。当你尝试实现 Domain‑Driven Design(DDD)概念,如 Entity …

用 Ruby 创建 Telegram 引用机器人

要求 - 必须使用 IDE - 项目结构: ``` project_folder/ ├── bot.rb ├── quote.rb ├── help.rb ├── start.rb └── Gemfile ``` 注意: 不提交 .env 文件。