This is the way… the Callable Way

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 a call method defined, be it an instance or class method, you can decide on whether to invoke the method as Foo.call() or as Foo.(). Or for instance methods, Foo.new.call() is the same as Foo.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!

via GIPHY

We want to work with you. Check out our "What We Do" section!