I'm developing a Rails 5 application following the "fat model / thin controller" pattern. As I'm adding things like logging and validation I'm finding my models are getting a bit too fat. For example, here's a sketch of what the method for subscribing to a list looks like...

class SubscriberList < ApplicationRecord# relationships and validationsdef subscribe!(args)log that a subscription is attemptedbegindo the subscriptionrescue errorslog the failure and reasonrethrowendlog successful subscriptionlog other details about the subscriptionSubscriptionValidationJob.perform_later( new_subscriber )return new_subscriberendend

Its increasingly getting in the way that logging and validation are welded to the act of subscribing. I understand I should solve this by moving logging and validation into decorators probably using draper.

I don't have much experience with decorators. My main concern is bugs due to code using an undecorated model when it should be using the decorated model. Or vice versa. The interfaces are the same, the changes are side-effects, so it would be hard to detect.

I'd be tempted to use decorates_association and decorates_finders liberally to avoid this, but Draper documentation says to avoid this...

Decorators are supposed to behave very much like the models they decorate, and for that reason it is very tempting to just decorate your objects at the start of your controller action and then use the decorators throughout. Don't.

However, Draper (and most Rails + Decorator articles I could find) seem to be focused on view functionality...

Because decorators are designed to be consumed by the view, you should only be accessing them there. Manipulate your models to get things ready, then decorate at the last minute, right before you render the view. This avoids many of the common pitfalls that arise from attempting to modify decorators (in particular, collection decorators) after creating them.

Unlike view functionality, where you have a controller to ensure the models its passing to the view are decorated, my decorators are for model functionality. The decorator is mostly for code organization and ease of testing, just about everything should be using the decorated model.

What are the best practices for using decorators to add to model functionality? Always use the decorated models? Something more radical like moving subscription and unsubscription into another class?

1

Best Answer


I don't think this is a good fit for decorators. In rails decorators mainly wrap model objects with presentation logic used in the view. They work as an extension of a single object which let you separate the different tasks of an object logically.

For example:

class Userdef born_onDate.new(1989, 9, 10)endendclass UserDecorator < SimpleDelegatordef birth_yearborn_on.yearendend

Decorators are not a good fit when it comes to proceedure like operations where several objects interact.

Rather what you should be looking at is the service objects pattern, in which you create single purpose objects that perform a single task:

class SubscriptionServiceattr_accessor :user, :listdef initialize(user, list)@user = user@list = listenddef self.perform(user, list)self.new(user, list).performenddef perform@subscription = Subscription.new(user: @user, list: @list)log_subscription_attemptedif @subscription.createsend_welcome_email# ...elselog_failure_reason# ...end@subscriptionendprivatedef send_welcome_email# ...enddef log_subscription_attempted# ...enddef log_failure_reason# ...endend

But you should also consider if you are composing your models correctly. In this example you would want to have three models interconnect as so:

class Userhas_many :subscriptionshas_many :subscription_lists, through: :subscriptionsendclass Subscriptionbelongs_to :userbelongs_to :subscription_listvalidates_uniqueness_of :user_id, scope: :subscription_list_idend# or topicclass SubscriptionList has_many :subscriptionshas_many :users, through: :subscriptionsend

Each model should handle one separate entity/resource in the application. So the SubscriptionList model for example should not be directly involved in subscribing a single user. If your models are getting two fat it might be a sign that you are shoehorning too much into a too small set of business logic objects or that the database design is poorly set up.