00:00:20.760
I hope you enjoyed your lunch and left some room for dessert, because we're going to eat some cake. Not a real cake, but rather a comparison between cakes and Rails architecture. Today, we're going to discuss how the world of confectionery can relate to building Rails applications.
00:00:33.300
So, what do we know about Rails architecture? Straight from the guides, we can see that Rails follows the MVC (Model-View-Controller) design pattern, and that’s a core design principle of the framework. Let's quickly recall: what is MVC, and what are its goals?
00:00:47.460
The idea of MVC comes from the 70s and 80s, originating from the Smalltalk programming language, long before I started my software engineering journey. I learned about MVC from a brilliant book on the ActionScript programming language. If you don’t know what ActionScript is, that’s okay; the language has become extinct but it was an enhanced version of JavaScript that helped us build a multitude of great web applications with Flash.
00:01:06.900
The definition from that book is still surprisingly applicable to Rails applications today. Essentially, MVC represents a separation of application components into three groups based on their responsibilities. The Model manages the state and business logic, the View handles the user interface, and the Controller translates user input into model updates.
00:01:29.220
However, in Rails, we do not fully adopt this mechanism of propagating model updates through events to view objects. Instead, we merely render. The controller is responsible for gathering data using models and passing it to the view for rendering purposes.
00:01:40.440
Yet, in Rails, there are tendencies to deviate from this diagram. For example, we can introduce a direct dependency between the model and view or make the controller responsible for business logic and state management. Furthermore, we can have the model generate data for the user interface, which deviates from the intended MVC usage.
00:01:54.780
Typically, instead of maintaining separation of concerns, Rails applications often suffer from a mix of responsibilities. This phenomenon could be explained by the limited number of baskets we have for all our eggs. Whenever we want to add new functionality, we are often left choosing between the controller, model, or view.
00:02:00.660
This limitation echoes in our cake analogy. When we bake a small cake, it’s common to have just three layers glued together with icing. This simplicity makes it manageable, as it doesn’t require special equipment to bake or transport small layers. Everything feels straightforward.
00:02:17.640
However, if we decide to bake a large cake but stick with just the three enormous layers, we’ll face numerous complications. Baking becomes harder; we must figure out how to uniformly cook each layer, transport such a massive cake without breaking it, and prevent it from collapsing.
00:02:30.240
Confectioners, however, have an elegant solution: instead of increasing the size of the layers, we simply introduce new layers. We can bake them independently and afterward glue them together with frosting or whatever suits our flavor.
00:02:54.060
This principle is akin to moving beyond MVC, as we introduce more layers in software development — in this case, abstraction layers. Today, we will discuss how to shift from a basic MVC design to a more sophisticated, yet efficient architecture.
00:03:09.300
Now, let me introduce myself officially. My name is Vladimir Dementyev, and I am an ActionScript developer. Previously, I built Flash applications, but nowadays I primarily work on Ruby projects.
00:03:19.560
You may have heard of some of them — many are open sourced. I contribute to Ruby and Rails regularly. You can find information about my recent contributions on the Rails Changelog podcast.
00:03:31.800
I highly recommend subscribing to this podcast to stay updated on the evolution of the Rails framework and learn about upcoming versions and changes. It's crucial for our talk today.
00:03:43.920
Additionally, I work at Evil Martians, where we're a product development consultancy, building amazing tools and features for exceptional clients. If you want to be part of this amazing journey, reach out and we will find ways to collaborate.
00:03:54.180
Lastly, this is my sixth RailsConf talk since 2018, marking quite a journey. Please let me know if you ever tire of my talks so I can consider skipping the next one — perhaps, not. Anyway, I enjoy this.
00:04:06.300
Now let's dive into layers on Rails and how to identify them. It’s easy to identify layers in a cake, so let’s simplify this. Every time I'm discussing layers, it's essential to visualize them clearly.
00:04:16.320
To understand layers in a Rails application, we can view it as a web application, where the primary purpose is to serve requests, usually HTTP requests. This goal defines the nature of the application and the way we structure our code, a significant difference compared to graphical user interface (GUI) applications.
00:04:31.560
In web applications, we lack a request-response lifecycle and independent units of work, which are defined as requests. The process of converting requests into responses can be pictured as an assembly line, where each workstation focuses on a specific part of the process.
00:04:45.540
We place the raw request on the belt and pass it through workstations before obtaining a packaged response. The core components in this assembly line are the Controller, Model, and View.
00:04:59.280
The Controller accepts the raw data; the Model performs the sophisticated work; and the View packages it as a response for the end user. Just as in real-life assembly lines, which aim to improve the efficiency of product building, we can enhance our software development process.
00:05:13.920
By modifying our processes and introducing new workstations, we can scale them effectively. In software development, we can do the same by incorporating more abstraction layers to improve efficiency and maintain distant concerns.
00:05:25.740
The goal of adding new abstraction concepts is to enhance the maintainability of the codebase, which invariably affects productivity and code quality. Maintainability serves as an umbrella for various qualities of code.
00:05:36.240
However, we will not cover all those qualities today as that can be overwhelming.
00:05:48.000
The critical question is: how do we introduce a new abstraction without decreasing maintainability or adding unnecessary complexity? Adding a fancy pattern or gem that introduces some abstraction may not be beneficial for the codebase.
00:06:02.380
I see this issue as akin to playing Tetris — if you add random abstractions merely because you want to, you won’t fit them well together, resulting in an overloaded codebase that becomes unmanageable.
00:06:14.920
However, adding a 'good abstraction' — by which I mean an abstraction that decreases complexity, hides common patterns, and enables easier use of specific features — is crucial for staying productive.
00:06:26.600
Now, let's discuss how to create a good abstraction for Rails applications. I've prepared a recipe with a few essential ingredients that we’ll discuss along the way.
00:06:39.540
Firstly, a good abstraction encapsulates internal logic while providing a high-level interface. We should aim for our abstraction layer to have a few related responsibilities instead of mixing entirely distinct ones.
00:06:53.520
Also, let's place a strong emphasis on testability, which is vital for productivity and usability. The main principle for our abstraction to work well within the Rails framework is to follow conventions.
00:07:04.260
When introducing your abstraction, ensure that it adheres to Rails conventions, as this contributes to making your new abstraction appear as though it belongs to Rails. Just follow the principles of least surprise.
00:07:18.540
The primary ingredient is following Rails conventions. What we intend to do is extend the Rails way, rather than modify it or break it. We are not reconstructing something here; rather, we are repairing our Rails application based on what already exists.
00:07:32.520
From a practical standpoint, learning how Rails operates internally is essential for this endeavor. By understanding Rails, we can manage to devise an abstraction interface that fits seamlessly into the framework.
00:07:47.520
Additionally, reusing Rails building blocks such as ActiveModel and ActiveSupport is significant, as it allows us to craft interfaces akin to the existing Rails structure.
00:08:01.600
The second ingredient involves determining where to place our abstraction in the assembly line. Each abstraction within a layered architecture holds its own designated place.
00:08:14.880
Comparing this to an assembly line, every abstraction has a specific location that aligns with the macro-level architectural design pattern.
00:08:27.840
It's crucial to separate components into horizontal layers, allowing each layer to depend solely on those below. For instance, in the presentation layer, which is the most significant, one must operate purely on their respective domain model.
00:08:43.920
This means presentation layer entities shouldn't depend on entities from lower layers to avoid leading to high coupling in the code.
00:08:58.560
In Rails, the presentation layer follows a specific model responsible for handling the interface with the end-user, usually the controllers.
00:09:12.180
A common example of violating this principle is passing parameters from the controller to the model, which can invoke unexpected behavior.
00:09:26.160
Therefore, we can avoid these kinds of pitfalls by ensuring that presentation layer entities are not passed directly down to application core structures.
00:09:36.720
The concept of closed layers and regulations allows us to control how deep we can go from each layer. By permitting the presentation layer to access only the application and domain layers, we preserve a healthy architecture.
00:09:53.040
In many Rails applications, we might encounter instances where every controller has an additional interactor object, even if it serves as a simple model save. The resultant architecture often leads to excessive boilerplate code.
00:10:06.780
However, we shouldn’t delve deeply into strict layer separation. Instead, we can employ ideas from paradigms like domain-driven design without overdoing it for Rails applications.
00:10:19.140
The most important idea is that while one architectural layer can possess multiple abstraction layers, each abstraction layer must belong solely to a single architecture layer.
00:10:35.880
For example, if a model is responsible for generating CSS classes, it becomes a part of the presentation layer, which can disrupt the architectural integrity.
00:10:45.300
To address this, we need to introduce another abstraction and relocate the CSS-related code into the view layer to maintain clean separation.
00:10:58.500
Let's go over a quick example of how to apply layered architecture principles to ascertain which layer an object belongs to.
00:11:06.540
Imagine we have an Authenticator class, which retrieves a token from the request, returning a user object after processing.
00:11:14.760
How do we determine which architecture layer this class belongs to? We can identify this by analyzing its dependencies on other objects.
00:11:22.200
If it relies on request-specific attributes, it clearly exists within the presentation layer.However, it also accesses the infrastructure layer, leading to a violation of the layered architecture principles.
00:11:31.740
So instead, we should convert this into an application-level object that solely deals with tokens and users without needing to know where the token originates.
00:11:40.020
This conversion lets us reuse the object with other presentation methodologies, such as controllers, channels, or web sockets, maintaining separation between concerns.
00:11:50.760
In summary, the layer of an object is determined by the highest level of its inputs and dependencies. This is fundamental to extracting code effectively.
00:12:02.820
The second ingredient connects us with the layered architecture ideas. We can adapt these concepts within our Rails application without strictly enforcing them.
00:12:15.720
The final ingredient will help us understand how to choose new abstractions. A common question arises: Where can I find a catalog of abstractions?
00:12:27.180
Unfortunately, instead of seeking a silver bullet abstraction, we should shift our focus to how to properly extract abstractions from the code.
00:12:39.420
You see, adding abstractions indiscriminately typically leads to increased conceptual complexity within an application. Instead, we should aim to add new concepts only when necessary to address existing issues.
00:12:54.420
Focusing on extracting rebundles of code aids in clarifying the reasoning behind establishing certain abstractions. To assist in this process, use analysis tools like Tractor Gem to conduct complexity evaluations.
00:13:06.480
By implementing layered architecture principles, we can ascertain where components belong and effectively abstract further.
00:13:20.520
Now, let’s jump to practical extraction examples. For starters, we’ll tackle webhooks. Picture an application that monitors GitHub activity for users, specifically tracking open pull requests and issues.
00:13:35.460
Our implementation in Rails often starts with a controller parsing incoming request data into a hash and identifying user logins.
00:13:48.480
However, the GitHub webhook format can be quite variable, so consistent parsing may require some adjustments. This initial approach can yield results.
00:14:02.460
However, there’s a catch: the event object isn't part of our model but instead belongs to an external third-party service that we can't control and can create dependencies within our model.
00:14:16.080
To rectify this situation, we can define a new model within our domain to represent a GitHub event. This centralizes mapping functionality and mitigates any hacks around parsing logins.
00:14:29.640
This clean approach allows us to pass our domain event object directly to our model without again worrying about parsing remote event hashes or JSON.
00:14:44.880
This abstraction results in a much cleaner codebase. Now, let’s address another common Rails pitfall: offloading all responsibilities onto the user model.
00:14:59.040
A common issue arises when all logic is crammed into the user model, leading to so-called 'God objects' — excessive weight placed on one class.
00:15:10.440
Let’s consider extracting responsibilities into a service object to maintain clean separation of logic.
00:15:22.440
In our case, we ideally want to identify a service object that will handle any related business logic, taking another step towards maintainability while keeping our user model lean.
00:15:38.520
While service objects have gained a significant reputation within the Rails community, we should be wary of simply placing code into the app/services directory without understanding their purpose.
00:15:53.520
Service objects should serve as a transitional phase in our abstraction journey, not as permanent fixtures that excessively increase complexity or lack purpose.
00:16:07.500
Any created service object could encapsulate specific logic while we wait for a more robust abstraction to emerge.
00:16:17.640
As our application evolves and we identify more webhook-related features, direct them to meaningful abstractions rather than cluttering existing models with miscellaneous methods.
00:16:31.680
Now, moving onto our final example about interface models. We often encounter models burdened with too many responsibilities within Rails applications.
00:16:43.560
Let's examine an annotation form that consists of a single field and checkbox, responsible for sending emails and creating users.
00:16:58.560
Initially, attempting the basic Rails approach involves using models and controllers where our model class encompasses multiple virtual attributes.
00:17:12.240
However, one must recognize that calling mailers from models should be avoided, as mailers fall outside the domain layer.
00:17:27.240
This practice can lead to elevated complexity, as models shouldn't manage business operations like notifications.
00:17:42.240
Instead, the encapsulated functionality should be placed in the application layer, particularly within the controller.
00:17:56.520
By employing a form object pattern in Rails, we can handle user input, verifying and transforming it effectively while invoking business level operations.
00:18:08.760
A common misunderstanding lies in the assumption that form objects should also accommodate rendering responsibility.
00:18:22.920
The focus should remain solely on submission handling, and rendering can be an optional functionality.
00:18:37.320
To illustrate, let’s extract this logic into a pure Ruby object containing the form data and methods for form submission.
00:18:51.240
This method localizes logic specific to the form, enhancing maintainability in our architecture.
00:19:08.880
However, we run into issues with typecasting and parameter handling within our form object.
00:19:22.620
The first problem lies in how form attributes are defined, as they are specific to the form interface rather than the domain.
00:19:35.400
With the controller attempting to deal with parameters, this complexity spreads across multiple locations, decreasing readability and maintainability.
00:19:48.840
Leveraging common methodologies and running the form approach through clear business processes helps consolidate functionality, enhancing overall clarity.
00:20:03.720
Fully realizing form objects requires recognizing common tasks and defining interactions they manage through a coherent interface.
00:20:18.360
Incorporating context-specific logic such as sending notifications must be tangibly situation-specific, while also conforming to the Rails structure.
00:20:32.760
In doing so, we facilitate a seamless integration into ActionView compatibility while adhering to Rails conventions, thus enriching the development experience.
00:20:47.520
Ultimately, we develop abstractions that deliver cleaner application architecture, spanning beyond forms, but applying to various facets of Rails development.
00:21:02.520
This recipe of abstraction involves not just isolation of logic but analyzing components as they expand, testing efficiency, and coding a reusable pattern for future enhancements.
00:21:17.520
While pondering the possibilities, you may wonder whether one can utilize non-Rails abstractions. Yes, experimentation in programming is always valid.
00:21:29.520
Altering ingredients and experimenting with combinations can yield advantageous results, as long as the core objective remains unequivocally aimed at improving coding efficiency.
00:21:43.740
Another question weighs on many minds: how many layers of abstraction are sufficient? My response: it depends.
00:21:56.520
Nonetheless, I will showcase examples from various projects that employed additional layers exceeding the original three-layer cake analogy.
00:22:09.720
Although it’s amusing, my favorite cake has five layers. It’s challenging to find similar cakes here in the United States.
00:22:22.500
In our remaining minutes, I'd like to highlight that numerous aspects are missing from this talk, being a short 40-minute session.
00:22:36.240
To reflect upon these ideas deeper, I must express that I plan to write an entire book exploring the extended Rails way, focusing on introducing new concepts and abstractions.
00:22:50.520
These concepts can extend productivity without deviating from established Rails practices, accommodating the evolution of codebases.
00:23:05.040
This book is set to be released this year. Thank you for your attention, enjoy the cake, feel free to request stickers, and ask any questions.