Talks

Rails as a piece of birthday cake

Rails as a piece of birthday cake

by Vladimir Dementyev

In the talk "Rails as a Piece of Birthday Cake" presented at RailsConf 2023, Vladimir Dementyev explores Ruby on Rails architecture, specifically focusing on the Model-View-Controller (MVC) design pattern. He compares it to a birthday cake, illustrating how the basic three-layer structure of MVC is sufficient for smaller applications but becomes impractical as projects grow larger and more complex.

Key Points Discussed:
- MVC Composition: The MVC pattern divides responsibilities among three core components: Model (business logic), View (user interface), and Controller (interacts with both Model and View). However, too often, Rails applications blur these roles, leading to poor separation of concerns.
- Scaling MVC: Adding new functionalities within the confines of MVC's three layers can lead to a cumbersome, tangled codebase. Dmitriev suggests that instead of enlarging these layers, developers should introduce new abstraction layers to maintain code clarity and manageability.
- Layers and Abstraction: Properly identifying and implementing multiple layers is crucial for crafting maintainable applications. Dmitriev discusses how to structure web applications as assembly lines where each layer handles specific tasks effectively, which directly impacts application performance and ease of maintenance.
- Best Practices for Abstraction: He emphasizes following Rails conventions, learning its internal workings, and applying layering principles while introducing abstractions. This method avoids unnecessary complexity while enhancing code maintainability.
- Practical Examples: The talk highlights practical examples where layers can be extracted: converting complex code into service objects and using form objects to encapsulate functionalities related to user inputs, demonstrating better organization and less coupling.
- Future Considerations: Finally, Dmitriev notes that while experimenting with other frameworks/tools is acceptable, aligning new strategies with Rails conventions is crucial to retain productivity and coherence in the codebase.

Conclusions: Dmitriev concludes that moving beyond a simplistic MVC model by embracing layered architecture can significantly enhance maintainability and clarity of larger applications. He emphasizes the importance of evolving Rails practices in alignment with its foundational principles and announces an upcoming book intended to further explore these concepts.

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.