📦

Dataクラスを使ってDTOのサンプルを作ってみる

2024/02/29に公開

Dataクラスとは

Ruby3.2で導入されたクラス。
Structとあまり使用感が変わらないものの、できることを減らしている。

https://bugs.ruby-lang.org/issues/16122

中でもimmutable(不変)であるところが大きい。

MyStruct = Struct.new(:a, :b)
MyData = Data.define(:a, :b)

my_struct = MyStruct.new(a: 1, b: 2)
my_data = MyData.new(a: 1, b: 2)

my_struct.a = 99
my_data.a = 99 # undefined method error

DTO(Data Transfer Object)とは

デザインパターンの一つで、データの受け渡しに使う入れ物。

使わない例

def show_profile(user_data)
  puts "name: #{user_data[:name]}"
  puts "age: #{user_data[:age]}"
end

show_profile({ name: "John Doe", age: 30 })

「RubyはすべてObjectだからhashもDTOだ!」と言えなくもない(?)が、型があまりになさすぎるので、保守性に欠ける。

使う例

class UserDto
  attr_reader :name, :age

  def initialize(name:, age:)
    @name = name
    @age = age
  end
end

def display_user_info(user_dto)
  puts "User Name: #{user_dto.name}"
  puts "User Age: #{user_dto.age}"
end

user_dto = UserDto.new(name: "John Doe", age: 30)
display_user_info(user_dto)

どの値をやりとりしているかが分かりやすくなったし、ゲッタで取れるのでコード自体も書きやすくなった

他の書き方

Structを使う例

Struct(構造体)を使うことでもう少し簡素に書ける

UserDto = Struct.new(:name, :age)

def display_user_info(user_dto)
  puts "User Name: #{user_dto.name}"
  puts "User Age: #{user_dto.age}"
end

user_dto = UserDto.new(name: "John Doe", age: 30)
display_user_info(user_dto)

しかしStructはゲッタの他にセッタも作るため、immutableには作れない

user_dto = UserDto.new(name: "John Doe", age: 30)
user_dto.age = 100
display_user_info(user_dto)

=> # User Name: John Doe
   # User Age: 100

DTOの概念としてはimmutableじゃないといけないということも無さそうだった、ただ自分の用途としては初期化以降は手を入れないものとして扱いたかった。

Dataクラスを使う例(本題)

ここでimmutableに作れるというDataクラスがうまくハマるのではと思ったのがこちら

UserDto = Data.define(:name, :age)

def display_user_info(user_dto)
  puts "User Name: #{user_dto.name}"
  puts "User Age: #{user_dto.age}"
end

user_dto = UserDto.new(name: "John Doe", age: 30)
display_user_info(user_dto)

こちらはしっかりエラーを出してくれる

user_dto = UserDto.new(name: "John Doe", age: 30)
user_dto.age = 100 # undefined method error

xxxServiceっぽいものを作ってみる

Railsで「Userテーブルを元にした集計データを返すサービス」という例で書いてみる。

class SummaryUsersQueryService
  Dto = Data.define(:count, :max_age)

  def execute
    Dto.new(count: User.count, max_age: User.maximum(:age))
  end
end

class ExampleController
  def index
    @summary = SummaryUsersQueryService.new.execute
    @summary.max_age # => 30
  end
end

immutableでしっかりしたDTOが割と簡素に書けるようになったと思う。

SMARTCAMP Engineer Blog

Discussion