https://github.com/collectiveidea/interactor I love this little nifty library which greatly encapsulates my logic and allows me to clear my Controllers.
Interactors are a form of
Service Objects (others may call an interactor a
Use Case Object) but you don’t have to define the interfaces yourself and a way of input, a way of invalidating the action as well as a way of checking if an action was a success or not are already there for you.
There are few things that are characteristic for that kind of objects:
- accepting input,
- performing some action,
- returning result.
If you decide to implement all of these three things you have quite a nut to crack and you will end up with something similar what Interactors gives you out of the box or something worse because you don’t have time for serious design. Of course, in some cases, very simple service object are enough but if your application’s business logic contains a few processes, you might consider something more powerful. That’s why I would like to show you this library because it comes with many features and yet it is light and simple.
Interactors accepts hash as an input which is great because it provides readable and idiomatic way of passing your data.
class UserController < ApplicationController def create result = RegisterUser.call(params: user_params) # response end private def user_params params.require(:user).permit(:email, :password) end end
Internally the library converts it to
Context object which is available inside interactor under
context attribute. This is a way of communication between interactors as well as with interactor itself but that’s for later.
Performing an action
Every interactor has single method
call which starts its piece of logic. Call method must return
…because a context is an object which tells us if an action was performed successfully or not. Context has two methods for this -
failure?, depends on our preferences. By default context is successful until you fail it inside your interactor.
class RegisterUser include Interactor def call user = User.new(context.params) if user.save context.user = user # context.success? returns true else context.fail! # context.success? returns false end end end
Knowing this, your controller might look like this:
class UserController < ApplicationController def create result = RegisterUser.call(params: user_params) if result.success? # 200 else # 422 end end private def user_params params.require(:user).permit(:email, :password) end end
That was only simple example and frequently some processes require more steps rather than just creation. In our example we would like to send a confirmation email after we create a user. Logic for that we could put next to user creation, however this process is getting more complex and we want to keep our logic divided by its responsibility and keep it easy to test. So, we can define two steps in this registration process:
- creating a user,
- sending him a confirmation email.
I’m going to put each step in its separate interactor -
class CreateUser include Interactor def call user = User.new(context.params) if user.save context.user = user # context.success? returns true else context.fail! # context.success? returns false end end end class SendConfirmationEmail include Interactor def call UserMailer.confirmation(context.user).deliver! end end
Now, we need a way of running both of these interactors in order to fulfill the process of registration. However, we don’t need to create any logic ourselves.
Library defines for us a special interactor which is able to run list of interactors in a sequential manner. That special interactor is called
Organizer and in our case it would look like this:
class RegisterUser include Interactor::Organizer organize CreateUser, SendConfirmationEmail end
Organizer call each interactor and passes context from one to the next. If any interactor fail in the middle of whole process, organizer stops and it is marked as failed as well.
As you can see, we expanded our registration process but our controller didn’t change and its job is only to call interactor and handle its result. Thanks to interactors, controllers stay clean, simple and easy to test.
I hope this post was informative for you and my bad English grammar didn’t make you cry.