Talks
Exploring Rails anti-patterns
Summarized using AI

Exploring Rails anti-patterns

by Elle Meredith

In the talk 'Exploring Rails Anti-patterns' presented by Elle Meredith at RubyConf AU 2024, the focus is on identifying and rectifying common anti-patterns in Ruby on Rails applications that can lead to performance issues, reduced developer velocity, and maintenance challenges. The session highlights the importance of clean architecture and provides practical solutions through examples from a simple app involving users, their addresses, and orders.

Key points discussed include:

- Definition of Anti-patterns: Common, yet ineffective approaches to recurring problems that can clutter code and hinder performance.

- Refactoring: Improving code without changing its external behavior and ensuring tests are in place to confirm functionality.

- Fat Controllers: The dangers of overloading controllers with business logic and conditional statements, suggesting the use of service objects for better organization. For instance, using a service object for handling orders reduces complexity in the orders controller.

- Avoiding Non-standard Actions: Emphasizing the importance of RESTful routes and architecture in controller design. An example is restructuring a controller to separate password-reset functionality into its own controller for clarity.

- SQL Query Optimization: Advocating for the use of Active Record queries to simplify and reduce SQL complexity via eager loading methods, and monitoring n+1 query issues with tools like the Bullet gem.

- Managing Relationships: Discussing the principle of low coupling by using delegation or presenter classes to manage object interactions, thus minimizing nil errors and preventing deep associations from complicating code.

- View Logic Management: Recommendations to keep views clean by outsourcing logic to helpers or decorators, avoiding logic-heavy views that violate the DRY principle.

- Model Responsibility: Encouraging the separation of model responsibilities to avoid God objects or fat models, suggesting that service objects encapsulate specific functionalities for better maintainability.

The talk concludes with a strong emphasis on maintaining a clear architecture through vigilance against anti-patterns in Rails applications. By adhering to good design principles and ensuring business logic resides in appropriate locations, developers can enhance both performance and maintainability of their Rails codebases.

00:00:03.640 Hi, I've given this talk a couple of times before; one of them was at a Ruby Melbourne Meetup a few years ago. A friend, Adam Rice, said, "Hey, I watched your talk that you’ve given, and it’s actually quite relevant. I think you should submit that to RubyConf." I said, "Oh, right, sure, maybe I will," and then I did. It got accepted, so I asked Adam, "Hey, can you give me some feedback on things I can improve?" He said, "Sure, here are some questions." I’m going to include some of his questions in this talk.
00:00:30.679 I have also changed the talk a bit; I've created an app to ensure that all my examples work the way they do in Rails 7. However, I decided not to use slides but to actually show you that in the app because I thought it would be clearer. In this talk, I will jump between controllers, views, and models—not necessarily in the typical MVC order.
00:01:07.720 I've created a small app for this talk. It involves a domain model that has a user, and the user can have many addresses. The addresses could be either a home address or a billing address. We also have an order that belongs to a customer, and an order can have many line items. We will use this very simple app to explain some of the concepts I want to cover.
00:01:44.799 First of all, what are anti-patterns? Anti-patterns are common approaches to recurring problems that ultimately prove to be ineffective. They are repeated patterns of action or processes that initially appear to be beneficial but ultimately do not work as well as we thought they would. These have bad consequences and very low beneficial results.
00:02:05.479 We also need to consider refactoring. Refactoring involves changing the internals of the code without altering the external behavior of the code. To effectively refactor, we must ensure that we have specs or tests in place to confirm that we aren't adding more functionality but instead improving what we have, making it better without changing what the end-user sees.
00:02:51.480 Let's start with fat controllers. Fat controllers typically contain business logic that probably should live in a model or a different object. Keeping that logic in the controller makes it harder to reuse and test all aspects of that logic. Common issues include overloading controllers with too much logic, having numerous conditionals to determine what to render or where to redirect, and spaghetti SQL queries that are unnecessarily long.
00:03:19.000 For instance, we can take a look at an orders controller that tries to accomplish too much. It attempts to decide if a request is for creating a new order or converting an existing one, processing payment, and sending confirmations all at once. This is just one example I pulled from a project I previously worked on. It also updates the session and uses a non-standard action name called 'placed' instead of usual REST actions. A better way would be to move that business logic to a presenter, service object, or model. In my case, I opted for a service object.
00:04:27.199 I initialize that service object within the Order model and call all the actions I want to perform. So why is this better than having the logic in the controller? First, this is a more appropriate location to house business logic. Testing the model is easier than testing it within the controller. It’s also easier to reuse when it’s not confined to the controller.
00:05:07.720 Now, if we revisit the same controller, I simply instantiate a PlaceOrder service object, passing in the order and calling its method. Although some people use different naming conventions for service object methods—like 'call', 'perform', or 'run'—the key is to keep it consistent throughout your application. This approach is much cleaner for managing business logic.
00:06:35.840 Moving on to another common issue, let's take the users controller. Here we see multiple problems, like a non-standard action and overcomplicated code that makes testing and understanding difficult. We see double negatives in conditional checks that should be avoided. We also have unnecessary parameters being added to the URL, and it constructs flash messages in an unclean way.
00:07:02.039 To fix these problems, we should avoid adding extra parameters to URLs and fully embrace RESTful principles. As applications grow, controllers often accumulate non-RESTful actions that should be extracted into their proper RESTful resources. For our case, we stripped the index action of password reset functionality, assigning it instead to a PasswordsResetController that manages actions specific to password resets. We created actions for 'new'—which displays the form—and 'create'—which handles password creation.
00:08:39.279 We use a service object for resetting the password, passing in the parameters, ensuring a much neater structure compared to the previous controller's action. Now, let’s discuss long, messy SQL queries. The best practice is to utilize Active Record queries directly in your model, which can significantly reduce the length and complexity of your database calls.
00:09:06.279 We should aim to utilize eager loading capabilities in Rails to reduce the number of queries executed. For example, when retrieving orders along with their line items, without eager loading, Active Record may trigger several separate queries—one for the orders and additional ones for the line items for each order. Instead, you should use `includes` or `preload` to load associated data in a single database call.
00:09:59.720 Identifying n+1 query issues can be done using tools like the Bullet gem, which can alert you to potential problems in your code base. Rails now has a built-in strict loading mode that can automatically detect and alert you about n+1 issues. You can also use Mini Profiler alongside various other tools to monitor and optimize performance.
00:11:14.640 Additionally, while designing your application, consider the principle of knowing too much about your neighbors. A user who has multiple addresses shouldn't have their controller directly accessed from deeper associations. Preconditions shouldn't require the order to know everything about the user and their address details—this leads to high coupling. Each object should ideally only be concerned with its direct relationships.
00:12:15.679 To solve potential nil errors in these situations, you could use the delegate method, where attributes like street or billing address can be accessed without traversing several relationships. A more elegant solution may involve creating a presenter class that abstracts those relationships, allowing you to retrieve complex data simply while keeping the structure of your objects clean and manageable.
00:13:52.960 The idea is to avoid cluttering your views with logic. For example, if you find yourself writing conditional statements to check for the existence of user attributes, those checks can often be moved into service objects or presenters. For nil objects, consider implementing a Null Object pattern, creating a 'Guest' class or similar, which provides a default set of attributes to prevent nil errors in views.
00:15:43.679 For instance, instead of checking,” if current user is present,” we can instantiate a guest class if the user doesn’t exist, which allows us to use the same logic in the views without inserting conditionals directly.
00:16:53.599 Next, we should be careful about overloading our view files with logic. This can lead to views that are difficult to manage and violate the DRY (Don't Repeat Yourself) principle. To mitigate this, we can use view helpers, decorators, or presenters to cleanly encapsulate logic relevant to our views, making them easier to test and maintain while limiting the amount of direct logic handled in the views.
00:17:59.760 I suggest avoiding passing too many variables into partials. Instead, use local variables and explicit local assignment to clarify which data the partial expects, preventing magic variable confusion within your view. Also, remember to utilize magic comments introduced in Rails, which can define local variables required for your partials, making it clear what data needs to be passed.
00:19:07.959 Moving to models, be aware of fat models or God objects, which can lead to fragile structures that are difficult to manage. It's essential to maintain a clear separation of responsibilities within your models instead of letting one model handle many aspects of your application. When your model takes on too many responsibilities, it can become unmanageable and harder to test.
00:20:57.360 For example, if a model needs to handle multiple operations or conversions, consider creating separate service objects or classes that focus solely on those functions. This not only allows for cleaner structure and easier testing but also prevents cluttering your models with unrelated logic.
00:22:24.640 Service objects should encapsulate actions related to specific functionalities, making it easier to maintain and test without worrying about unintended consequences that can arise from a simpler, top-level call in a model or controller. Always consider whether your callback logic in Active Record is necessary, as overreliance can lead to complicated behavior that is hard to trace. Opt for service objects or observer patterns to maintain clarity in your code.
00:24:32.679 The key takeaway from this discussion is to keep your classes small and focused on single responsibilities. Refactor your code appropriately, but make sure you have tests to validate your changes. This will not only improve maintainability but also the performance and readability of your code base.
00:26:00.960 In summary, maintaining a clean architecture in your Rails app involves being vigilant about anti-patterns within controllers, views, and models. By focusing on good design principles and keeping business logic where it belongs, you can ensure that your Rails application is not only performant but also easy to maintain and extend.
00:26:51.920 Thank you very much.
Explore all talks recorded at RubyConf AU 2024
+14