Skip to content
Mark Burns edited this page Jul 3, 2024 · 10 revisions

Interactify provides a DSL on top of interactor-contracts for commonly used patterns in interactor chains.

each

Before Interactify, our code often looked like this:

class BatchUpdateWidgetStatistics
  include Interactor::Organizer
  
  organize GatherStatistics, LoadWidgets, UpdateEachWidget
end

class UpdateEachWidget
  include Interactor

  def call
    context.widgets.each do |widget|
      UpdateWidget.call(widget: widget, statistics: statistics)
    end
  end
end

This approach led to wrapping classes like UpdateEachWidget, which were both detail-sparse and repetitive, merely abstracting a loop.

With Interactify, this is simplified:

class BatchUpdateWidgetStatistics
  include Interactify
  
  organize GatherStatistics, 
    LoadWidgets, 
    each(:widgets, UpdateWidget)
end

This reduction in code not only eases maintenance but also helps maintain a high-level focus, simplifying navigation to the exact details without the need to jump across files.

Contexts

Underneath its user-friendly abstraction, Interactify automatically generates a new class encapsulating the specified functionality. For instance, in the each example mentioned earlier, a class like BatchUpdateWidgetStatistics::EachWidget_7382 might be created. This auto-generated class receives the entire context from the parent organizer, which includes:

- `context.widgets` — the collection being iterated over
- `context.widget` — the current item in the collection
- `context.widget_index` — the index of the current item

This setup may initially seem confusing as it provides access to both the entire collection and individual elements simultaneously within the interactor. However, this is consistent with Ruby’s native iteration constructs. Consider the following Ruby code:

widgets.each_with_index do |widget, widget_index|
  # Inside the block, you have access to 
  # `widgets`, 
  # `widget`, and `widget_index`
  # as well as any other items in the context.
end

if

Handling conditional logic often leads to trivial switching between code paths:

class HandleWidgetPurchase
  include Interactor::Organizer

  organize \
    TakePayment, 
    UpdateUserStatistics, 
    UpdateWidgetStatistics,
    NotifyUser
end

class NotifyUser
  include Interactor

  def call
    if context.user.app_notifications_enabled
      context = SendAppNotification.call(context)
      UpdateAppNotificationStatistics.call(context)
    else
      SendEmailNotification.call(context)
    end
  end
end

Interactify streamlines this with:

class HandleWidgetPurchase
  include Interactify
  expect :user

  organize \
    TakePayment, 
    UpdateUserStatistics, 
    UpdateWidgetStatistics,
    -> { _1.app_notification = _1.user.app_notifications_enabled },
    self.if(:app_notification, 
      then: [SendAppNotification, UpdateAppNotificationStatistics],
      else: SendEmailNotification
    )
end

While this approach trades some visual clarity for a higher-level overview, it retains the benefits of isolated and testable interactor chains. Auto-generated class names like HandleWidgetPurchase::IfAppNotification_4510 can be assigned to constants for better testability:

class HandleWidgetPurchase
  include Interactify
  expect :user

  NotifyUser = self.if(:app_notification, 
    then: [SendAppNotification, UpdateAppNotificationStatistics]
    else: SendEmailNotification
  )

  organize \
    TakePayment, 
    UpdateUserStatistics, 
    UpdateWidgetStatistics,
    -> { _1.app_notification = _1.user.app_notifications_enabled },
    NotifyUser

end

This modification not only promotes cleaner, more maintainable code but also enhances the readability and testability of conditional flows in business logic.

Clone this wiki locally