Benito Serna Tips and tools for Ruby on Rails developers

A simple way to organize your app in use cases

October 20, 2018

I have already told you that organizing your code in use cases can help you to start your projects using TDD … And other people also recommend it for other purposes… You really should try it!

The problem is, that is not really obvious how to do it!…

For example…

How should you expose a use case?…
Should you do it as an object or a function?
If you decide to use an object, which methods should that object have?
Should you use the same method name to execute all of them?
Which arguments should you pass at initialization?
Which arguments should you pass at execution?

And suppose that you have decided how to expose them?…
Do you know how you should integrate that object in to you framework of choice?

Have you ever thought about it? Are you struggling deciding on how to expose the use cases in your app?

Well if that is you case, I want to propose you a method…

Start as simple as possible… Instead of exposing objects with state, try to start with functions and values… And instead of looking for a fancy way to integrate them with your framework, start by just calling a function.

Hmmm… Let’s expand a little more here…

When I say try to expose functions and values instead of objects

Is because if you want to expose an object, you will have to decide the name of the object, the name of the function that you will call, which arguments to pass at initialization and which arguments to pass at execution. Also one day you will have to decide if a new method should belong to that object or not…

Instead if you just expose a function you just need the name of the method and maybe the module or namespace where you want to put that function, and if later you decide to move that function elsewhere, as there is not state involved, the task will be much more easier!

So instead of deciding between…

GenerateInvoice.call(payment, config)
# or
GenerateInvoice.new(config).call(payment)
# or
GenerateInvoice.new(config, payment).call()
# or
InvoiceGenerator.new(config).generate_for(paymen)
# or
payment = Payment.new(attrs)
payment.generate_invoice(*args)

Try to expose a function

module Payments
  def self.generate_invoice(payment, config)
    # something
  end
end

Payments.generate_invoice(payment, config)

The main benefit with this solution, is that inside that module you can solve the problem in whatever way you prefer, maybe even using one of the other options above, without worrying which is better… Because all of them can be good solutions for different problems at different times…

And if later you have a different opinion and decide that other way is better or you need to add a new dependency, you can refactor everything inside that function, without worrying on breaking something outside or in your tests

Because each time you want to expose that behavior you can use that function… That means that all your test for that use case run through that function, and also the integration of that use case with your framework run through that function

Maybe with something like this in your controller/route…

class InvoicesController < ApplicationController
  create
    config = {
      #...
    }

    status = Payments.generate_invoice(payment, config)

    if status.success?
      # render success
    else
      # render error
    end
  end
end

And testing it like…

module Payments
  RSpec.describe "Generate invoice" do
    def generate_invoice(payment)
      config = {
        #...
      }

      Payments.generate_invoice(payment, config)
    end

    it "creates an invoice record for the payment" do
      expect(invoices_store).to receive(:create).with(payment_id: payment.id)
      generate_invoice(payment)
    end

    #....
  end
end

In this way you will write modules with functions that expose what your software is able to do. You will be able to test those functions in isolation and integrate them with your framework will be a function call…

What do you think?…

Wouldn’t it be easier for you to test the behavior of your app?

Don’t you think that now, decoupling your business logic from the framework could be less scary?

Related articles

Weekly tips and tools for Ruby on Rails developers

I send an email each week, trying to share knowledge and fixes to common problems and struggles for ruby on rails developers, like How to fetch the latest-N-of-each record or How to test that an specific mail was sent or a Capybara cheatsheet. You can see more examples on Most recent posts or All post by topic.