【Ruby】RubyのStructを活用する

# 1. Struct
RubyにはStructという、構造体を扱うクラスがあります。

:::note info
構造体とは？
構造体とは「1つ以上の、任意の種類の値をまとめて1つにしたもの」である。

データや情報、プログラムのコードなどを意味のある単位に分類・分割・整理して、構造的にまとめることが目的。

無秩序なデータをオブジェクト指向的に記載できるので最高。
:::

## TL;DR
- 「シンプルなデータ構造」「一時的なデータのカプセル化」を目的としている。ここでの「一時的」の定義は、主にプログラムのスコープやライフサイクルの短さを指す
- 「構造や目的がはっきりしているが、完全なクラスを定義するほどではない場合」に用いられる
- リファクタリングの文脈だと「責務が複数存在するモデルを分割するための最初のステップ」として用いられる
- Hashでサイレントエラーが起きてしまった場合にStructが検討されることもある(Structで属性間違えたらエラーが出るので、エラー検知が迅速)



# 2. Structの基本的な定義・振る舞い
- 任意のパラメーター、メソッドを定義できる構造体クラス
- 配列やHashのようにアクセスでき、型変換も可能
- 属性を持つクラスを簡単に作成
- 各属性には自動的にアクセサメソッドが提供される
- Structはクラスの一種なので、通常のクラスと同様にメソッドを追加したり、継承を使用できる（継承については非推奨らしい）

```ruby:簡潔な使用方法
# Structの定義
Person = Struct.new(:name, :age)

# Structのインスタンス作成
person = Person.new("Alice", 30)

# 属性へのアクセス（通常の方法）
puts person.name  # => Alice
puts person.age   # => 30

# 属性へのアクセス（ハッシュの記法）
puts person[:name]  # => Alice
puts person["age"]  # => 30（文字列キーも使用可能）

# 属性へのアクセス（配列の記法）
puts person[0]   # => Alice
puts person[1]   # => 30

# 属性の更新
person[:name] = "Bob"
puts person.name  # => Bob

# 型変換（ハッシュへの変換）
person_hash = person.to_h
puts person_hash  # => {:name=>"Bob", :age=>30}

# 型変換（配列への変換）
person_array = person.to_a
puts person_array  # => ["Bob", 30]

```

```ruby:Structクラスでメソッドを定義
# Structを定義時に、ブロック内でメソッドを追加
Person = Struct.new(:name, :age) do
  def introduction
    "#{name}です。#{age}歳です。"
  end

  def birthday
    self.age += 1
    "お誕生日おめでとうございます！あなたは#{age}歳になりました。"
  end
end

# Personのインスタンスを作成
person = Person.new("Alice", 30)

# カスタムメソッドの使用
puts person.introduction  # => Aliceです。30歳です。
puts person.birthday      # => お誕生日おめでとうございます！あなたは31歳になりました。
```


# 3. Structを使用する目的・使用タイミング
## 3.1 Structが用いられる典型的なシナリオ
### 意味・意図を付与したい
- 構造や目的がはっきりしているが、完全なクラスを定義するほどではない場合(Hashだと意味や意図を付与しづらい)
- HashよりはClassなどにしたほうがいいが、Classを定義するほど考えがまとまってない時

### プログラム内の短いスコープ内で限定的に使用したい
Structを使用する典型的な場面は、ある特定の処理や計算のために短期間だけ必要とされるデータ構造を作成する場合である。例えば、一つのメソッド内や短い処理フロー内でのみ利用され、その後は必要とされないような場合だ。

### リファクタリングをしたい
既存のコードを整理する過程で、一時的にStructを用いてデータを整理し、その後より適切なクラスや他のデータ構造に移行する場合がある。この文脈での「一時的」は、コードベース全体の進化や変更における過渡期を指す。例えば、責務が複数存在するクラスを分割する過程など。

### コードの試行錯誤を行いたい
新しい機能を開発する初期段階で、Structを使って素早くデータ構造を試作し、後により恒久的な解決策に置き換える場合もある。ちょっとコードを書いて検証とかしたい時にも使える

## 3.2 Structの運用が適切でないシナリオ
- 複雑なビジネスロジックが必要な場合
    - 多くのメソッドや複雑な振る舞いが必要な場合(Classの方が機能が豊富)
- データ構造が頻繁に変更される場合
    - 構造が頻繁に変更される可能性がある場合(Hashの方が柔軟)

## 3.3 備考

:::note info
StructとClass
- StructはClassに似てる。構造体という括りではClassとStructは同義であると考えていい
- その中で、使い分けの方法がある
    - Classはソフトウェア開発において仕様が定まり切っており、半永久的に使われる場合に使用される
    - Structは簡易クラスの作成であり、リファクタリングの文脈においてクラス分割をする一時的な処理で記載されることも多い(Struct自体に命名ができるので)
    - 簡単な振る舞いや軽微なロジックのみを必要とするオブジェクトの場合、Structを使用すると、クラスよりも簡潔に表現できる
:::

:::note info
StructとHash
- ハッシュとしての柔軟性が必要だが、一定の属性に対する明確なインターフェース（例：名前、年齢）が必要な場合、Structは適している。これにより、ハッシュのキーのスペルミスなどのミスを防ぐことができる。
- Structを使用すると、データをカプセル化し、外部からの直接の変更を防ぐことができる(安全性)。ClassとStructはそれができるが、Hashはそれができない
:::


# 4. Structの振る舞いから観測する、Class・Struct・Hashのそれぞれの違い

| 特徴 | Class (ActiveRecord) | Struct | Class (Plain Ruby) | Hash |
|------|----------------------|--------|--------------------|------|
| 複数の異なる種類のデータを一つの単位でまとめる | ◯ | ◯ | ◯ | ◯ |
| アクセサーメソッド(名前付き属性の定義) | ◯ | ◯ | ◯ | ✕ |
| 複雑なビジネスロジックが必要な場合 | ◯ | ✕ | ◯ | ✕ |
| 柔軟性 | ✕ | ✕ | ✕ | ◯ |


## 4.1 複数の異なる種類のデータを一つの単位でまとめる

| 特徴 | Class (ActiveRecord) | Struct | Class (Plain Ruby) | Hash |
|------|----------------------|--------|--------------------|------|
| 複数の異なる種類のデータを一つの単位でまとめる | ◯ | ◯ | ◯ | ◯ |

```ruby:1. Class (ActiveRecord)
class Person < ApplicationRecord
  # name: string, age: integer, email: string
end

user = Person.create(name: "Alice", age: 30, email: "alice@example.com")
```


```ruby:2. Struct
Person = Struct.new(:name, :age, :email)

person = Person.new(name: "Alice", age: 30, email: "alice@example.com")
puts person.name  # => Alice
```


```ruby:3. Class (PORO)
class Person
  attr_accessor :name, :age, :email

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

person = Person.new(name: "Alice", age: 30, email: "alice@example.com")
puts person.name  # => Alice
```


```ruby:4. Hash
person = { name: "Alice", age: 30, email: "alice@example.com" }

puts person[:name]  # => Alice
```



## 4.2 アクセサーメソッド
| 特徴 | Class (ActiveRecord) | Struct | Class (Plain Ruby) | Hash |
|------|----------------------|--------|--------------------|------|
| アクセサーメソッド | ◯ | ◯ | ◯ | ✕ |


```ruby:1. Class (ActiveRecord)
class Person < ApplicationRecord
  # name: string, age: integer のカラムがあると仮定
end

person = Person.new(name: "Alice", age: 30)
puts person.name  # => Alice
person.age = 31
puts person.age   # => 31

# 存在しない属性へのアクセスするとエラーになる
person.email  # => NoMethodError

# 初期化時に指定していないパラメーター名を渡すとエラーになる
Person.new(name: "Alice", age: 30, email: "alice@example.com") # => ArgumentError

```

```ruby:2. Struct
Person = Struct.new(:name, :age)

person = Person.new("Alice", 30)
puts person.name  # => Alice
person.age = 31
puts person.age   # => 31

Person.new(name: "Alice", age: 30, email: "alice@example.com") # => ArgumentError

# 存在しない属性へのアクセスするとエラーになる
person.email  # => NoMethodError

# 初期化時に指定していないパラメーター名を渡すとエラーになる
Person.new(name: "Alice", age: 30, email: "alice@example.com") # => ArgumentError
```

```ruby:3. Class (PORO)
class Person
  attr_accessor :name, :age

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

person = Person.new("Alice", 30)
puts person.name  # => Alice
person.age = 31
puts person.age   # => 31

# 存在しない属性へのアクセスするとエラーになる
person.email  # => NoMethodError

# 初期化時に指定していないパラメーター名を渡すとエラーになる
Person.new(name: "Alice", age: 30, email: "alice@example.com") # => ArgumentError
```

```ruby:4. Hash
person = { name: "Alice", age: 30 }

# 存在しない属性へのアクセスするとnilを返す
puts person[:email]  # => nil

# 任意のキーを追加できる
person = { name: "Alice", age: 30, email: "alice@example.com" }
puts person[:email]  # => alice@example.com
```

# 4.3 複雑なビジネスロジックが必要な場合

| 特徴 | Class (ActiveRecord) | Struct | Class (Plain Ruby) | Hash |
|------|----------------------|--------|--------------------|------|
| 複雑なビジネスロジックが必要な場合 | ◯ | ✕ | ◯ | ✕ |


```ruby:Class (ActiveRecord)
class Person < ApplicationRecord
  # name: string, age: integer のカラムがあると仮定

  def full_profile
    "Name: #{name}, Age: #{age}, Status: #{status}"
  end

  private

  def status
    age > 30 ? "Senior" : "Junior"
  end
end

# 使用例
person = Person.new(name: "Alice", age: 30)
puts person.full_profile  # "Name: Alice, Age: 30, Status: Junior"
```

```ruby:Class (PORO)
class Person
  attr_accessor :name, :age

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

  def profile
    "Name: #{name}, Age: #{age}, Category: #{category}"
  end

  private

  def category
    age > 30 ? "Adult" : "Youth"
  end
end

# 使用例
person = Person.new("Carol", 40)
puts person.profile  # "Name: Carol, Age: 40, Category: Adult"
```

Structはあくまでシンプルなデータ保持のために設計されており、ビジネスロジックの定義は単純かつ補助的役割に留めたい。なぜなら、複雑なロジックを組み込むとコードが複雑化し、保守が困難になるため。

Hashは、単純なデータ格納や一時的なデータ操作に適しており、複雑なビジネスロジックの表現をするならClassに分割するべき。


## 4.4 柔軟性

| 特徴 | Class (ActiveRecord) | Struct | Class (Plain Ruby) | Hash |
|------|----------------------|--------|--------------------|------|
| 柔軟性 | ✕ | ✕ | ✕ | ◯ |

データ構造が頻繁に変更される場合

```ruby:柔軟性がある例
person = { name: "Alice", age: 30 }

# 使用例
# 新しいキーと値を自由に追加
person[:email] = "alice@example.com"
puts person[:email]  # => alice@example.com

# キーと値を動的に変更可能
person[:hobby] = "Reading"
puts person[:hobby]  # => Reading
```


# 5. 参考文献
https://wa3.i-3-i.info/word13243.html
https://qiita.com/megane42/items/5bfdb0764fa575efbdab
https://qiita.com/ex_SOUL/items/696b3ac2869a2f71f3c0
https://blog.shogo-mizuno.me/entry/2018/02/16/155855
https://speakerdeck.com/osyo/jin-geng-wen-kenai-struct-falseshi-ifang-tojin-hou-falseke-neng-xing-nituite
https://zenn.dev/powder/articles/ruby_struct_20221103