Advanced Rails Architecture Way (Part 1)
“Your time is limited, so don’t waste it living someone else’s life”
Wasting no more minute let’s get down to study what Ruby on Rails is. Perhaps it’s the perfect framework for you to learn. Let’s go! What is Ruby on Rails? Ruby on Rails or RoR is a web framework written in the Ruby programming language. Sometimes people not very familiar with Rails say: “Do you know the Rails language?” Friends, Rails is not a programming language, it’s a web framework for web application development. RoR framework was invented by David Heinemeier- Hansson at Basecamp in 2004 and became increasingly more popular, being used by other successful internet firms like Hulu, Shopify, AirBnB, Twitter etc.
The theme of rails Architecture is very complex. Actually, Rails is a really cool framework. However, from time to time we can observe flaming, blaming and throwing mud at Rails in the developer community. But in fact, someone has written bad code, and blames Rails for his failure. Personally, we’ve learned how to use Rails and are able to find an elegant and failure-proof solution fast to almost any problem, or feature-request. Interesting to learn how?
The whole architecture of Rails builds upon the concept of a monolithic application, as opposed to microservice architecture, which splits single applications into multiple shards of small apps which are intertwined. The basis architecture that Rails provides us with is MVC: Model, View, Controller. MVC is present in a lot of frameworks which are used to develop user-facing applications, Rails just popularized this approach alongside with Python’s Django outside of the enterprise world, because it is a very simple approach to application design. The Model View Controller principle divides the work of an application into three separate but closely cooperative subsystems.
- Model — is responsible for work with the data: we insert data into the database or retrieve information via SQL or ActiveRecord, Rails’ layer on top of SQL. Model provides an interface and binding between the tables in a relational database and the Ruby program code that manipulates database records.
- View — it is a presentation of data in a particular format, triggered by a controller’s decision to present the data. They are script-based template systems like JSP, ASP, PHP, and very easy to integrate with AJAX technology.
- Controller — This is where the actual HTTP request by the client is handled: a typical controller action delegates database-related logic to a Model and user interface logic to a View with respect to the format requested by the client — HTML, XML or JSON for example. In other words, Controller it’s facility within the application that directs traffic, on the one hand, querying the models for specific data, and on the other hand, organizing that data (searching, sorting, massaging it) into a form that fits the needs of a given view.
On this stage a question arise: where should we include business logic? Every app is about business, not about the code, isn’t it? Business gives us money and we write code. That is a sequence, not vice versa. Developers new to MVC application design might assume it is put in the controller, because this is the point of entry/exit in the application. So, a task of a seasoned developer might be refactoring a messy and fat controller. Take a quick look at this sample:
This is a real screenshot from a project which we’ve dealt with. The fact it’s a project that earns millions of dollars, makes us think that the case is not in the code quality. This is our IDE (Integrated development environment). The piece of code pictured in the screenshot is called the Controller. Okay, what’s the matter? You can observe 137 lines in the IDE (the code is blurred, no need to gaze). We purposely сompressed the code, there is a special method in the controller called Create. This method takes 137 lines (77 controller methods) and we have 1024 lines. How did this happen?
We reached this amount of lines because we were not taught where to put the business logic in a timely manner. You may be suggested: take logic from Controller — put it in the Model. Meanwhile, the application grows bigger and we can keep fat Models organized by using mixins or extract business logic to POROs (plain old ruby objects). How the latter can be done nicely is described in the following.
In the first year of life of your application, the simplest thing you, as the tech-lead on the project, can do, is to enforce the use of architectural patterns in order to increase code quality. The pattern Service Object is known to all, in all languages and technologies. Patterns migrate between technologies, and visit Rails as well. The simplest thing you can do, is to extract shared or complex logic to newly introduced service objects, in order to keep the model layer organized.
If you open Wikipedia, you will find the definition of a Service Object. When some operation is happening in the business part of an application and it operates with several different data models, services are exactly what you need.
The service object pattern is simple, it is an object that has one method (default name would be call conventionally). This method accepts certain data, it’ll be good to accept hashes, because it’ll make the data easier to validate. On the screenshot above you can see the scheme of operations — what needs to be done.
And now an example of a controller, where the story begins — the innocent controller, which subsequently turns into 170 lines, sometimes it happens. It is very popular when it comes to the integration with external services. The picture is an example of the integration of the payment system Stripe: we determine the amount of the payment, the customer starts to do something, charge, do a redirect, rescue (for example if the card has not passed). Everything seems fine, the real troubles begin when we meet conditions (if, unless). Basing on the code above, we want to do a check out page, something like this:
Clean Ruby code that resolves everything.
So, you’ve been continuously developing your project for one year. You understand, that software architecture can’t be compared to normal architecture, the matter of building solid structures that are meant to not be changed for centuries. If software architecture is not flexible, and you cannot easily change it, it is bad architecture. The criterion of good architecture, is the ability to make changes to the application quickly and flexibly.
After the first year of development you will have an incredibly large number of services. Among basic: SOLID, DRY and KISS. We will talk more about this principles in the next articles. So you’re moving further and your file folder has become significant.
On the one hand, it is more than clear, that using only the services pattern is not sufficient. And you expand a project folder to such size:
Note: models, views, controllers and services are there too.
Levels Of Indirections
Developers often make a common mistake, which is described by the concept of levels of indirection. Levels of indirection is the process, when the layers of your application start to become redundant. We reccomend you to try to avoid this during the first year of development. As your application grows, you can’t know in advance what you will need in the future. Wait until your services will grow to the point where they become crowded. Then you will be able to group your services layers by function and maintain a set of different service layers, each representing an abstract and commonly used function.
We are proponents of the approach called progressive enhancement — a gradual complication of the software architecture. What if you try to keep your app simple, as long as you can, and complicate only when your team encounters or anticipate problems with increased code complexity? You will surely benefit from it. As the the practice shows, sophisticated architecture can be made very simple. It’s a difficult task to make the architecture easy to understand, flexible and changeable.
Take a look at the following screenshot:
As you may see, MVC is described as the HTTP level in the screen above. On the business level, when the services become large in number, the first thing you need to think about is separations of concerns. Here you can see, while queries are responsible for fetching data from the database, operations can alter data and emit destructive actions like email delivery. Policies are for enabling business requirements by enabling business logic validation. For example: A policy would check for how many users are registered already and how many are allowed to sign up.
Over time, you will feel that you do not want to repeat the same actions and will need something different to use. At this stage, various third-party libraries, that help to organize the business logic come to the rescue.
There are many libraries to be found in the width of the Internet, among my favorites is Active Interaction.
This is a library that implements service oriented design partially, while an interaction is the combination of an operation and business logic requirements like seen in policies.
Another decently popular library, implementing service oriented design, is Rectify, which is bigger compared to ActiveInteraction.
Rectify introduces 4 new software design layers:
Forms are responsible for validation. Commands are comparable to operations. Presenter — is partly responsible for the view. Query — responsible for data retrieval. If you use Rectify, your controller will look like this:
You can observe an example of Rectify used within a controller in the screenshot above.
When you observe the create action, you pass user input from the params hash provided by Rails to a newly instantiated form. Take a look:
The form will validate everything and perform all the necessary processes. That is, all errors with user input regarding business logic can be forwarded to the form. This is convenient because it can be reused between view, API and so on.
The form is then passed to a command, taking care of actually performing validations on the form. In case the provided data is valid, the command will execute potentially destructive actions like email delivery or changes to the database. This example makes use of a transaction, which makes sure all previous actions are rolled back, in case one of the actions fails.
Read the continuation in Advanced Rails Architecture Way (Part 2) soon.
The article is based on the Volodymyr Sveredyuk’s lecture at ITVechornytsi in Ivano-Frankivsk.