How to organize your controllers in RoR
Ruby on Rails (RoR) is an MVC framework. Let’s think about what “C” means. “C” stands for controllers. The controller layer is where you collect and organize all of your business rules. That is, business rules and decision making ability generally DOES NOT leak into your M or V level, but instead these rules are grouped into named controllers.
For the purpose of this discussion let’s say we were trying to build the following application:
- 5 module complex work flow system. (e.g., automobile design aid: module 1 – spaceframe, module 2 – drivetrain, module 3 – aerodynamics, module 4 – environmentals, module 5 – controls)
- Each of these modules are fairly complex and should themselves be implemented as n-step wizards
- The modules can either be used in sequence to complete an end-to-end design project (XYZ Concept Car), or they can be used independently to assist in the design of a single subsystem or multiple independent subsystems. i.e., a project can encompass one or more modules
- Some modules will produce data that other modules can use to complete their particular function (for example: controls module needs to know about drive train and environmentals)
What’s the best way to organize the controllers in such an application?
The Naive approach.
The naive approach would be to make each controller represent a group of business rules or a decision making process. Using this line of associative reasoning, your sample workflow application, should be organized like this:
/controller/spaceframe_workflow_controller.rb /controller/drivetrain_workflow_controller.rb /controller/aerodynamics_workflow_controller.rb /controller/environmentals_workflow_controller.rb /controller/controls_workflow_controller.rb
And each step in your workflow should be a different action. Here, all of the business rules associated with your spaceframe construction workflow are grouped in the spaceframe controller. And all of the business rules relating to the drivetrain construction workflow are put into the drivetrain controller. Now what about actions?
So let’s say your spaceframe wizard/workflow has three steps. But what are steps? Steps are a further “breakdown” of business rules and decisions. The business rules & decision for each step would be in an action. Let’s say you were hosting your application at yourapp.com. And you had an action called “step1,” “step2,” and “step3.”
yourapp.com/spaceframe_workflow/step1 yourapp.com/spaceframe_workflow/step2 yourapp.com/spaceframe_workflow/step3 yourapp.com/spaceframe_workflow/finished
You could do the same thing for your “drivetrain” workflow:
yourapp.com/drivetrain_workflow/step1 yourapp.com/drivetrain_workflow/step2 yourapp.com/drivetrain_workflow/step3 yourapp.com/drivetrain_workflow/finished
So basically the mapping for your process is:
WORKFLOW -> CONTROLLER WORKFLOW STEP -> ACTION
No rocket science here…..I’m just reasoning from first principles: MVC.
Just as CONTROLLERS are about processes, MODELS are about things whose features you what to track. That’s where you would track features of your drive train and space frame.
THING WITH FEATURES YOU WANT TO TRACK > TABLE IN DATABASE (WRAPPED BY)==> MODEL
I’m just guessing but maybe there is something called a “space frame” with actually features that you want to track …. like height, width, weight, and price. Then you should have a table called SPACEFRAMES and a model to wrap that up called /model/spaceframes.rb.
/model/spaceframes.rb /model/drivetrain.rb /model/controls.rb
etc…. The model is where you model your world with THINGS THAT YOU WANT TO TRACK IN THE DATABASE. That model may or may not correspond in a 1-to-1 fashion with your workflow. There may be a complicated combination of features with different values of your model objects that need to interact to be able to make a decision in your action/controller/workflow/business rule. (E.g. If Controls.temperature * Drivetrain.mass > CRITICAL_HEAT redirect_to :action => ‘emergency_shutdown’) That’s why we separate those three layers.
If you wanted to actually edit/destroy/view the values of a spaceframe, then you would do that in the spaceframe controller ((/controllers/spaceframe_controller.rb)) as distinct from the spaceframe workflow controller (/controllers/spaceframe_workflow_controller.rb).
Your URLs for managing your things in your world woud look like this:
yourapp.com/spaceframe/list yourapp.com/spaceframe/view yourapp.com/spaceframe/edit
yourapp.com/drivetrain/list yourapp.com/drivetrain/view yourapp.com/drivetrain/edit
yourapp.com/control/list yourapp.com/control/view yourapp.com/control/edit
etc.
I think this is a pretty innocent and straightforward way to organize the engineering workflow app as described above, and it should suffice for the first iteration. A better approach would be to refactor even before you wrote a single line of code, by writing out your user scenarios and trying to see the patterns.
The modular approach
The naive approach of organizing your controllers in workflows is an acceptable way to do things but not the best way of organizing the code, because it’s not modular. What you’ll find is that after refactoring & modularizing your business logic, your business rules will tend be clustered around user actions and informed by your data model.
Basically, what you need to do is write down a list user actions. TRY TO FIGURE OUT WHAT THE USERS ARE REALLY TRYING TO DO. You can do this by writing out in detail the workflow, then extracting out the unique actions that the users are trying to accomplish. For example if this were eBay you would have two groups of users. BUYERS and SELLERS. First I’ll write down a list of actions that each user class will engage in.
SELLER:- Seller signs up
- post item details
- view bids
- accept a bid
- get notified by email that somebody bid on your product
- view & respond to questions on an item
- Buyer signs up
- view item details
- get notified by email that a bid was accepted
- bid on an item
- ask questions on an item
After making that list. You should, WHILE THINKING ABOUT YOUR DATA MODEL, GROUP THOSE ACTIONS INTO CATEGORIES like this:
SELLER:- Seller signs up – USER
- post item details – ITEM
- view bids – BID
- accept a bid – BID
- get notified by email that somebody bid on your product – EMAIL
- view & respond to questions on an item – COMMENT
- Buyer signs up – USER
- view item details – ITEM
- get notified by email that a bid was accepted – EMAIL
- bid on an item – BID
- ask questions on an item – COMMENT
THOSE CATEGORIES WILL END UP BEING YOUR CONTROLLERS. As you can see that we have extracted out five controllers for our eBay application:
- USER
- ITEM
- BID
- COMMENT
This is a more modular way of defining your controllers compared to the workflow-centered way of designing your app, because business logic will not be repeated over and over throughout the different workflows. What if you had 100 workflows. Would you want to encode the email notificaiton business logic 100 times in the 100 different controllers? Of course not. That would not be DRY. In the the eBay case, we have 2 workflows one for buyers and one for sellers. If we didn’t extract out the commonalities between the two workflows we would essentially be doubling our business logic code.
Notice that NOT ALL OF THE CONTROLLERS ARE DATA-CENTRIC. The controllers: USER, ITEM, BID and COMMENT are obviously data, but the EMAIL notification is more of a group of system actions, for example. So, while the data model should inform the grouping of the actions they do not fully determine the existence of the controllers. Some controllers may have actions without being part of the data model.
It’s in the views where you will talk to different controllers. For example, on the view item page in the ITEM view page, you will have a form that links to the BID controller for example. And on the BID view page, you might have a link to the COMMENT controller so that users could leave and respond to comments.
Posted by David Beckwith on Saturday, May 19, 2007