! Method Implementations in Rails
--
In my previous post, “Using ! Wisely with Rails Development,” I explained how a good convention with ! methods will prepare subsequent coders by maintaining return value consistency. My suggestion for the best convention was the Rails connotation, “which indicates a method that raises on failure (as opposed to failing without blowing up the chain of execution).”
To display this convention in practice, I’ve coded part of an example application for the U.S. Census. I’ll show a simple case followed by a series of more complex examples where multiple models and layers of the application interact.
In my app, “Person” is a resource exposed via an API. To make things morbid, I will create an endpoint to mark a person deceased.
class PeopleController
# POST people/:id/death
def death
person = Person.find(params[:id])
if person.mark_dead
render json: { success: true }, status: :created
else
render json: { errors: person.errors.full_messages }
end
end
end
`Person#mark_dead` marks the time of their death. If successful, this endpoint will return a small JSON blob. If unsuccessful, the endpoint will return the appropriate error messages. I’ll code `Person#mark_dead` to return true or false depending on whether the record persisted — this makes the method consistent with other Rails library methods like `save` and `update_attributes`.
# v1
class Person < ActiveRecord::Base
…
def mark_dead
self.dead_at = Time.now.utc
save
end def mark_dead!
self.dead_at = Time.now.utc
save!
end
end
Let’s say the app’s business logic requirements become more complicated and I need to make a `death_certificate` while also marking the person dead. This `death_certificate` serializes a variety of attributes about the person at their time of death. I will invoke the creation of a death certificate with a new instance method I’ll call `generate`.
In keeping with our contract for ! methods, we should have the ! raise on failure and the non-! methods return a boolean. While I only need to implement one, I’ll showcase both examples here:
# v2
class DeathCertificate
belongs_to :person def generate
assign_death_time_attributes
save
end def generate!
assign_death_time_attributes
save!
end private def assign_death_time_attributes
assign_attributes(
reason: …,
place_of_burial: …,
…
)
end
end
It only makes sense to generate a death certificate when the person is marked dead, so we might have `Person#mark_dead` call `DeathCertificate#generate`. (Alternatively, we could write a service to invoke both methods, but I’ll keep things simple.) To respect our convention, the invoking method should raise and use a ! if one of its child methods raises. Otherwise, the invoking method should neither raise nor use a !.
#v2
class Person < ActiveRecord::Base
has_one :death_certificate
…
def mark_dead
self.dead_at = Time.now.utc
death_certificate.generate
save
end def mark_dead!
self.dead_at = Time.now.utc
death_certificate.generate!
save!
end
end
While this code works within the framework of our convention, it is not transactional. If either the death certificate or the person failed to persist, the code would have inconsistent and potentially undesirable results. In order to complete my code snippet, I’ll need to wrap both persistence calls (`generate` and `save`) in a transaction block.
There are several ways of ensuring that code is transactional. I’ll demonstrate two that work with the `generate` code written in v2 above. ActiveRecord::Rollback is a special exception provided by Rails that rolls back the transaction block without passing the exception on to the caller. When dealing with methods in transactions that do not explicitly raise exceptions themselves (shown below) this special exception can come in handy.
# v3a# app/models/person.rb
def mark_dead
transaction do
self.dead_at = Time.now.utc
unless save && death_certificate.generate
raise ActiveRecord::Rollback
end
true
end || false
end# v3b# app/models/person.rb
def mark_dead
transaction do
self.dead_at = Time.now.utc
begin
death_certificate.generate
save!
rescue ActiveRecord::RecordInvalid
raise ActiveRecord::Rollback
end
true
end || false
end
Both of these methods respect the ! convention while maintaining transactionality. They will either complete or entirely revert the action and the caller can test for success using only `true` or `false`. This convention simplifies our API and gives the developer a simple set of expectations to work with. Our original controller code remains the same and the `mark_dead` method still returns a receptive boolean.
class PeopleController
# POST people/:id/death
def death
person = Person.find(params[:id])
if person.mark_dead
render json: { success: true }, status: :created
else
render json: { errors: person.errors.full_messages }
end
end
end
The final option is a trivial task: re-raising the exception in the `mark_dead!` method while keeping the method transactional. This option is useful in a background job where you’d want a failure to be noisier.
# v3c# app/models/person.rb
def mark_dead!
transaction do
self.dead_at = Time.now.utc
save!
death_certificate.generate!
end
end
By maintaining a consistent contract, these methods will give clear expectations to developers coding using your API. You will avoid unexpected results which might arise from minute coding style differences. Most importantly, you give your fellow developers a more seamless coding experience.
I hope you’ve enjoyed this post, and feel free to check me out at http://dylandrop.com or https://medium.com/@dylandrop for more updates.
Thanks to my friend Spencer Horstman for helping me edit this article.