Ruby
Rails
Gem

RailsでActive Interactionを使う


はじめに


注意書き

未経験初心者Rubyistがメモ同様の記事を書いているものなので間違っている部分やもっといい書き方の出来る部分があります(確信してます).

がっつりまさかり投げてください.勉強させていただきます.


何故記事をかいたのか

ActiveInteractionを勧められて使ってみて良さをひしひしと実感している今日此の頃,ふとQiitaでActiveInteractionの記事を検索してみると全く無かったのでこれは勧める他ねえと啓示を頂いたため.


ActiveInteractionとは

Github

RubyGems

commandパターンを利用してコードをめちゃくちゃスッキリさせられる.すごい

名前空間を使えるのでcontoroller/function の形でファイルを管理できる.見やすい.すごい1

と,言うのをなんとなく自分で意訳しまくるとapp/contoroller/concerns に入る予定だったmoduleを別のところ(app/interactions/)に,よりキレイに置いておけるもの


メリット


  • コードの可読性がめちゃくちゃ上がる(個人的所感)

  • interaction毎にテストが書けるのでテストが書きやすい

  • 静的型付けになっているのでエラーを見つけやすい(後述)


使ってみる

取り敢えず使ってみないことには(見せないことには)理解が曖昧なままだし何より自分で使ってみようという気持ちにならないので使ってみます.


interactionを使わない状態

それっぽく書いた動かないコードなので注意


controller/hoge_controller.rb


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を使う


忙しい人の為の概要


  1. gemfileにgem 'active_interaction', '~> 3.5', '>= 3.5.2'を記述

  2. bundle install


  3. app/interactions/Models/FileName.rbを新しく作る

静的型付けする時の型一覧


app/interactions/Hoges/CreateData.rb

module Hoges

class CreateData < ActiveInteraction::Base
string :url #静的型付けで引数のデータ型を定義

def execute
#ここにコードを書く
str = 'hello, world'
str
end
end
end


4 app/controllers/hoges_controller.rbに記述


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を継承させます.


app/interactions/Hoges/ScrapingData.rb

module Hoges

class ScrapingData < ActiveInteraction::Base

def execute
end
end
end



Interactionに渡すデータを設定する

ActiveInteractionのファイルは静的型付けで引数を取ります.

これによってデータ型が違う時点でエラーが出せるのでバグを発見しやすくなります.

今回scrapingするのに渡したいデータはparamsの中にあるurlです.

urlはstring型なので以下のように記述します.


app/interactions/Hoges/ScrapingData.rb

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)の形で呼び出しをします.


controllers/hoges_controller.rb

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すごく読みやすかったのでお薦めです.





  1. commandパターンとnamespaceに関しては用法,理解が違う可能性があります. 



  2. コードはRubyGemsから拝借