Manage Rails app business logic using ActiveInteraction gem

railsAugust 29, 2020Dotby Alkesh Ghorpade

While working with Rails applications, developers are concerned about adding business logic in the right place. Developers have to ensure that the business logic is well understood, maintainable and readable as the application grows.

To abstract and encapsulate the application business logic, developers use service classes or service objects, presenters, decorators, etc. But we have a few gems that make our life easier in maintaining our code.

Let us explore the ActiveInteraction gem.

ActiveInteraction

As per the GitHub repo,

ActiveInteraction gives you a place to put your business logic.
It also helps you write safer code by validating that your inputs conform to your expectations.

Installation

To use this gem, we need to add the following line to the Gemfile and execute the bundle install command.

gem 'active_interaction', '~> 5.1'

Basic Usage: Multiply two numbers

Let's say we want to multiply two numbers that handle the basic validations and return the result. We create a class MultiplyTwoNumbers which gets inherited from ActiveInteraction::Base.

class MultiplyTwoNumbers < ActiveInteraction::Base
  integer :a
  integer :b

  def execute
    a * b
  end
end

We defined our inputs a and b of integer type, which are the arguments that will be passed when calling this interaction. We added an execute method that returns the multiplication of a and b.

We can call the .run or .run! method on MultiplyTwoNumbers to execute it. The former will not raise any exception in case of any errors, but the errors can be accessed using the .errors.messages method. The latter will raise an exception in case of any error.

# .run method
outcome = MultiplyTwoNumbers.run(a: 1, b: "two")
outcome.valid?
# => false

outcome.errors.messages
# => {:b=>["is not a valid integer"]}

outcome = MultiplyTwoNumbers.run(a: 1, b: 2)
outcome.valid?
# => true

outcome.result
# => 2

# .run! method
MultiplyTwoNumbers.run!(a: 1, b: "two")
ActiveInteraction::InvalidInteractionError: B is not a valid integer

MultiplyTwoNumbers.run!(a: 1, b: 2)
# => 2
  1. We called the .run method and passed argument b as a string instead of an integer.

  2. We assigned the result to an outcome variable and used the .valid? to verify if there were any errors. In this case, .valid? returns false.

  3. To access the error messages, we can use .errors.messages.

  4. When we pass valid arguments the .valid? method returns true.

  5. We can fetch the output by using the .result method.

  6. When we call the .run! method and pass invalid arguments to it, it raises an ActiveInteraction::InvalidInteractionError and displays the error message.

  7. If we pass valid arguments .run! method directly returns the result.

Note: ActiveInteraction does not provide its validations. They are from ActiveModel. We can hence add custom validations in our interactions.

class MultiplyTwoNumbers < ActiveInteraction::Base
  integer :a
  integer :b

  validates :a, presence: true

  def execute
    a * b
  end
end

MultiplyTwoNumbers.run!(a: nil, b: 2)
ActiveInteraction::InvalidInteractionError: A is required

ActiveInteraction first checks for the type of input. After this check passes, ActiveModel validations come into the picture. When both these checks are green, execute method gets called.

Using ActiveInteraction in Rails

ActiveInteraction gels well with Rails. As per the gem, creating an /interactions directory is recommended under Rails /app folders.

As per the above example, MultiplyTwoNumbers is a single-purpose interaction. In real-life scenarios, we might have to deal with multiple interactions.

Let us imagine a scenario where a Rails application is supposed to import employee data from different companies and convert it into a standard format.

We might have to break the task into three steps:

  1. Fetch employee data from the company portal.

  2. Convert the employee data into a standard format, which our system can import.

  3. Import the standard format into our system.

Each step can be its own interaction class, and we create a parent interaction class that calls these three interactions in sequence.

# ImportEmployeesData: Parent Interaction
class ImportEmployeesData < ActiveInteraction::Base
  object :client,
         desc: "Third-party service from where we will fetch employees' data"

  def execute
    employees_data = compose(
      FetchEmployeesData,
      client: client
    )

    standard_data = compose(
      ConvertEmployeesDataIntoStandardFormat,
      data: employees_data
    )

    compose(
      ImportEmployees,
      data: standard_data
    )
  end
end

# FetchEmployeesData
class FetchEmployeesData < ActiveInteraction::Base
  object :client,
         desc: "Third-party service from where we will fetch employees' data"

  def execute
    # API call to the third-party service
    client.fetch_employees
  end
end

# ConvertEmployeesDataIntoStandardFormat
class ConvertEmployeesDataIntoStandardFormat < ActiveInteraction::Base
  object :data,
         desc: "Employee data that will be converted into the standard format"

  def execute
    # code that converts the employee data into the standard format
  end
end

# ImportEmployees
class ImportEmployees < ActiveInteraction::Base
  object :data,
         desc: "Standard data that will be imported into our system"

  def execute
    # code to import employees into our database
  end
end

As seen in the above example, we can call interactions within other interactions using the #compose method. If anyone of the interactions fails, an exception gets raised like the exception raised in .run! and the execution stops. If all the interactions get executed successfully, we return the result directly.

Note: We have a client object that refers to the third-party service. For example, let's assume a company employee's data exists on the 15Five portal. We fetch their data using 15Five APIs and create a class as below.

class FifteenFive
  def initialize
   # client id and client secret
  end

  def fetch_employees
   # API to fetch employees' data
  end
end

We can similarly create classes for different clients we have in our system. Finally, to import the employees, we can call the parent interaction as shown below:

ImportEmployeesData.run!(client: FifteenFive.new)

The advantage of using this gem is as follows:

  1. We don’t have to write code for validating the inputs. The gem is taking care of it.

  2. We can break down complex and huge business logic into smaller interactions.

  3. These smaller interactions can help us achieve the Single Responsibility Principle.

  4. Writing test cases for these interactions is much easier and faster.

  5. The smaller interactions can be reused at multiple places making our code DRY.

  6. Adding a new feature to the existing business logic becomes easier

  7. Finally, our controllers and models are skinny.

A few disadvantages or issues when using this gem are as follows:

  1. Sometimes, the model logic gets added in the interaction file we create, breaking the Rails coding convention. For e.g., when working with state machine AASM gem, if we choose to follow the ActiveInteraction pattern, we might have to shift our model AASM into its file.

    class Job
      include AASM
    
      aasm do
        state :sleeping, initial: true, before_enter: :do_something
        state :running, before_enter: Proc.new { do_something && notify_somebody }
        state :finished
    
        after_all_transitions :log_status_change
    
        event :run, after: :notify_somebody do
        before do
          log('Preparing to run')
        end
    
        transitions from: :sleeping, to: :running, after: Proc.new {|*args| set_process(*args) }
        transitions from: :running, to: :finished, after: LogRunTime
      end
    end

    Using the interaction gem

    class JobStateChange < ActiveInteraction::Base
      object :job
    
      def execute
        return if job.finished?
    
        job.log
    
        job.running!
    
        job.notify_somebody
      end
    end
  2. The ActiveInteraction gem has support for Formtastic and simple_form. But, unlike Rails form validations, error highlighting is different in the case of ActiveInteraction gem. To know more about the issue, please refer to this link.

  3. The gem has support for basic filters like Hash, Array and Interface. But support for the complex data structure is missing. Check this link to know more about the issue.

To know more about this gem and its features, check out this GitHub Repo.

Closing Remark

Could your team use some help with topics like this and others covered by ShakaCode's blog and open source? We specialize in optimizing Rails applications, especially those with advanced JavaScript frontends, like React. We can also help you optimize your CI processes with lower costs and faster, more reliable tests. Scraping web data and lowering infrastructure costs are two other areas of specialization. Feel free to reach out to ShakaCode's CEO, Justin Gordon, at justin@shakacode.com or schedule an appointment to discuss how ShakaCode can help your project!
Are you looking for a software development partner who can
develop modern, high-performance web apps and sites?
See what we've doneArrow right