Reputation: 164679
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 validations
def subscribe!(args)
log that a subscription is attempted
begin
do the subscription
rescue errors
log the failure and reason
rethrow
end
log successful subscription
log other details about the subscription
SubscriptionValidationJob.perform_later( new_subscriber )
return new_subscriber
end
end
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?
Upvotes: 2
Views: 2666
Reputation: 101811
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 User
def born_on
Date.new(1989, 9, 10)
end
end
class UserDecorator < SimpleDelegator
def birth_year
born_on.year
end
end
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 SubscriptionService
attr_accessor :user, :list
def initialize(user, list)
@user = user
@list = list
end
def self.perform(user, list)
self.new(user, list).perform
end
def perform
@subscription = Subscription.new(user: @user, list: @list)
log_subscription_attempted
if @subscription.create
send_welcome_email
# ...
else
log_failure_reason
# ...
end
@subscription
end
private
def send_welcome_email
# ...
end
def log_subscription_attempted
# ...
end
def log_failure_reason
# ...
end
end
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 User
has_many :subscriptions
has_many :subscription_lists, through: :subscriptions
end
class Subscription
belongs_to :user
belongs_to :subscription_list
validates_uniqueness_of :user_id, scope: :subscription_list_id
end
# or topic
class SubscriptionList
has_many :subscriptions
has_many :users, through: :subscriptions
end
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.
Upvotes: 2