-
Notifications
You must be signed in to change notification settings - Fork 1
DSL
Interactify provides a DSL on top of interactor-contracts for commonly used patterns in interactor chains.
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.
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
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.