Keep Models out of Your Views' Business!

One of the core goals of the MVC architecture is to separate the presentation of a model from its implementation and semantics. And yet Rails, out of the box, takes the representation of models---even the names of tables and columns in the database---right through to the user. The default behavior, for form labels and other places where models and attributes are referred to, is simply to use the name of the model or attribute.

Does this mean Rails' implementation of MVC is broken? Not at all! My favorite definition of "architecture" in software is "the set of decisions that will be hard to change later." And the best architectures are the ones that keep that set small, allowing developers to move quickly without fear of being boxed-in later in the project. Rails uses its default behavior to let you move quickly, but leaves your options open for changing that behavior later.

I think Rails' defaults work really well in most cases. After all, we should be working with a domain language that we share with customers and domain experts, so it's likely that our schema and models will use names that are appropriate for presentation. Nevertheless, it's common to reach a point where the user interface needs to use different terms for some of the domain concepts.

So let's see how Rails can help. Assume we've developed the "canonical" Rails application, a blogging engine. The primary model is called Post, and each post has a title and body. And then, the customer decides that the users will prefer writing "articles" rather than "posts", and "headlines" instead of "titles".

What can we do?

Breaking the Link: The Obvious Ways

The most obvious, brute-force solution is to avoid the defaults, instead supplying literal names in all of our views. While there are places where it will be appropriate to supply literal labels, this option should be reserved for specific places where the context requires a different label. Using this approach to change labels throughout the application would be way too much work, and create a maintenance nightmare.

You could actually change your model---rename the table, or column, along with every place that name appears in your Ruby code. But that approach would be tedious and error-prone if the name were central to your model. More importantly, it would be a mistake to change the domain model understood by those involved in the project just to satisfy presentation constraints. It's at this point that the MVC architecture needs to pull its weight by helping you deal with this problem.

Digging a little deeper, you realize that the places in ActionView that use model and attribute names (e.g., the label helper) transform them to "human" form first, removing underscores, adding spaces, and capitalizing appropriately. They do that by calling the human_name method (for model names) and human_attribute_name (for attributes). Every ActiveRecord model inherits those methods from ActiveRecord::Base. So one answer is to override those methods. That's much better than either of the other options, but it's still not very good. If you solve your problem this way, the model will be actively involved in presentation issues. We want to work with the MVC architecture, not against it.

Breaking the Link: The Right Way

The answer to our problem can be found in the documentation for ActiveRecord::Base#human_attribute_name:

This used to be depricated in favor of humanize, but is now preferred, because it automatically uses the I18n module now.

We can use Rails' internationalization support to supply the new names we want to present to users. Think about your current problem as specifying the English localization for your application.

If it's not already there, create the file config/locales/en.yml, with the following contents:

  en:
    activerecord:
      models:
        post: "Article"
      attributes:
        post:
          title: "Headline"

Under the en.activerecord key there are two sections that define presentation names for the locale: models contains names for models, and attributes contains names for ... well, I think you get the idea. So in our example, the new name for a post is "Article", and the new name for a post's title is "Headline". Note that you specify attribute names in the context of a model class, so if there were another title attribute in your system on a different model class, it would not be affected.

Suddenly, throughout your application, those changes will take effect. You can still supply different labels in particular contexts as needed, but the default is to use the names in the locale file.

Update: As commenter Tim Watson pointed out, it's not currently automatic everywhere. It does automatically happen in error messages, but not in labels (making labels handle this automatically is planned for an upcoming Rails point release). You can supply the proper label text on your own (using the human_attribute_name method). If you're using custom FormBuilders, you can easily encapsulate this change; otherwise, see commenter Priit Tamboom's solution for doing a quick monkeypatch of the label helper until Rails handles it properly.

As you may have gathered from the documentation excerpt above, you should use Post.human_attribute_name('title') instead of calling humanize on an attribute name when writing views. Likewise, you should use post.class.human_name rather than post.class.name.humanize or the literal 'Post' for model class names.

Also, associations are treated as attributes, rather than inheriting the new name of their target model. So if some model had an association to multiple posts, you would need to define an attribute renaming in the appropriate model; it would not automatically be called "Articles".

Finally, this approach doesn't deal with controller names in URLs; for that, you should modify the routes in config/routes.rb.

More To Come

There's more to this story, because there's another place where your models peek through into the user interface in undesirable ways. I'll address that in a follow-up post.