Rails で ActiveInteraction を使ってModelのビジネスロジックを切り出す
経緯
Worker から動かすためのビジネスロジックが、Worker(task) にもあるし、Model の中にもある状態でした。
少なくともModel からは切り離したかったのですが、Workerが肥大化すると視認性が落ちるため、役割を分割できるようにしておきたいと思いました。記述は他のModelが見える has_many の近く(つまりModel)に置く方が関係性を認識しやすいので、まず共通部分は Concern に切り出してみました。
その後、Concern にも切り出せないコマンド系の処理が Model に残りました。
例えば #run, #execute, #start, #stop などです。
明らかに Model に存在するのは違和感があると思い、Service 層へ切り出す方向で検討を開始しました。
ちなみに Controller と Worker では、同じモデルを別のロジックで動かします。
Worker は一連の処理を連続してバックグラウンドで行いますが、 Controller は ブラウザからの設定・管理・レポート表示を担います。
アプリケーション全体でみると、Controller と Worker に書かれているロジックが異なるため、Controller / Worker / Model の3ヵ所にビジネスロジックを書いている状態となっていました。
しかしここでは、一連のバックグラウンド処理を担うWorkerのビジネスロジックに焦点をあてています。( Controllerは、まだMVCで収まる規模であるため。)
検討
app/services 配下に Service 層を置く
まずはService層を検討しました。ビジネスロジックを一ヵ所に集められないか?と思ってのことです。
しかし、アンチパターンでもある、という見解も目にしたため見合わせました。
Service Objectがアンチパターンである理由とよりよい代替手段(翻訳)
https://techracho.bpsinc.jp/hachi8833/2018_04_16/55130
何が良いのか、設計としてどうあるべきかは、常に賛否あるみたいですが。
app/commands 配下に 独自の Command パターンを置く
Gemがなければ自分で実装していたと思います。Commandパターンを考えたのは Model に残った処理がCommand系だったからです。それ以上の意味は特にありませんが、拡張性に耐えられるかどうかは不安でした。
Active Decorator みたいなGemを探していれる
過去、Active Decoratorを入れたときに、Modelに持たせたくない責務を、View側から見るとModelの一部のように使える点に利便性を感じました。
同じように扱いやすいGemはないか?と思い、探して見つけたなかで、
Active Interaction が Command/Service層に近かったので検討しました。
なお、名前は Interaction ですが、実装は Interaction か?と言われると違い、Command だと思う(公式にもそう書いてある)ので、Command の gem だと思って入れました。
Active Interaction の導入
GitHub
https://github.com/AaronLasseigne/active_interaction
基本的にはREADMEを読むことになります。
gem 'active_interaction'
配置
app/interactions が推奨されているので、そこに置きました。
さらに Model ごとに directory を分けて、Model 別の interaction を置くことにしました。
- app/
- interactions/
- order/
- processor.rb
- processor/
- open.rb
- submit.rb
- send_keys.rb
:
ルール
interractions/[model名]のdirectory 配下は、
- Model の外部化である
- 処理系は Processor である
- Model はProcessorの内部構造を知らない
- ProcessorはModelを把握している
というルールにしました。
実現したかったのは、様々な実行を集めることではなく、Model自身が簡素化されることです。
Commandをここに置くと決めると、各所の処理が集まってしまいます。
手段の目的化を避けるため、「あらゆるCommandを集める」ことはしないように決めました。いまControllerの処理を持ち込まないことにしたのも、このためです。
Model からの呼び出し
Order は type を持っているため、子クラスが存在します。
子クラスは run を持たず、typeの違いは Order::Processor が処理することにしました。
class Order < ApplicationRecord
def run
Order::Processor.run(order: self)
end
end
class Order::Open < Order
end
class Order::Submit < Order
end
class Order::Processor < ActiveInteraction::Base
object :order # class チェック. Order を継承している子クラスもOK
def execute
order.class ... クラス別の呼び出し
end
end
class Order::Processor::Open < Order::Processor
def execute
begin
if order.path.present? then
:
rescue => e
:
end
end
end
これによりモデルから Order::Open
のインスタンスを run すると、 Order::Processor::Open.run
を呼び出して、ActiveInteraction によって #execute が呼ばれる、という流れになりました。
呼び出し
今回、Worker (いまはtask)からは、Modelの一部として扱える状態を保ちました。
たとえば
steps.each do |order|
order.run
end
という構造はキープしました。ここをCommand呼び出しにすると、
steps.each do |o|
OrderProcessor.run( order: o )
end
となりますが、これではWorkerがビジネスロジックごとに最適なコマンドを把握する必要があります。ビジネスロジックを外部化した意味が薄れるためCommandの直接呼び出しは避けました。
まとめ
ActiveInteraction を使うことで、Model のなかに冗長化していたビジネスロジックをスッキリさせることができました。
とくにエラーハンドリングや条件に応じた切り分けは、単純な処理にも関わらず、丁寧に書くと冗長になります。この部分が外部化されたことは視認性を高めました。
また、ActiveInteraction にルールを設けたことで、Modelとの役割分担が分かりやすくなりました。Model には状態と簡素化された振る舞いだけを書くことができました。
また、Worker(ここではtask)からみるとModelの一部に見えるため、Rails の Model から外れることもありませんでした。
ただし、ルール無しに利用するとリスクがあるとも感じました。
直接Commandを呼び出すだけの、Railsから外れた書き方になりやすそうです。特にWorkerやControllerから呼び出す場合は、単純なコマンド呼び出しの連鎖になるリスクも感じています。
ControllerやWorker内のビジネスロジックを分けていくときには、別の分かりやすいのルールが必要かもしれません。
Author And Source
この問題について(Rails で ActiveInteraction を使ってModelのビジネスロジックを切り出す), 我々は、より多くの情報をここで見つけました https://qiita.com/tettuan/items/aa33123a18f9a473b683著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .