How to change objects
This recipe helps you find the best approach for mutating object state in a Rails app. It covers the whole pipeline from the view, through the controller all the way to the model.
Rails has some awesome magic that allows us to build apps very quickly. The Rails Way is very useful during the initial stages of development, and for simple use cases. However for more complex scenarios, I prefer to use Object Oriented best practices like Service Objects.
The flow chart below helps you choose the best approach for mutating object state by looking at two criteria:
- How many objects will you change?
- What additional processing is required?
How many objects will you change?
Are you working with input data for a single object, or for multiple ones? This criterion helps you decide whether to use regular resource forms, or nested forms.
NOTE: We don’t look at how many objects will eventually be affected; that will be dealt with under additional processing.
Change a single object
Attributes for a single object are given.
Change multiple objects
Attributes for multiple objects are given. E.g., changing a user and some of the user’s projects with a single form submission.
What additional processing is required?
This question is concerned with the complexity of the change. E.g., what else needs to be done as a consequence of this change? What other resources are involved?
No additional processing
All we do is change the attribute and nothing else.
Simple processing
We do some very simple processing around the change. E.g., we sanitize some data or compute some dependent values. ActiveRecord callbacks are suitable in this scenario, as long as they reference only internal state and no other objects.
Complex processing
Involves one or more of the following:
- The process touches other objects.
- The process uses a 3rd party service.
- There are multiple processing steps and a few places in the process where things can go wrong.
- There are several possible paths to take during the process, depending on the input data.
The various approaches
Rails magic
Example scenario:
Update @user.accepted_tos after user accepts Terms of Service.
Use the vanilla Rails Way:
-
Model: A standard ActiveRecord based class:
# app/models/user.rb class User < ActiveRecord::Base end
-
View: A standard resource form:
<%# app/views/users/edit.html.erb %> <% form_for @user do |f| %> <%= f.check_box :accepted_tos %> I accept the ToS <% end %>
-
Controller: A standard RESTful controller:
# app/controllers/users_controller.rb class UserController < ApplicationController def update @user = User.find(params[:id]) if @user.update_attributes(params[:user]) redirect_to user_path(@user) else render :action => 'edit' end end end
ActiveRecord callbacks
Example scenario:
Before updating @user, strip whitespace from email.
Use the vanilla Rails way with ActiveRecord callbacks.
Important!
Callbacks should use internal state only. There should be NO references (read or write) to external objects or services.
Read this blog post for more information: The Problem with Rails Callbacks
-
Model: A standard ActiveRecord based class with callbacks.
# app/models/user.rb class User < ActiveRecord::Base before_save :strip_whitespace_from_email def strip_whitespace_from_email self.email = email.strip end end
- View: A standard resource form for @user
- Controller: A standard RESTful controller for @user
More information on callbacks:
ActiveRecord nested attributes
Example scenario:
Update @user and delete some of @user’s projects.
Use the vanilla Rails way with nested attributes:
-
Model: A standard ActiveRecord based class with nested attributes:
# app/models/user.rb class User < ActiveRecord::Base has_many :projects, :dependent => :destroy accepts_nested_attributes_for :projects, :allow_destroy => true end
-
View: A standard resource form with
fields_for
:<%# app/views/users/edit.html.erb %> <% form_for @user do |f| %> <%= f.check_box :accepted_tos %> <%= f.fields_for :projects do |project_fields| %> Name: <%= project_fields.text_field :name %> <%= project_fields.check_box :_destroy %> Delete <% end %> <% end %>
-
Controller: A standard RESTful controller for @user.
More information on nested attributes:
- Rails API: ActiveRecord::NestedAttributes::ClassMethods
- Rails API: ActionView::Helpers::FormHelper#fields_for
- Complex Rails Forms with Nested Attributes
Service Object
Example scenario:
Import several users and their projects from a spreadsheet.
Create a Service Object for importing users and use it for the form and controller. Please see 7 Patterns to Refactor Fat ActiveRecord Models for more information on how to use Service Objects.
-
Model: A PORO (Plain Old Ruby Object) service object:
# app/models/user_import.rb class UserImport # Please see the linke above for more information # on Service Objects. end
-
View: A resource view for the service object:
<%# app/views/user_imports/new.html.erb %> <% form_for @user_import do |f| %> Spreadsheet to import: <%= f.file_field :user_list %> <% end %>
-
Controller: A RESTful controller for the service object:
class UserImportsController < ApplicationController # Use regular REST actions on the Service Object. end
Note: If there are several possible outcomes, or several places where things can go wrong, then you might consider using Outcome.rb as the Service Object’s return value.
More information on Service Objects:
- 7 Patterns to Refactor Fat ActiveRecord Models.
- Service Objects: What They Are, and When to Use Them.
- Service classes as an alternative to observers/callbacks
Related concepts
- How to handle AR touch
updated_at
column onbelongs_to
- Isolate ActiveRecord for fast tests.
- Message queues for asynchronous workers.
- State machines
- Outcome.rb for complex return values
- Is there any place for
after_...
AR callbacks? Are they a code smell?
Further reading
- 7 Patterns to Refactor Fat ActiveRecord Models.
- Service Objects: What They Are, and When to Use Them.
- Rails, Models and Business Logic via wekeroad.com
- an experimental approach to presenters, interactors and repositories
- Mutations: Putting SOA on Rails for security and maintainability
- 5 simple rules to good OO in Rails
- DCI, Concerns and Readable Code
- Models, Roles, Decorators, and Interactions – A modest proposal for a toned done version of DCI that isn’t as janky as Concerns.
- The Tortoise and the Hare: Hexagonal Rails and Service Objects
- Service classes as an alternative to observers/callbacks
- Architecture the Lost Years - Robert Martin
- How to Stop Using Nested Forms