Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
menu search
person
Welcome To Ask or Share your Answers For Others

Categories

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

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
123 views
Welcome To Ask or Share your Answers For Others

1 Answer

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

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
...