はじめに
注意書き
未経験初心者Rubyistがメモ同様の記事を書いているものなので間違っている部分やもっといい書き方の出来る部分があります(確信してます).
がっつりまさかり投げてください.勉強させていただきます.
何故記事をかいたのか
ActiveInteractionを勧められて使ってみて良さをひしひしと実感している今日此の頃,ふとQiitaでActiveInteractionの記事を検索してみると全く無かったのでこれは勧める他ねえと啓示を頂いたため.
ActiveInteractionとは
commandパターンを利用してコードをめちゃくちゃスッキリさせられる.すごい
名前空間を使えるのでcontroller/function の形でファイルを管理できる.見やすい.すごい1
と,言うのをなんとなく自分で意訳しまくるとapp/controller/concerns に入る予定だったmoduleを別のところ(app/interactions/)に,よりキレイに置いておけるもの
メリット
- コードの可読性がめちゃくちゃ上がる(個人的所感)
 - interaction毎にテストが書けるのでテストが書きやすい
 - 静的型付けになっているのでエラーを見つけやすい(後述)
 
使ってみる
取り敢えず使ってみないことには(見せないことには)理解が曖昧なままだし何より自分で使ってみようという気持ちにならないので使ってみます.
interactionを使わない状態
それっぽく書いた動かないコードなので注意
def create
  data = Data.new(data_params)
  scraped_title = params[:data][:url].scraping(title) # titleをscrapingする
  if params[:data][:title].nil?
    data.title = scraped_title # titleが空ならscrapingしたtitleを使う
  end
  if data.save
    redirect_to root_path
    flash[:notice] = '登録しました!'
  end
end
createmethodの中にscrapingするコードが入っているので機能を複数(scrapingする,登録する)動かすことになってしまいます.
このscrapingの部分,createじゃないのになって思いませんか?分割したい気持ちになりませんか?
ActiveInteractionの為になってください.
使いたい気持ちになったのでActiveInteractionで実際に綺麗に整えようと思います.
ActiveInteractionを使う
忙しい人の為の概要
- gemfileに
gem 'active_interaction', '~> 3.5', '>= 3.5.2'を記述 bundle install- 
app/interactions/Models/FileName.rbを新しく作る 
module Hoges
  class CreateData < ActiveInteraction::Base
    string :url #静的型付けで引数のデータ型を定義
    def execute
      #ここにコードを書く
      str = 'hello, world'
      str
    end
  end
end
4 app/controllers/hoges_controller.rbに記述
def create
  greet = ::Hoges::CreateData.run(url: params[:data][:url])
  greet.result # => 'hello, world'
  # 戻り値はresultに収納されている
  # modelのようにgreet.valid? が使える -> interactionにvalidateを記述したときはerrorsに格納される
end
コードをいい感じにActiveInteractionでまとめる(初心者向け)
初心者向けなのでどういう風に考えていくか流れを踏まえて使ってみます.
前準備
gemfileにgem 'active_interaction', '~> 3.5', '>= 3.5.2'を記述します.2
bundle installを走らせます.
/app/interactionsフォルダを追加しておきます.ここにinteractionのコードを記述していきます.
Interaction用のファイルを作る
まずどこからどこまでを区切るのか,それはどういう機能なのかを考えてその機能を記述するファイルを作りましょう.
今回はscrapingを行う機能を分割したいのでscraping_data.rbとします.
/app/interactions以下に/Hoges/scraping_data.rb または /scraping_data.rb を追加します.どちらでも大丈夫ですが今後modelが増えることも考えてHoges/以下にファイルを作ることにします.
Interactionの雛形を記述する
Interactionを使うためにclassにはActiveInteraction::Baseを継承させます.
module Hoges
  class ScrapingData < ActiveInteraction::Base
    def execute
    end
  end
end
Interactionに渡すデータを設定する
ActiveInteractionのファイルは静的型付けで引数を取ります.
これによってデータ型が違う時点でエラーが出せるのでバグを発見しやすくなります.
今回scrapingするのに渡したいデータはparamsの中にあるurlです.
urlはstring型なので以下のように記述します.
module Hoges
  class ScrapingData < ActiveInteraction::Base
    string :url # ここを追加
    def execute
    end
  end
end
executeの中に処理を書いていく
今回はcreateの中に書いていたこちらのコードを記述します.
  scraped_title = params[:data][:url].scraping(title) # titleをscrapingする
executeの中に入れるときは型付けをしたurlを用います.
def execute
	  scraped_title = url.scraping(title)
	  scraped_title # 戻り値にscraped_titleが来るようにする
end
controllerでinteractionを使う
controller部分のscrapingの処理をinteractionに置き換えます.
NameSpace.run(data: hogehoge)の形で呼び出しをします.
def create
  scraped_data = ::Hoges::ScrapingData.run(url: params[:data][:url])
  if params[:data][:title].nil?
    data.title = scraped_data.result # resultで戻り値を取得
  end
  if data.save
    redirect_to root_path
    flash[:notice] = '登録しました!'
  end
end
小話
interaction classとresultメソッド
Interactionを利用したときに帰ってくる戻り値はInteraction特有のclassです.
scraped_data.class #=> Hoges::ScrapingData
と返ってきます.
この状態から指定した戻り値(今回ならscraped_title)は出力出来ないので,resultmethodを用います.
scraped_data.resultとすることでscraped_titleを取得できます.
また,resultを付けていない状態からは
scraped_data.valid?でvalidateチェックが出来,
scraped_data.errors.full_messagesでエラーメッセージを取得できます.
エラーのあるときはresultはnilになっているので意識しておくといいかもしれません.
interaction内でvalidateを確認する
interactionの中で先にmodel内に記述したvalidateを確認して動かす事は出来ません.
というか,saveの段階でvalidateを確認しているのでsave前でのvalidateチェックは自動で行われないです.(そのはず)
でもvalidateのチェックは行いたい,そんな場合にはinteraction内にmodelと同じ書き方でvalidatesを記述出来ます.
その際のエラーメッセージは型付けした引数がattributesの中に入るので注意してください
例:url_dataという引数にpresence: trueのvalidatesをつける
validates :url_data, presence: true
# ---
>  ::Hoge.run(url_data: '').errors.full_messages
# => ["Url dataを入力してください"]
引数に違う型を入れた時の挙動
もし間違えて入れてしまった時,どういう挙動になるのでしょうか.
不肖わたくしが少しだけ調査してみました.
::Recipes::CreateRecipe.run(recipe_url: params[:url])
こちらのInteractionはString型を取ります.
ここにintegerを入れてみましょう
> ::Recipes::CreateRecipe.run(recipe_url: 00)
> @details={:recipe_url=>[{:error=>:invalid_type, :type=>"translation missing: ja.active_interaction.types.string"}]}, @messages={:recipe_url=>["translation missing: ja.active_interaction.errors.models.recipes/create_recipe.attributes.recipe_url.invalid_type"]}>
長いですがおわかりいただけるでしょうか.部分でいうとerrorsの部分です
i18nが入っているので翻訳データがありませんというエラーになっていますが,要はtypeがstringであるはずだというエラーが表示される予定でした.
もちろんvalid?はfalseになります.
今回は無理やりintegerを入れたので型エラーが起こりましたが,paramsは基本stringで返ってくるのでparamsを使っているうちはそうそうintegerが入ってエラーになった,という状況は起こりにくいかと思われます.
ですが万が一の事を考えてエラー処理を記述しておくのも手だと思います(今回ならi18nのlocaleを追加で記述しておくだけで十分でしょう)
引数にちゃんと値を入れているのにデータが上手く扱われない
私がこの事案に引っかかり,少し詰まりました.
ActiveInteractionは名前からデータを推測します.
record :userと書けばUser modelだと推測し,record :tagと書けばtag modelだと推測します.
この時にrecord :userと型付けをしてrun(user: Tag.first)と入れようとしてもエラーになってしまいます.
modelと同じ名前や他のmodelと被らない名前を付けたほうがこういったバグには引っかからずに済むかと思います.
まだしっかりと検証は出来ていない(後日追記予定)ですが,validatesで追加されたerrorもcontrollerの方でerrors.full_messagesを用いて表示させる必要があるかと思われます.
デバッグをする
rails consoleにそのままNameSpace.run()で動かすことが出来ます.これがすごい助かる.楽ちん
今回は可読性の為に変数に入れてからresultとかしてますが,別にrun().resultでも全然大丈夫なのでパッと戻り値確認したい時とかにはお薦めです.
まとめ
ActiveInteraction はすごい
あまりActiveInteractionの記事見かけないので是非こういう書き方しました,とかこういう使いみちもありました,みたいなの皆出してほしいです……🙏
訂正や追記もお待ちしてます.
Rubygemsを見ると分かる通りrails依存ではないのでRubyだけでも使えます!是非気軽に使ってみてください!
参考
ActiveInteraction のGithub
READMEのDocumentationすごく読みやすかったのでお薦めです.