I’m going to be giving a talk at RailsConf 2014 on this topic, and I’ll happily give credit to those who help with this discussion.
I’m organizing my thoughts on the various options for improving Rails
code via:
- Concerns
- View Helpers
- Draper Decorators
- Page Objects, POROs linking a controller method to one or more views.
- Service Objects, POROS linking a controller method to a complicated model
interaction.
I’m going to take crack at how I’d explain this, and I’d like to get some
constructive feedback.
Overall, my decision making of which approach is guided by the following
principles:
- DRY is always right. If the code is duplicated, it’s worth figuring out a way
to eliminate the duplication. Code duplication usually counts as at least 2
or lines. A single line of duplicated code does not always justify
de-duplication. 2 lines probably. 3 lines definitely. - Simplicity is better than complexity, and I’m not going to create an
independent class or module unless it contains at least 3 methods. - Easier to test is always a bonus, but it’s not the first reason I refactor.
- What’s going to be easiest place for me to find the relevant code later, so
that I can reuse it? It’s usually easier to look in a well named concerns or
a Draper decorator rather than some PORO.
Concerns
Applicable Situation
A giant model file with corresponding giant spec file.
Solution
The giant model can safely and easily be broken down into logical chunks (concerns), each with a
correspondingly smaller spec file per concern.
Advantages
-
Ease and safety of refactoring. Concerns are a great first refactoring step
because using concerns involves simply moving the methods and tests into
separate files. And code accessing those methods does not need to change. - A core part of Rails 4, so one can expect familiarity among Rails developers.
- Simplicity. It’s just a code organization that makes it easier to navigate to
the right source and test file. - Can DRY up code when a concern is shared among multiple models.
Disadvantages
- The model object does not become more cohesive. The code is just better
organized. In other words, there’s no real changes to the API of the model. - For a complex operation involving many different methods, and possibly
multiple models, a Service Object, aka PORO, better groups the logic. - If the code view-centric, and relies on helpers, then a Decorator is a better
choice.
View Helpers
Applicable Situation
- Duplicated code among multiple views, such as used to create some html.
- Typically non-model-specific code, although Rails by default will generate
a view helper per model. Consider using a decorator if the view code is model
specific. - If you’ve got too much code in a view used to create some complicated HTML,
and maybe that code is duplicated among multiple views, then a view helper
can be the perfect answer.
Solution
Create a view helper method, either in the ApplicationHelper if the method applies
for many models, or in a specific view helper matching up to a model. View
helpers are simply modules that get mixed into the context of the view, and they
have access to any of the methods available in the view, such as request
,
params
, url helpers, etc. These are good for very simple amounts of code.
Advantages
- Familiar Rails technique for code organization.
- Simple, effective.
- Effective for view code that is orthogonal to any given model (can’t use
Draper for that).
Disadvantages
- If you find that you have to pass the model object into each of your view
helper methods, then that suggests that a model decorator is a better place
for the code. - If you find that you have several methods closely linked together or a very
long method, then that suggests using some sort of PORO. - Awkward to use view helper methods outside of views. So if you want the
methods available in controllers, models, etc., then a decorator typically
works better.
Draper Decorator
Applicable Situations
- Code in model that really only applies to views and presentation.
- Calculations done in view on model object.
- Code in view helper that is more closely aligned with the model.
- Code is that seems relatively general as opposed to clearly useful in only
one view.
Solution
Create a Draper decorator for the model. Move the code from the view or model
into the decorator. Very similar to a concern except:
- Decorator methods should be presentation/view specific.
- Decorators lack the “Concern” support for “included”, such as
has_many
, and
scope
, as well as for static methods. - If method feels relatively generic for views, but specific for a given model,
then the decorator is a good place to put the method.
Advantages
- Relatively simple way to add functionality to the model that only applies to the
connection of the model to the view. - Can separate view specific code from the model.
- Methods added to the model via the decorator have access to both view helper
methods as well as the model. - It’s super easy to automatically convert the retrieved model object into it’s
decorated instance, either manually or automatically, at the controller
layer. - Simple to find the decorator methods, such as using for verification in
feature specs.
Disadvantages
- Shares similar problems with concerns in that the model object is still
getting fatter. - If there’s multiple methods needed for some complex logic, that should be
broken into it’s own PORO. - The advantage of ease of adding more methods can also be a disadvantage in
that the Decorator can turn into a junk-drawer of loosely coupled methods.
Page Object: Complicated controller setup for a view in a PORO
Applicable Situation
- Controller is initializing multiple instance variables for the view.
- Controller has several private methods supporting the public method
associated with the action. - View has lots of inline Ruby for calculating instance variables
- Logic is specific to one controller method and one view, or maybe a couple
different response types for the same controller method. - If you find same code in multiple Page Objects for same model, then see if
you can move to a decorator. A module is another option for DRY’ing up
duplication.
Solution
-
Controller instantiates one PORO, called the PageObject, that is used by the
view. -
Inline ruby code from view and the controller is moved to the PageObject.
-
Possibly include this snippet of code so that view instance methods are
available:include Draper::ViewHelpers
Advantages
- Clearly bundles the code around the work the controller is doing and what the
view needs. - Simplifies code at the controller and view layers.
- Provides the possibility of unit testing the Page Object.
Disadvantages
There might be simpler techniques than creating another class and object, such
as putting some logic in the Draper decorator. A few lines of inline ruby code
may work better than moving this code to a separate class. A PORO has to carry
it’s own weight to justify it’s existence as a class.
Service Objects
This is a variation of the Page Object technique, with more of an emphasis on
complex model interactions. A Service Object can return a Page Object for
consumption by the view, or the Service Object can serve as the Page Object for
the view.
Applicable Situation
- Some action involving the interaction between several models, such as a customer
creating an order and charging the purchase. - Long complicated methods, and groups of methods on the model or controller
suggest the use of a Service Object.
Solution
- Create a class whose initializer takes instances of the relevant objects or
controller parameters. - Move all applicable methods into this class from the controller and models.
- Use an instance of ControllerResponse (see below) to handle responses from Ajax requests.
Use a PageObject to handle the response for views, or simply have the views
consume the Service Object.
Advantages
- Clearly bundles code around a specific, complex action.
- Promotes the creation of smaller, more clear methods when all methods in a
small class are around a single purpose. - Allows better unit testing of methods on this ServiceObject.
Disadvantages
- It can be overkill for a very simple action. In other words, having a
separate class has to pull it’s own weight. If the action is reasonably
simple, and only involves one model class, I’m much more likely to simply
move the functionality out of the view/controller to a concern or decorator.
ControllerResponse
This class handles the communication from ServiceObjects back to the Controller.
# Return value object from Service Objects and the Models back to the controller.
# Rather than throwing an exception from the Service Objects, this class will encapsulate any
# error status to send back to the client.
class ControllerResponse
attr_reader :flash_message
attr_reader :flash_type
attr_reader :http_status_code
attr_reader :data
attr_reader :redirect_path
attr_reader :session_data
# http_status can be either symbol or number
# Pass in flash_message as a html_safe string if you do not want it escaped.
def initialize(flash_type: :notice,
flash_now: false,
flash_message: "",
http_status: :ok,
data: {},
redirect_path: nil,
session_data: {})
@flash_type = flash_type
@flash_now = flash_now
@flash_message = flash_message
@http_status_code = Rack::Utils::status_code(http_status)
@data = data
@redirect_path = redirect_path
@session_data = session_data
end
def self.symbol_to_status_code(symbol)
Rack::Utils::SYMBOL_TO_STATUS_CODE[symbol]
end
# Currently supports applying the:
# 1. session_data
# 2. flash, flash_type, and flash_now
# 3. redirect_path
# Does not apply the http_status_code, nor the data
def apply(controller)
@session_data.each { |k, v| controller.session[k] = v }
if @flash_message.present?
if @flash_now
# NOTE: There is no need to escape, as Rails will do that unless the flash_message is html_safe
controller.flash.now[@flash_type] = @flash_message
else
# NOTE: There is no need to escape, as Rails will do that unless the flash_message is html_safe
controller.flash[@flash_type] = @flash_message
end
end
controller.redirect_to @redirect_path if @redirect_path.present?
end
def set_flash(flash)
# NOTE: There is no need to escape, as Rails will do that unless the flash_message is html_safe
flash[flash_type] = flash_message if flash_message.present?
end
# Returns the html escaped version of the flash.
# This method should be used when setting value in json.
# In the case of setting the flash on the controller, there is no need to escape, as Rails will do
# that unless the flash_message is html_safe
def flash_message_escaped
if @flash_message.html_safe?
@flash_message
else
CGI::escapeHTML(@flash_message)
end
end
end
Refactorings
Refactoring Steps to Concern or Decorator
Large controller method
Large method on controller does not allow simple stubbing, nor simple unit
testing.
-
Break out method lines into separate private method.
-
Move method to a concern or decorator for the given model.
-
Remove parameter of the model instance.
-
Assign the model instance var name to equal self.
my_model = self
-
Inline variable my_model.
-
Remove
self.
where it improves readability. -
Break up long method into several smaller private methods.