😎

【Rails】分割代入っぽいことをしてクラス初期化を見やすくする

2023/12/28に公開

オブジェクトがよしなに動いてくれて記述量が減るよりも、記述量が多少増えても開発者の目に優しい方がいいよね派の藤谷です。

外部クラスの初期化に使う値を渡す際の工夫について書きます

元のコードと課題感

  • CQRSアーキテクチャを踏襲し、Commandにdb更新の責務を持たせる
  • ActiveModel::Attrubitesを使用してクラス内のプロパティを初期化
  • クラス実行時の引数にはitem_create_paramsを渡す
class Api::V1::ItemsController < BaseController
  before_action :authenticate_request
  
  def create
    item = Items::ItemCreateCommand.new(item_create_params).run
    render json: item
  end
  
  private

  def item_create_params
    item_params = params.require(:item).permit(:name, :quantity)
    item_params.merge(category_id: params[:category_id], user: current_user)
  end
end
class Items::ItemCreateCommand
  include CommandModule
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :name, :string
  attribute :quantity, :integer
  attribute :category_id, :string
  attribute :user

  validates :name, presence: true
  validates :quantity, presence: true
  validates :category_id, presence: true

  def run
    user.items.create!(
      name: name,
      qantity quantity,
      category_id: category_id
    )
  end
end

上記の実装でもpost処理は成功しますが、下記の課題を感じました。

  • controllerでの外部クラス実行時にActionController::Parametersが渡されており、一目で引数とプロパティの関係が分からない
  • 外部クラスにcurrent_user(sorcery提供のオブジェクト)を渡す際にstrogn parameterに直接mergeするやり方になっていて複雑

これらの課題を解決しつつ挙動を変えないために、分割代入での実装を解説します。
※typescriptでにた実装をそう呼ぶだけで、rubyでその呼ばれ方されてないっぽいです

values_at

配列やオブジェクトから値を順番に取り出すメソッドです。
これを使用して分割代入を再現します。

https://docs.ruby-lang.org/ja/latest/method/Hash/i/values_at.html

修正後のコード

  • ItemCreateCommandは変更なし
  • createメソッド内でvalues_atを使用して,パラメータ内容をメソッド内がスコープのプロパティに分割代入
  • Commandクラスへの引数を明示的に書くことで初期化プロパティと引数が一対一で対応付いている。見やすい。
  • current_userをstrong parameterにmergeせず引数で渡すことでitem_create_paramsの内容がシンプルになる
class Api::V1::ItemsController < BaseController
  before_action :authenticate_request
  
  def create
    name, quantity, category_id = item_create_params.values_at(:name, :quantity, :category_id)
    item = Items::ItemCreateCommand.new(
      name: name,
      quantity: quantity,
      category_id: category_id,
      user: current_user
    ).run
    
    render json: item
  end
  
  private

  def item_create_params
    params.require(:item).permit(:name, :quantity)
  end
end
class Items::ItemCreateCommand
  include CommandModule
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :name, :string
  attribute :quantity, :integer
  attribute :category_id, :string
  attribute :user

  validates :name, presence: true
  validates :quantity, presence: true
  validates :category_id, presence: true

  def run
    user.items.create!(
      name: name,
      qantity quantity,
      category_id: category_id
    )
  end
end

記述量が増えましたが、コードとしては見やすくわかりやすくなった感じが個人的にはあります。
値の渡し方とかはプロジェクトの規約で決まってくる所ですが、こんなのどうでしょう?って提案になれば嬉しいです。

Discussion