Stop Writing Rails Controllers

Writing Rails controllers can be a pain. They exist in a weird transitional point in your application, facilitating communication between the world of hypertext, headers, and response codes (Railsy code) and your application domain.

They are never the interesting portion of the code, often boring boiler-plate code with lots of different code paths to test. A simple PUT to update a model involves parsing parameters, checking if the user has permissions on that model, trying to update, rendering errors if there are any, rendering the model if there weren’t and deciding what format to render the results in. Phew! That’s a lot of responsibility for one little controller. Even skinny controllers have a lot to do, but what they are doing is repetitive and boring.

I decided to never write another controller if I could help it. That’s why I wrote DDC.

Data Driven Controllers

Data Driven Controllers (DDC) lets you declare via data how to convert back and forth from HTTP to your application’s domain without the need for code. By adhering to a couple of interfaces, you can avoid writing most controller code and tests. DDC breaks the process of handling a request into three parts.

1. Convert parameters.

This step is handled by some sort of context builder. It is mostly in charge of gathering parameters, but may need to pluck out additional information from the controller. The information is collected into a form that the domain code can digest (usually a data blob via a Hash or Struct).

2. Process the domain request / action.

The domain level service object takes the necessary information and processes it (update the database, send emails, external services). The service then returns a result that knows nothing about HTTP-land. It includes things like status (application, not HTTP), objects, errors, etc.

3. Glue.

DDC is the glue that holds it all together. It creates a controller class that does all the default things for you, but allows you to override and fill in the blanks where necessary. When defining your glue, you simply tell the action how to get the params from the context builder and what service object to send them off to.

DDC Example Code

Controllers

# controllers/monkeys_controller.rb

DDC::ControllerBuilder.build :monkeys
  before_actions: [:authenticate_user!],
  actions: {
    show: {
      context: 'context_builder#user_and_id',
      service: 'monkey_service#find'
    },
    index: {
      context: 'context_builder#user',
      service: 'monkey_service#find_all'
    },
    update: {
      context: 'context_builder#monkey',
      service: 'monkey_service#update'
    },
    create: {
      context: 'context_builder#monkey',
      service: 'monkey_service#create'
    }
  }

Context Builders

# lib/context_builder.rb

class ContextBuilder
  def user(context_params)
    HashWithIndifferentAccess.new current_user: context_params[:current_user] 
  end

  def user_and_id(context_params)
    user(context_params).merge(id: context_params[:params][:id])
  end

  def monkey(context_params)
    info = context_params[:params].permit(monkey: [:color, :poo])
    user_and_id(context_params).merge(info)
  end
end

Services

Service objects can themselves be boiler-plate and boring. DDC includes a simple CRUD service generator to get you started.

# lib/monkeys_service.rb

class MonkeyService
  def find(context)
    id, user = context.values_at :id, :current_user
    me = find_for_user user, id
    if me.present?
      ok(me)
    else
      not_found
    end
  end

  def update(context)
    id, user, updates = context.values_at :id, :current_user, :monkey
    george = find_for_user user, id

    if george.present?
      update_monkey(george, updates)
    else
      not_found
    end
  end

  private

  def update_monkey(george, updates)
    if george.update_attributes updates
      ok(george)
    else
      model_errors(george, george.full_messages)
    end
  end

  def not_found
    {status: :not_found}.freeze
  end

  def ok(obj)
    {status: :ok, object: obj}
  end
  # ...

end

# shortcut for default CRUD service
MonkeyService = DDC::ServiceBuilder.build(:monkey)

Going Forward

So far I love defining my controllers this way. My system tests make sure that things are glued together and individual pieces can be tested easily. By defining all the pieces in the Railsy locations, routes and class reloading all work flawlessly. In the immortal words of Rails creator, DHH: “Look at all the things I’m not doing”. What I am doing is declaring what needs to be glued together in my application instead of how to glue them. Stop writing Rails controllers today.

Conversation
  • Daniel says:

    So basically you’re just moving code from one point to another, spreading the entropy, needlessly overcomplicating stuff and forgetting about KISS completely. What’s the point?

    • Shawn Anderson Shawn Anderson says:

      Thanks for your response Daniel. I agree that one should avoid needless complication and definitely “keep it simple”.

      Let me sum up the point more succinctly:

      A service class more clearly maps the mental model of the problem you’re solving without blending your ideas with web- or framework-centric code and state. Controllers are, by design, tightly coupled to Rails and its way of exposing web stack details to you. It’s not that controllers are bad; its that they occupy a very specific place in your web request processing stack, and if you have consistent ideas about how controllers should be implemented to provide access to your services, why not automate the consistent parts and free yourself to focus on the core ideas?
      (config/routes.rb abstracts away the fine details of connecting http verbs to assumed controllers and actions, why stop there?)

    • Casey Jenks says:

      Agreed 100%

      • Casey Jenks says:

        I’m all for service classes to simplify controllers and models, but I don’t think this is the solution.

  • Ben says:

    I have to agree with Daniel.
    To loosely quote Eloquent Ruby: “Sometimes the best code is the code you don’t write.”

    • Shawn Anderson Shawn Anderson says:

      Thanks Ben, I couldn’t agree with you more about the best code being the code you don’t write; that’s exactly why I wrote DDC.

      Each action built from data in DDC, represents a pattern of about 30 lines of code that would be duplicated in most of the other actions: pull in parameters, shuttle off to service object (an ActiveRecord model qualifies here), look at the response to determine status code / errors, then render the appropriate formatted data response. Users of DDC can define the interesting pieces (context builder and service) and move on to the code of their application domain.

  • John Lito says:

    I really didn’t like this, really. It isn’t easy to compreent. Almost all the times it is better to have a straightforward code. Could you show more complex examples? I don’t write CRUD applications, and the actual way provides the flexibility and simplicity I want.

  • Todd says:

    I think I like the concept here. It sort of forces you to have skinny controllers and to build your application in a consistent way. The first use case that comes to mind is building an API that delivers consistent and straightforward responses.

  • Lasse says:

    I don’t like Rails for the same reasons that you don’t like Rails controllers. I like this! :)

    I typically do the same thing (in Sinatra or someting else non-Rails), but without the abstraction you present here. My controllers simply just delegate to another class that contains the actual logic.

    I diverse from you, in the way that my controllers still handles param parsing, status codes and that kind of http-stuff and my logic-code handles pure logic and nothing else. That way my logic-classes are more reusable IMHO.

    • Shawn Anderson Shawn Anderson says:

      Thanks for your kind response, Lasse.

      When I made the move to Rails 4, I looked for a good way to separate the parameter parsing with strong parameters from my other controller code and came across this article by Pivotal Labs. Since I already had separate classes for parsing parameter, it made sense to separate them out when writing DDC.

      The service objects do return a status, but it’s an application status not an HTTP status. DDC will translate application statuses to HTTP statuses based on it’s status map. It has a default mapping, but should definitely be updated to a custom mapping for anything other than basic CRUD controllers.

  • Polo Santiago says:

    Hi, thanks for sharing this idea. I have two questions:

    – How is session and flash handled?
    – What does DDC::ControllerBuilder (which looks like a factory) exactly do?

    Maybe it is too much to ask but It would be great if you could share a fully functional demo project to be runned

    Thanks again!

    • Shawn Anderson Shawn Anderson says:

      Hey Polo, glad to share. :)

      You’re right, the ControllerBuilder is a factory that builds Controller Classes. Calling build with :monkeys will define a MonkeyController class that extends from ApplicationController or pardon the pun, monkey patch it if it already exists. The builder defines a method for each action that follows conventions to build up a context, send it to your domain, and render it back to the user. I liked this approach because you can just reopen the class if you need to add any special sauce to you controllers such. For example, I did this for the our JSON registration controller that needed to sign the user into the session.

      Flash is an interesting question. This pattern was extracted from a CRUD JSON API from my current project and has no need for flash. I think DDC could be extended to allow for a conventional way of handling it. Pull requests welcome. :)

  • I am absolutely with you about controllers. I’ve been thinking about doing something like this for a while now…kudos to you for actually doing it!

    I”m not positive that I like the particulars of your solution, but you’ve given me a reminder about this problem. Maybe I’ll do something about it now.

    Thanks!

    • Shawn Anderson Shawn Anderson says:

      Thanks Steve.

      It’s nice to know that I’m not the only one that feels this pain. Give DDC a try on your next project and/or post any solution that you come up with back here on Spin. I’d love to take a look at what you come up with.

  • Feña Agar says:

    The idea is somewhat good, however I don’t think it applies to a simple CRUD app.

    I see some usescases where a function in the contoller (while keeping the 7 basic functions of it) may need different contexts to work… not that I have seen this in many cases, but would allow you to work with different “contexts” in the same controller.

    Another use would be, if you allow yourself to move away from REST, you can use controllers as simple endpoints for a quickly done API allowing to pass different “contexts” to the “services” (or endpoints in this case).

    I’m just trying to justify the main idea, but anyway… there’s always many ways of solving different cases, this is just one more… just need imagination or be really in the need for it.

  • John Marston says:

    Goes to prove again that you can write Java in any language ;)

  • Milan Vladimirovic says:

    Hi,

    thanks for sharing your approach. I always like to see people come up with ways to better handle everyday coding tasks. I am sorry to see a lot of negative comments on this. Guess if you would tweak the syntax a bit more to a DSL or more towards ruby functions it would make people more at ease.

    That said: I was going to comment on your DDC Builder being just another way of writing a controller until I actually looked at the source to find out, what it is doing. I think your blog posts hides that you are actually also handling the error cases as well as setting instance variables.
    Maybe you should extend this article a bit to better showcase this.

    tl;dr
    Improve on syntax and better showcase error handling, format handling and instance variable assignment.

    • Shawn Anderson Shawn Anderson says:

      Thanks for your kind words Milan. I’m planning on doing a follow up post after using DDC for a while. I’ve made heavy use of DSL based syntax in Gamebox and wanted to try something more data driven. I’m definitely tempted to sprinkle a DSL on top of the current implementation.

  • Comments are closed.