Federico Bond

random thoughts on programming

Managing Complex Apps in Cuba

Today I’m going to show you some of the things I learnt while working on a moderately big Cuba application.

Let’s start with matchers. What is a matcher? Matchers are DSL methods let you define handlers when specific conditions of a request are met. The most common conditions are the HTTP verb and the path. I believe, altough I may be wrong, that Sinatra was the first Ruby framework to expose such kind of matchers. This is how you write one in Sinatra (example taken straight from their website).

get '/hi' do
  "Hello World!"
end

This will match all GET /hi requests coming to the application. Sinatra does not handle nested matchers. There is an extension called Sinatra::Namespace that provides a limited form of namespacing, but it feels a little bolted on. Cuba, on the other hand, was baked in from the beginning with support for arbitrary matchers, so you can do things like this:

Cuba.define do
  on get, 'hi' do
    res.write "Hello World!"
  end
end

You can also nest matchers like this:

Cuba.define fo
  on 'hi' do
    on get do
      res.write "Hello, stranger!"
    end

    on post do
      name = req.params[:name]
      res.write "Hello #{name}"
    end
  end
end

Although this example is a bit contrived, nesting matchers is a very powerful concept that can provide more structure and improve cohesion in your application. Cohesion is the level by which code that is conceptually close remains physically close in the source file. In the last example, by defining the matcher on the /hi route, we have forced the GET and POST handlers to remain close together. Nobody can add code in between that has nothing to do with these two, because it must necesarily belong to the informal namespace created by matching the /hi path.

Consider the same example written in Sinatra:

get '/hi' do
  "Hello, stranger!"
end

post '/hi' do
  name = params[:name]
  "Hello, @{name}"
end

Now there is nothing stopping someone from trying to add a handler in between these two, so a big application, if not carefully built and mantained, may end up with these two handlers separated by hundreds of lines. This reduces cohesion and also increases duplication, because if we ever have to change the /hi path to /welcome or internationalize it, we have to change code in several places.

Nested matchers are very powerful, but should also be used with care. Moving around matchers in Sinatra is easy because they all exist at the same level, but wrongly or deeply nested matchers in Cuba can become more difficult to refactor. This is an example to see what I’m talking about:

Cuba.define do
  on get do
    on 'hi' do
      res.write "Hello, stranger!"
    end
  end

  on post do
    on 'hi' do
      name = params[:name]
      res.write "Hello, @{name}"
    end
  end
end

See what I did there? I chose a very generic condition for the topmost matcher, effectively separating the application into two sections and forcing an unnatural division of concepts. Now the code for the /hi handlers lives in two very different places. As the application grows bigger, the problem is exacerbated, and it becomes very easy to introduce bugs. A better practice would have been to match routes first, as they let you define much more domain-specific sections within you code. With a better nesting structure, all the /admin code can live in a certain place, clearly separated from the /user or /api code. This makes it easier to make sense of the structure of application.

This technique also makes it easier to decompose an application into multiple ones, so you can move from this:

Cuba.define do
  on get, 'hi' do
    res.write "Hello World!"
  end

  on 'admin' do
    on 'dasboard' do
      res.write "Admin Dashboard"
    end
  end
end

To this:

Admin = Cuba.new do
  on 'dashboard' do
    res.write "Admin Dashboard"
  end
end

Cuba.define do
  on get, 'hi' do
    res.write "Hello World!"
  end

  on 'admin' do
    run Admin
  end
end

I hope this is useful to you when structuring bigger Cuba applications, but also as an exercise in thinking about how code structure can affect maintainability in general. If you want to learn more about these concepts, I highly recommend reading Clean Code by Robert C. Martin, which expands on these and many other topics.