I have a situation where I have basic models that I want to add business logic to. For example, I might have something like this.
class List < ApplicationRecord
has_many :subscriptions
has_many :subscribers, though: :subscriptions
end
class Subscriber < ApplicationRecord
has_many :subscriptions
has_many :lists, through: :subscriptions
end
class Subscription < ApplicationRecord
belongs_to :list
belongs_to :subscriber
end
Subscribing and unsubscribing is easy via the normal association methods.
# Subscribe
list.subscriptions.create(
subscriber: subscriber
)
# Unsubscribe
list.subscriptions.destroy(subscription)
# Unsub from all lists
subscriber.subscriptions.destroy_all
But there's logging and tracking and metrics and hooks and other business logic. I could do this with callbacks. However I'd like to keep the basic models simple and flexible. My desire is to separate the core functionality from the extra business logic. Right now this is to simplify testing. Eventually I'll need to add two different sets of business logic on top of the same core.
Currently I'm using a service object to wrap common actions with all the current business logic. Here's a simple example, there's a lot more.
class SubscriptionManager
def subscribe(list, subscriber)
list.subscriptions.create( subscriber: subscriber )
log_sub(subscription)
end
def unsubscribe(subscription)
subscription.list.subscriptions.destroy(subscription)
log_unsub_reason(subscription)
end
def unsubscribe_all(subscriber)
subscriber.subscriptions.each do |subscription|
unsubscribe(subscription)
end
subscriber.lists.reset
subscriber.subscriptions.reset
end
end
But I'm finding it increasingly awkward. I can't use the natural subscriber.subscriptions.destroy_all
, for example, but must be careful to go through the SubscriptionManager methods instead. Here's another example where this system caused a hard to find bug.
I'm thinking about eliminating the SubscriptionManager and instead writing subclasses of the models which have the extra logic in hooks.
class ManagedList < List
has_many :subscriptions, class_name: "ManagedSubscription"
has_many :subscribers, though: :subscriptions, class_name: "ManagedSubscriber"
end
class ManagedSubscriber < Subscriber
has_many :subscriptions, class_name: "ManagedSubscription"
has_many :lists, through: :subscriptions, class_Name: "ManagedList"
end
class ManagedSubscription < Subscription
belongs_to :list, class_name: "ManagedList"
belongs_to :subscriber, class_name: "ManagedSubscriber"
after_create: :log_sub
after_destroy: :log_unsub
end
The problem is I'm finding I have to duplicate all the associations to guarantee that Managed objects are associated to other Managed objects.
Is there a better and less redundant way?
See Question&Answers more detail:os