Hello, fellow readers! Once again, we are here to talk about some neat Ruby features to make your perfect service/operation class. This time, about the Callable Pattern and how it can be used to make a powerful and flexible construct that will allow you to put order in your house.
If you are familiar enough with Ruby, it is not difficult to realize the flexibility of the language. There are always tons of ways of achieving the same results for any given challenge you may stumble upon.
That being said, one of the greatest challenges in any given project, be it because you are creating the project from the ground up or because you are refactoring an old codebase is defining patterns and practices to be used application-wise so you can enforce consistency between different parts of the project.
More than having a linter in your project (hello Rubocop), it’s crucial to have flexible constructs that fit the needs of both you as a developer and the project.
But first…
Let’s talk about Bruno "Code Organization"
For Ruby applications there’s no universally agreed-upon way to organize or write your code, just like any other language. And this is a double-edged sword, as we’ll see in a minute. Rails, however, does have a bit more of a "default way" of doing things.
TBH the enforcement of this out-of-the-box structure often leads to the implementation of antipatterns and bad practices in a project that can lead, in their own turn, to less experienced developers developing bad habits.
But that’s beside the point (topic for another article). The actual point is that in a codebase, having pieces of code that "look like each other" can be very beneficial in many different ways. Don’t believe me? Let me show you something… Just the beginning of a little file:
class BooksController < ApplicationController
before_action :set_book, only: %i[ show update destroy ]
# GET /books
def index
@books = Book.all
render json: @books
end
...
end
Now, I KNOW that if you have at least a bit of experience with Rails applications, you don’t even had to actually read the code to know that this was a Controller Class. Still skeptic? Want another? This time I’ll actually obscure some pieces of the code that could give more semantic meaning to it:
class FOO < BAR::BUZZ[7.0]
def change
create_recipe :guacamole do |r|
r.ingredient :avocado
r.ingredient :tomato
r.ingredient :onion
r.ingredient :chili_pepper
r.ingredient :cilantro
r.utensils
end
end
end
I’m pretty confident that even if you are a seasoned fellow Rails-ist you spent very little time ever with one of those opened in your text editor. BUT I CAN BET you got the feeling of seeing this structure before and didn’t take more than a few seconds to figure this is a Migration Class.
class CreateAppointments < ActiveRecord::Migration[7.0]
def change
create_table :appointments do |t|
t.date :date
t.time :time
t.string :place
t.boolean :virtual
t.string :url
t.timestamps
end
end
end
So yeah, I can complain all day about lots of stuff I don’t like about Rails (although there are many more things I love about it), but I have to admit that Rails is very good at burning those core structures with very accessible and easy to pick DSLs in our minds.
To the point that everything in a class can look like gibberish, but just because of how a class looks and is organized, you can easily recognize the type of file you are working on and switch from "controller mode" to "migration mode" very quickly.
But I can hear you asking: So, what? That’s all it has of "important"?
Not quite, my young padawan… The thing is that writing code that looks familiar for each layer of our applications has some really useful effects in our daily practice as developers. For instance:
Helps quickly identifying the kind of file you are working on and reducing "context switching fatigue"
I don’t know about you, but when I have to switch between lots of messy files that have no recognizable pattern between them, I can get annoyed very quick of reading code in that codebase. This is bound to make our productivity slowly dwindle, especially the more complex a task gets;
Helps speeding up the onboarding of new developers
It’s no surprise that having well-defined constructs and "stylistically" distinct classes for each macro-layer of your application can help with the process of onboarding a new developer and making the time they will need to get going shorter. We are excellent at recognizing patterns.
Lowers the effort of writing new code, updating old code and can help with documentation
Writing classes in the same layer of your application that all look distinct from each other or have no pattern can get very messy very quickly.
This increases the cost for maintainability of the codebase and makes it difficult to document since you have to come up with different ways of documenting each distinct class style. If you have a to-go pattern, you could even get generators, autocomplete tools, and tools like GitHub Copilot to write most of the boilerplate code and speed up the time of writing new classes.
Not just writing new code gets easier, but updating old code too, since the familiarity of knowing intuitively where things are and how they are done.
Small changes stick out more
This is something I find amazing in establishing those internal patterns and coding styles, and it kind of wraps up the first point and the one above. It’s not just that you can write code faster, but you can read and tell code apart better as well.
I see you scratching your head, let me give an example. Look at the two sets of classes below, and tell me the one easier to understand. Pay special attention to the differences between them. Both of the examples below consist of two different classes, one applies a common coding style between the two classes, and the other does not.
First, the one in a codebase WITHOUT coding patterns:
# update_author.rb
class UpdateAuthor
def initialize(id, name, birthday, bith_place)
@id, @name, @birthday, @bith_place = id, name, birthday, bith_place
end
def call
author = find_author(@id)
return nil unless author
update_author_result = update_author(author, @params)
return {errors: update_author_result.errors} unless update_author_result.success?
return author
end
# ...
end
# book/update.rb
# This class needs to update the author linked to a book
class Books::Update
def call(params)
book = find_book(params[:id])
return [:not_found] unless book.present?
update_book_result = update_book(book, params[:book])
return [:unprocessable_entity, update_author_result.errors] unless result.success?
update_author_action = UpdateAuthor.new(
params[:author][:id],
params[:author][:name],
params[:author][:birthday],
params[:author][:bith_place]
)
update_author_result = update_author_action.call
return update_author_result if update_author_result[:errors] # good thing both Hash and Author have an array-like interface to access values, right?
return book
end
# ...
end
Now, the one in a codebase WITH coding patterns. Assume DefaultOperation
just has some common boilerplate code for both classes:
# author/actions/update.rb
class Authors::Actions::Update < DefaultOperation
def call
author = find_author(@params[:id])
return Failure(:not_found) unless author
update_author_result = update_author(author, @params)
return Failure(:unprocessable_entity, update_author_result.errors) unless update_author_result.success?
return Success(author)
end
# ...
end
# book/actions/update.rb
# Also needs to update the author, but more organized
class Books::Actions::Update < DefaultOperation
def call
book = find_book(@params[:id])
return Failure(:not_found) unless book.present?
update_book_result = update_book(book, @params[:book])
return Failure(:unprocessable_entity, update_author_result.errors) unless update_book_result.success?
update_author_result = UpdateAuthor.(@params[:author])
return Failure(update_author_result.failure) unless update_author_result.success? # error forwarding is tricky to deal with, but let's go with this for now.
return Success(book)
end
# ...
end
surely we can simplify the code above to make it DRYer, but that’s to prove a point
Can you notice how easier it is to get used to reading the second example? By the time you understand the "conventions" of the first class, you can read and understand the second in a breeze.
Not only that but since you don’t need to understand the conventions again and the classes look similar, it’s easier to understand the difference in scope of the second class because those little differences between them really stick out.
Besides that, can you imagine the mess the controllers of the first application probably are? Having to deal with so many different kinds of errors and formats? ughh, disgusting…
All of this was to prove to you that keeping consistency between parts in a layer is not just useful but important. The benefits I listed above are just a few you can get from enforcing this organization in your codebase.
You already got some spoilers above, but in this next topic, we’ll talk about my favorite way of doing this for the "logic" layer of a Rails app!
This is the way… the Callable way…
First of all, let’s get something cleared out of the way: If you truly like or defend using rails out-of-the-box just as-is, this section is probably not for you. I know it’s possible to have a project that will only use Rails’ models, controllers, views, concerns, etc, but that IS NOT the rule.
Most of the projects I worked on are too complex and require a proportionally more complex architecture. And if your project sticks around for time enough chances are that you will also need it as it grows.
A great way to put what we discussed up until now in practice is, as I have hinted before, to use Ruby’s Callable Pattern. This pattern has some very cool features that allow for a great deal of flexibility while providing that "unique signature" for the application layer of your application.
Let’s reuse the example above without using dry_monads, for the Update Book and Update Author Actions, but now with a whole flow. Assume the models are as simple as they can get, with just enough code to support the features seen here:
Fun fact about Ruby: for any class
Foo
with acall
method defined, be it an instance or class method, you can decide on whether to invoke the method asFoo.call()
or asFoo.()
. Or for instance methods,Foo.new.call()
is the same asFoo.new.()
. Just a little bit of syntax sugar to make our days sweeter.
# app/controllers/api/v1/books_controller.rb
class Api::V1::BooksController < Api::BaseController
def update
result = Books::Actions::Update.(params: book_params)
if result[:failure]
render json: result[:errors], status: result[:failure]
else
render json: result, status: :ok
end
end
end
# app/controllers/api/v1/authors_controller.rb
class Api::V1::AuthorsController < Api::BaseController
def update
result = Authors::Actions::Update.(params: author_params)
if result[:failure]
render json: result[:errors], status: result[:failure]
else
render json: result, status: :ok
end
end
end
# app/modules/callable.rb
class Callable
def self.call(**kwargs)
new.call(**kwargs)
end
def initialize(*args, **kwargs)
kwargs.each { |k, v| instance_variable_set("@#{k}", v) }
end
end
# app/modules/books/actions/update.rb
class Books::Actions::Update < Callable
def call(params:)
validate_params_result = validate_params(params)
return { failure: :bad_request, errors: validate_params_result[:errors] } if validate_params_result[:errors]
update_book_result = update_book(params[:id], params[:book])
return { failure: update_book_result[:failure], errors: update_book_result[:errors] } if update_book_result[:failure]
update_author_result = update_book(params[:book][:author])
return { failure: update_author_result[:failure], errors: update_author_result[:errors] } if update_author_result[:failure]
return update_book_result
end
private
def validate_params(params)
errors = []
errors << { "book.id" => "should be an integer" } unless params[:id].is_a? Integer
errors << { "book.title" => "should be a string" } unless params[:book][:title].is_a? String
errors << { "book.publisher" => "should be a string" } unless params[:book][:publisher].is_a? String
errors << { "book.published_at" => "should be a date" } unless params[:book][:published_at].is_a? Date
errors << { "book.title" => "should not be empty" } if params[:book][:title].blank?
errors << { "book.publisher" => "should not be empty" } unless params[:book][:publisher].blank?
{ errors: errors }
end
def update_book(id, params); Books::Interactions::Update.(id:, params:) end
def update_author(author_params); Authors::Interactions::Update.(id: author_params[:id], params: author_params) end
end
# app/modules/books/interactions/update.rb
class Books::Interactions::Update < Callable
def call(params:)
book = find_book(params[:id])
return { failure: :not_found } unless book.present?
book.update(process_params(params))
return { failure: :unprocessable_entity, errors: updated_book.errors } if updated_book.errors
book
end
private
def find_book(id); Book.find_by(id: id); end
def process_params(params)
{
title: params[:title],
publisher: params[:publisher],
published_at: params[:published_at]
}.compact
end
end
# app/modules/authors/actions/update.rb
class Authors::Actions::Update < Callable
def call(params:)
validate_params_result = validate_params(params)
return { failure: :bad_request, errors: validate_params[:errors] } if validate_params[:errors]
result = update_author(params[:id], params[:author])
return { failure: result[:failure], errors: result[:errors] } if result[:failure]
return result
end
private
def validate_params(params)
errors = []
errors << { "author.id" => "should be a an integer" } unless params[:id].is_a? Integer
errors << { "author.name" => "should be a string" } unless params[:author][:name].is_a? String
errors << { "author.birthday" => "should be a date" } unless params[:author][:birthday].is_a? Date
errors << { "author.name" => "should not be empty" } if params[:author][:name].blank?
{ errors: errors }
end
def update_author(id, params); Authors::Interactions::Update.(id:, params:) end
end
# app/modules/authors/interactions/update.rb
class Authors::Interactions::Update < Callable
def call(params:)
author = find_author(params[:id])
return { failure: :not_found } unless author.present?
author.update(process_params(params))
return { failure: :unprocessable_entity, errors: updated_author.errors } if updated_author.errors
author
end
private
def find_author(id); Author.find_by(id: id); end
def process_params(params)
{
name: params[:name],
birthday: params[:birthday],
pseudonyms: params[:pseudonyms]
}.compact
end
end
We can see two instances where the classes in the module
folder are being called: By controllers and other callable classes. For the controllers, you can probably notice right away how having a common interface for returning errors, failures, and even successful results simplifies the code and makes states easier to handle without the need for extensive handle-error-like exception classes.
The same goes for the Action classes that use the interaction ones. See that even if they have different logic, it is easy to spot and understand the differences without having to spend a lot of time understanding the implementation details. Even the interface to invoke the code is simple and straightforward: just a .()
after the constant name.
A quick tangent
I had a discussion with a fellow programmer regarding the "signature" of the callable classes and how he would prefer to have the new
keyword in place where he would then pass the params and later send the actual call without params, not needing the boilerplate in the Callable
class.
My take on this is that I like to imagine the new
method as the place to set the configuration of the operator (that would be the instance of the class), while using the call to actually invoke the operation while sending the data.
This approach has several benefits, especially regarding tests. Let’s get the Books::Actions::Update
class as an example of this. For this case, we’ll change the class to use a bit of DI (Dependency Injection) to isolate our unit tests and allow for a greater deal of flexibility:
# app/modules/books/actions/update.rb
class Books::Actions::Update < Callable
def initialize(update_book_interaction: Books::Interactions::Update, update_author_interaction: Authors::Interactions::Update, **kwargs)
@update_book_interaction = update_book_interaction
@update_author_interaction = update_author_interaction
super
end
def call(params:)
...
end
private
def validate_params(params); ... end
def update_book(id, params); @update_book_interaction.(id:, params:) end
def update_author(author_params); @update_author_interaction.(id: author_params[:id], params: author_params) end
end
Now you have the option of using this class with the default options for update_book_interaction
and update_author_interaction
, or overriding them on initialize:
Books::Actions::Update.new(
update_book_interaction: OtherBookUpdateInteraction,
update_author_interaction: OtherAuthorUpdateInteraction
).call(params: { ... my params ...})
Books::Actions::Update.call(params: { ... my params ...})
This is particularly useful when, for instance, you have a dependency that makes HTTP requests and you need to test this operation. So instead of relying on tools to record and replay those requests, you can simply create a mock of the api and inject it in your operation on initialization, allowing you to isolate the tests and avoiding having to store hundreds of lines of yamls.
Other callables
The call pattern is not just a thing we can implement in our classes but something deep within Ruby itself. In this section, we’ll talk a bit about Procs, Lambdas, and Blocks but will not go into a lot of detail on how each one of those works and its differences.
Being as flexible and having so many metaprogramming features as Ruby has, it should not be a big surprise that, on the topic of "Closures", Ruby has many different ways of getting to the same result. Understanding this can be particularly useful when used in conjunction with the initialize strategy shown above.
To demonstrate the use of each one of those constructs, Lambdas, Procs, and Callable Classes (we’ll explore Blocks in a later example), let’s imagine that you have a subscription manager that can create users in several different streaming platforms. It receives some raw user params and a symbol that selects what service to use to create the account in the selected streaming platform after processing the params.
class SubscribeUserToStreamingPlatform < Callable
def call(params:, platform:)
user = create_user(params)
case platform
when :netflix
# do some specific processing...
# call netflix web API...
# do some more specific processing...
when :disney_plus
# do some specific processing...
# call disney_plus web API...
# do some more specific processing...
when :hbo_max
# do some specific processing...
# call hbo_max web API...
# do some more specific processing...
end
end
private
def create_user(params); ... end
end
# and to use:
SubscribeUserToStreamingPlatform.(params: { ... user params ... }, platform: :netflix)
SubscribeUserToStreamingPlatform.(params: { ... user params ... }, platform: :disney_plus)
SubscribeUserToStreamingPlatform.(params: { ... user params ... }, platform: :hbo_max)
SubscribeUserToStreamingPlatform.(params: { ... user params ... }, platform: :something_else) # => returns `nil`
This is "fine" but if I need to add another streaming platform in the future, I’d have to come here and add another when
statement with the specific logic for it, increasing the complexity of this operation. What if we can simplify it, extract the specific logic for each platform to different places and free "SubscribeUserToStreamingPlatform" of the burden of knowing how many streaming platforms there are? Let’s do it:
class SubscribeUserToStreamingPlatform < Callable
def call(params:, platform_service:)
user = create_user(params)
return unless platform_service
platform_service.call(user)
end
private
def create_user(params); ... some logic ... end
end
netflix_lambda = -> (user) {
# do some specific processing...
# call netflix web API...
# do some more specific processing...
}
disney_plus_proc = Proc.new do |user|
# do some specific processing...
# call disney_plus web API...
# do some more specific processing...
end
class HBOMaxCallableClass
def call(user)
# do some specific processing...
# call hbo_max web API...
# do some more specific processing...
end
end
# and to use:
SubscribeUserToStreamingPlatform.(params: { ... user params ... }, platform_service: netflix_lambda)
SubscribeUserToStreamingPlatform.(params: { ... user params ... }, platform_service: disney_plus_proc)
SubscribeUserToStreamingPlatform.(params: { ... user params ... }, platform_service: HBOMaxCallableClass)
SubscribeUserToStreamingPlatform.(params: { ... user params ... }, platform_service: nil) # => returns `nil`
# Keeps pretty much the same interface
Of course, I’d never suggest implementing your services in that manner, as it goes exactly against the principle of "similar things, should behave similarly" we have been advocating for. But this is to prove the power of the callables! Now you can test the SubscribeUserToStreamingPlatform
class without having to deal with the complexity of specific services since each one of them can now be tested separately as well.
A final tip
Before wrapping up, let’s talk about a final tip to ensure you’ll have the best possible experience walking the Callable Way.
And it is: avoid using attr_*
This is a tip I’m very opinionated about, but I think it’s worth sharing. When using this pattern, It’s recurring to see stuff like this:
class Authors::Interactions::Update
attr_reader :id, :params
def initialize(id, params)
@id, @params = id, params
end
def call
return { failure: :not_found } unless author.present?
author.update(processed_params)
return { failure: :unprocessable_entity, errors: updated_author.errors } if updated_author.errors
author
end
private
def author; @author ||= Author.find_by(id: id); end
def processed_params
{
name: params[:name],
birthday: params[:birthday],
pseudonyms: params[:pseudonyms]
}.compact
end
end
This may look fine, but as complexity increases it can be very confusing finding out where those values are coming from or whether the tokens you are calling are actually methods or just variables; plus this exposes you instance variables to the outside world, making the operation that should be a one-way closed flow prone to bugs and inconsistencies and to leaking state (especially if in the place of attr_reader
you’d have attr_acessor
up there).
If you want your operations instances to be "reusable" you’d be better off using some other kind of pattern/design. One may argue that the attr_reader
above could be below the private keyword, and this would solve this problem and it would be correct, but yet, I’d rather be explicit on whether I’m invoking an instance variable that simply holds a value or a method, or a local variable.
To summarize, my advice here would be:
- always call instance variables with the @ before it and avoid using
attr_<anything>
; - always call private methods with
()
. I usually recommend using positional arguments for private/internal methods and keyword arguments for external/public ones. For instance, a private#find_author(id, params)
but a#call(params: params)
; - local variables can be called plainly.
Remember: "Similar things, should behave similarly".
BTW, on this topic, I would recommend this excelent article by my friend and former mentor Thiago Araújo: Towards Minimal, Idiomatic, and Performant Ruby Code
. Most tips there complement the ones I talk about here and it’s a great read anyways, so you should go read it (after finishing reading this post, of course…)
The end of the way
Well, that was a long one.
Today we talked about how you can harness the POWER OF THE CALLABLE to improve your codebase and make things more readable, concise, and easy to maintain. I have been using a version of these patterns for the best part of my Ruby/Rails developer life, but just now I think I managed to put most of it together in a way I feel satisfied about.
I say most of it because, in a following post, I’ll be talking about how we can BEEF IT UP by leveraging the power of the Dry ecosystem. These people have been doing an amazing job with those libs and it makes developing in Ruby even more enjoyable. Hey, It’s all about developer happiness, okay…?
Anyway, always remember this is just one of the ways to do things in Ruby, and that is one of the coolest things about this beautiful language.
Let me know your thoughts on the topics we discussed today and what, if anything you’d improve, replace, discard, or use!
And see you in the next one!
—
We want to work with you. Check out our "What We Do" section!