Sunfox


Actor

I created a Ruby gem called Actor. It helps you harmonize your application’s service objects.

Service Objects?

Service objects are a way of making your web applications grow in complexity while keeping your controllers and models thin. For that you’d create classes that represent your actions, for example, CreateComment, PlaceOrder, DestroyUser, etc.

At the start, these actions might hold very little code and look a lot like a regular controller. But as your app grows, you’ll realize that something simple like creating a comment can turn into a serie of actions:

  • creating a record in the database,
  • checking the contents for spam or duplicates,
  • updating counters and cache keys,
  • sending email or Slack notifications,
  • logging the action to another system,
  • triggering a job to translate the contents,
  • etc.

These actions deserve a way of being represented as first-class citizens in your application. That’s where actor comes into play!

Creating an actor

With the Actor gem, your business logic is represented by a class that starts with a verb. It declares what arguments it accepts and what argument it returns. Here’s an example of creating a comment in a Ruby app:

# app/actors/create_comment.rb
class CreateComment < Actor
  input :author
  input :text
  output :comment

  def call
    self.comment = Comment.create(author: author, text: text)
  end
end

Now, if placing a comment takes more steps, here’s how it could look like thanks to the way Actor chains the output of the previous actors into the next when using play:

class PlaceComment < Actor
  play CreateComment,
       CheckCommentSpam,
       NotifyUserOfComment,
       UpdateCommentCounters,
       ClearCommentCacheKey,
       NotifyNewCommentOnSlack
end

If an actor along the way encounters an error, it can call fail! which halts the chain and allows previous successful actors a chance to rollback their changes.

Using an actor

Actor doesn’t depend on Rails, but Rails controllers are a good example of where you’d use your actors:

class CommentsController < ApplicationController
  before_action :authenticate_user!

  def create
    authorize

    result = PlaceComment.result(author: current_user, text: params[:text])
    if result.success?
      redirect_to comment_path(result.comment)
    else
      redirect_to :back, alert: result.error
    end
  end
end

Now your controller is only responsible for calling an actor, testing its success and deciding how to handle it. Your business logic can now be tested by itself and be called from a console, a job, or an another controller.

Influenced by Interactor

Actor is similar to the Interactor gem that I have been using on a number of different projects at Cults and KissKissBankBank and love the way it has helped give a common interface for all our services.

If you’re coming from Interactor, know that Actor:

  • Allows defaults, type checking, and conditions on inputs.
  • Delegates methods on the context: foo vs context.foo, self.foo = vs context.foo =, fail! vs context.fail!.
  • Has a shorter setup syntax: inherit with < Actor vs having to include Interactor and include Interactor::Organizer.
  • Allows lambdas when using play.
  • Allows calling play with conditions, which can also be used to trigger an early success.
  • Defaults to raising errors on failures: actor uses call and result instead of call! and call. This way, the default is to raise an error and failures are not hidden away because you forgot to use !.
  • Encourages you to document all arguments with input and output.
  • Does not hide errors when an actor fails inside another actor.

All this led me to create my own version, which has been smoothly running in production at Cults.

Go play with it!

Install it by adding the following lines to your application’s Gemfile:

# Composable service objects
gem 'service_actor'

Read more about the gem on Actor’s readme on GitHub. Don’t hesitate to send your questions, star the project, submit ideas as issues.

👨🏻‍🦰 Sunny Ripert

est un développeur web vivant à ParisContactArchives

Textes et contenus sous licence Creative Commons.