Here is an idea I had - using rails ActiveRecord validations to ensure that the proper methods are used to update state.
Part of the allure of active record callbacks (
before_destroy etc) is that you don’t need to worry about remembering to run
For example, if updating a
Comment should kick off a job to notify the
Author of a
BlogPost via email, well, just use something like:
class Comment < ApplicationRecord # ... def notify_blog.author_of_new_verified_comment return unless poster.verified? NotifyAuthorOfCommentJob.perform_async(self.id, blog.author.id) end after_save :notify_author_of_new_verified_comment end
Then in any controller, we don’t need to worry about making sure we remember to enqueue the notify job, but only if the comment poster is a verified account etc.
This becomes a more comfortable crutch to lean upon as the number of contributers (or just the sheer codebase size) increases. A proliferation of callbacks can (not always, but can) lead to pain - you can spend a lot of time trying to figure out complex happy-paths that end up being reliant on several callbacks chained together.
I’ve long liked the idea of using specific objects and/or methods to encapsulate
behavior that needs to happen when changing some state. Things like a
BlogPost#publish! method that not only updates the post’s
published, but also increases the authors
post_count, and kicks off a job to
rebuild some post cache somewhere. Maybe it even sends off some emails.
This is often easier to test, makes it clear where to add future behavior that may be needed, and is conceptually clear - if I want a set of things to happen when a post is published, that is the responsibility of the ‘Publisher’.
BUT… how do we make sure that the BlogPublisher is used consistently? Maybe we
add a shortcut action somewhere, and instead of remembering to call the
BlogPublisher, we only call
@post.update(status: :published). If we had an
ActiveRecord callback, we wouldn’t need to worry about that, right?
Well, here is a crazy idea (It may also be terrible, I haven’t used this pattern
in anger): Let’s use AR validations to raise an error if something other than
Post.publish! method is used to set the
status column to
Here is an example:
class Post < ApplicationRecord attr_reader :in_publish_flow class ImproperUpdateError < StandardError; end # ... validate do unless @in_publish_flow || !will_save_change_to_attribute?(:status, to: :published) raise ImproperUpdateError, "use Post#publish! to publish a blog post" end end def publish! @in_publish_flow = true update(status: :published) author.increment_post_count! RebuildBlogPostCacheJob.perform_later(author.id) end end
Notice that we’re raising an error, instead of the usual
errors.add(:status, "...") seen in validations. This is intentional, because
we want this to be caught in development, this isn’t something we want to
bubble up to the users of our app1.
Also, we’re using an attr_reader for in_publish_flow, and that value is only set
publish! method - this attr being set to
true is the guard against us
update(..) instead of using the proper method
In the end, if we really want/need to, we can still just call
post.update_attribute(status: "published") (this is ruby, after all, there is
always a workaround), but at least at that point it becomes a conscious choice,
and we aren’t doing this by accident.
I fully acknowledge that this in itself might be a smell ↩