In the beginning of using rails it was amazing at how fast everything was compared to my previous java web experiences. I could finally run free without chains, I could build anything. Well, this faster had side effects. It was only a siren song to dull my design sense. The "rails way" becomes so convenient that I follow it even when I should run the other way.
Finally one day early this year I was monkey patching conditional validation support in active record when the soma wore off and I remembered if I design my code well I won't need to fight against the framework. Lately Uncle Bob has articulated and reinforced the idea with his screaming architecture blog. Convention over configuration is a heuristic, not a rule. So, I have decided to share some of my findings of where the "rails way" is a slippery slope. This blog is specifically about the rails controller.
- The user interacts with the user interface in some way (for example, by pressing a mouse button).
- The controller handles the input event from the user interface, often via a registered handler or callback, and converts the event into an appropriate user action, understandable for the model.
- The controller notifies the model of the user action, possibly resulting in a change in the model's state. (For example, the controller updates the user's shopping cart.
- A view queries the model in order to generate an appropriate user interface (for example the view lists the shopping cart's contents). The view gets its own data from the model. In some implementations, the controller may issue a general instruction to the view to render itself. In others, the view is automatically notified by the model of changes in state (Observer) that require a screen update.
- The user interface waits for further user interactions, which restarts the control flow cycle.
Point 2 says our first responsibility is to "handle the input from the user interface, and convert the event into an appropriate user action, understandable for the model". So, rails came up with injecting the data from the params in to the models through the constructors. Everyone has seen this method:
1 def create 2 @project = Project.new(params[:project]) 3 respond_to do |format| 4 if @project.save 5 ... 6 end 7 end 8 end
Tip: Inject understandable data
I think not having to map html names to object accessors is a great abstraction. Let's leave it there though. Only on the basic 1-1 data to model value mappings. Once you get to fancy, you end up having to normalize the data in the model. When you are changing the data in the model, what you are doing is tying yourself to the specific view implementation. However, the point of MVC is to isolate the M from the V. Models should already be able to understand the data passed to them.
Point 3 says the controller notifies the model of the user action. The key word is notifies of the "user action," not does the action. In that case an action might look like this:
1 def assign_user 2 assigner = StoryAssigner.new(:story_id => params[:id], :assigned_user_id => params[:story][:assigned_user_id]) 3 assigner.assign_to(assigned_user) 4 end
Tip: Business models to implement user actions
Use models that are not Active Record data models, but instead behavior models that manipulate the Active Record models. These models perform the business logic of the action. This is also where you get to put your designers hat back on. With thin controllers and thin Active Record models, it means we have quite the business logic layer. This means no data updates from your controller!
Point 4 has two parts relevant to the controller. It talks about the controller telling the view to render itself(rails does a really good job providing us with the respond_to block and the render options). The other part is how we relay information to the view. Rails let’s you set instance variables in your controller that you can later use in the view. This is a really convenient way to pass data to the templates without having to create a formal contract between the two that needs to be updated with every change. It looks something like:
1 class ProjectsController < ApplicationController 2 3 def index 4 @iteration = Iteration.find(params[:id]) 5 end 6 7 end
1 = form_for [@project, @iteration], :remote => false do |f| 2 -@iteration.days.each do |day|
Tip: Views want data closest to key-value pairs
This convenience can turn awry quickly though if you start sending ruby objects back to the view. Then the views start to contain logic and make decisions based on those objects. We want to use this rubric from the C2 wiki, "We need SMART Models, THIN Controllers, and DUMB Views". Why? Cause logic in views means we need to start making nil checks. What if days in the example is nil? Or some of our method calls could have side affects (rails callbacks bite you again?). Also, and most importantly, it is hard to test a clever view.
Both the view and the controller tests should be real simple, since they are about data display, structure and, message dispatching. All the rocket science happens in the business models. So, cleanest, dumbest view possible is doing nothing more than key-value replacements.
Remember, rails is there to make some things convenient. There is the possibility to make everything convenient, which is when you need to tie yourself to the mast of your ship and sail through the storm. So keep your controllers thin, fit, clean, and well tested.