Model-View-Controller (MVC) is a design pattern for structuring software systems. The MVC pattern separates application logic from the user interface. MVC stands for Model, View, and Controller, the three main components of the MVC web application architecture. Each of these components should operate as discrete units. The discrete nature of MVC components is crucial for app maintenance and testing.
We often hear that the best way to write clear and concise code in Rails is by following the “Fat Models and Skinny Controllers” approach, which refers to how the Model and Controller in an MVC architecture should ideally work together. Having a “skinny” Controller (which is common practice) means that all non-critical logic moves to the Model; this simplifies testing and maintenance. However, we suggest taking the “skinny” approach a step further: put both your Models and your Controllers on a diet. Often we focus on Models and Controllers, but what about the View? The View can also be “fat,” and in fact, we believe that the View also should go on a diet.
What is the Function of Each MVC Component?
To get a clear understanding of why we should keep all MVC components skinny, we first need to clarify what each of these components is responsible for.
The Model is a layer between a database and an application that stores business logic related to a specific entity. Each Model is responsible for a different entity, and for connecting this entity with other entities. Models are usually invoked by Controllers.
For example, let’s say that a user wants to sign up. To do so, they must enter their email, a username, and a password. All these attributes are received by a Controller, which tries to create a new user (within the MVC architecture, this user is conceptualized as a ‘Model Record’). The Model runs validations: it checks whether the email entered is in the correct format, whether the password is secure enough, and whether the email and login are unique. If validation is successful, the Model then persists data to the database and a
after_save callback is triggered. An example of a callback is a notification email that’s sent to a registered user and to an admin. If validation fails, the Model returns an error object that can be rendered and shown to the user.
Controllers are responsible for reading input data (requests), choosing appropriate actions (business actions), and returning the resulting output data (responses).
For example, a Controller receives data from a client, and the router chooses an appropriate action in the existing Controller. On the basis of the received parameters, the Controller performs the following actions:
- Authentication, when the Controller checks if a logged-in user is working with an app at the moment
- Authorization, when the Controller checks whether a user is authorized to perform an action
- Filtration of input parameters (
permitted_attributes), when an action controller provides an interface for protecting attributes from end-user assignment.
- Calls methods in Models or services.
- Determines which format to give information to a client (JSON, HTML, PDF, XML) and selects the right View.
A View is a visualization of the attached Model state. Put simply, a View is what the user sees. It’s the only MVC component that users interact with directly.
For example, a Controller chooses a template to represent user posts and inserts it into the proper layout. The View displays user information and contains some HTML elements, such as links to posts and a form for creating new posts.
What Happens If an MVC Component Does Someone Else’s Work?
- It becomes difficult to maintain code in cases when the Controller contains large methods that perform unrelated actions.
- Testing becomes almost impossible, as writing Unit Tests means testing concrete classes and modules that are responsible for specific functionality. It’s difficult to track a chain of method calls in “spaghetti code” (tangled code that includes a lot of unstructured and difficult objects and methods).
- Every class should be responsible for a single part of the total functionality provided by the software. But often Controllers start including business logic that doesn’t belong or even forms of View logic. Model can end up including logic that isn’t related to persistence, and Views can contain calculations that aren’t welcome there at all.
The main rule we must follow to avoid fat MVC components is summed up as follows: “Everyone should mind their own business.”
How Can You Make MVC Components Skinny?
To achieve skinny Models, Views, and Controllers, we have to constantly refactor. Refactoring is a process of restructuring existing code. While refactoring doesn’t change anything from the perspective of the end user, it helps keep code clean, maintainable, and easy to test, which is beneficial for developers.
Refactoring is following the simple principle that if you make a mess, you should clean up after yourself. Refactoring is the constant cleaning up that happens after all code changes. You can’t build a skyscraper or paint a masterpiece without a lot of mess in the process, and it’s the same with writing quality code. That’s why we need to refactor each time we implement a new feature.
We can use various design patterns to aid us in the refactoring process, including:
- Service Objects (and Interactor Objects)
- Value Objects
- Form Objects
- Query Objects
- View Objects (Serializer/Presenter)
- Policy Objects
To get a better understanding of how refactoring works and when it’s necessary, we’ll highlight some practices we often use at RubyGarage in the next post.