问题
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?
回答1:
I don't really understand why do you need to define the associations again in the subclasses. However, I have a tip that you could use directly in your Subscription
model.
If you want to keep your model simple, and don't overload it with callbacks logic, you can create a callback class to wrap all the logic that will be used by the model.
In order to do that, you need to create a class, for example:
class SubscriptionCallbacks
def self.after_create(subscription)
log_sub(subscription)
end
def self.after_destroy(subscription)
log_unsub_reason(subscription)
end
end
Then in Subscription
model:
class Subscription < ApplicationRecord
belongs_to :list
belongs_to :subscriber
after_destroy SubscriptionCallbacks
after_create SubscriptionCallbacks
end
That way, your model stand clean and you can destroy
a subscription and apply all custom logic without using a service.
UPDATE
Specifically, what I don't understand is why are you making Single Table Inheritance on three models just to add callbacks to one of them. The way you wrote your question, for the three subclasses you override the associations to use the subclasses created. Is that really necessary? I think that no, because what you want to achieve is just refactor your service as callbacks in order to use destroy
and destroy_all
directly in the Subscription
model, I take that from here:
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.
Maybe with conditional callbacks is enough, or even just normal callbacks on your Subscription
model.
I don't know how the real code is wrote, but I found tricky to use Single Table Inheritance just to add callbacks. That doesn't make your models "simple and flexible".
UPDATE 2
In a callback class, you define methods with the name of the callback that you want to implement, and pass the subscription
as a parameter. Inside that methods, you can create all the logic that you want. For example (assuming that you will use different logic given a type
attribute):
class SubscriptionCallbacks
def after_create(subscription)
if subscription.type == 'foo'
log_foo_sub(subscription)
elsif subscription.type == 'bar'
log_bar_sub(subscription)
end
end
private
def log_foo_sub(subscription)
# Here will live all the logic of the callback for subscription of foo type
end
def log_bar_sub(subscription)
# Here will live all the logic of the callback for subscription of bar type
end
end
This could be a lot of logic that will not be wrote on Subscription
model. You can use destroy
and destroy_all
as usual, and if a type of subscription is not defined in the if else
, then nothing will happen.
All the logic of callbacks will be wrapped in a callback class
, and the only peace of code that you will add to the subscription
model will be:
class Subscription < ApplicationRecord
belongs_to :list
belongs_to :subscriber
after_create SubscriptionCallbacks.new
end
来源:https://stackoverflow.com/questions/52434318/consistently-using-associated-model-subclasses