Talks
Refactoring Fat Models with Patterns
Summarized using AI

Refactoring Fat Models with Patterns

by Bryan Helmkamp

In the presentation titled "Refactoring Fat Models with Patterns," Bryan Helmkamp discusses the challenges posed by "fat models" in Rails applications, emphasizing the importance of adhering to the Single Responsibility Principle (SRP). He highlights that as applications evolve, complexity increases and often leads to models that become overwhelmed with responsibilities, resulting in a tangled codebase. To address these issues, Helmkamp introduces seven patterns for managing complexity in the domain layer, aiming to refactor fat models into manageable, smaller objects.

Key points discussed include:

- Greenfield vs. Brownfield Development: Initially starting with ideal conditions in new projects (Greenfield) often leads to complexity and technical debt over time as requirements change.

- ActiveRecord Overview: ActiveRecord simplifies adding new models and CRUD behaviors but can lead to tightly coupled domains and bloated models.

- Value Objects: These encapsulated objects manage complexity by focusing on value rather than identity, allowing for better organization of code. For instance, encapsulating rating logic into a dedicated Rating object helps streamline functionality.

- Service Objects: By encapsulating standalone operations, these objects help keep models lean and organized. For example, separating authentication logic into dedicated service objects can clarify code and reduce the complexity found in a user model.

- Form Objects: Useful for handling forms that interact with multiple models, they simplify validations and state management, ensuring that users are taken through processes without directly manipulating ActiveRecord objects.

- Query Objects: Enabling encapsulation of database queries, these objects prevent overly complex class methods, allowing for cleaner, more maintainable code.

- View Objects: These handle the logic related to the presentation layer, which should not be intermixed with models, thereby clarifying the separation of concerns.

- Policy Objects: Useful for encapsulating business rules, these objects help maintain clarity regarding the logic behind conditions like sending user notifications based on specific criteria.

- Decorators: These provide the capability to enhance objects with new behaviors, such as conditionally triggering notifications while keeping the core model simpler.

Helmkamp emphasizes that while growth in complexity is inevitable, structured refactoring with these patterns can maintain manageable, effective applications without over-engineering. He concludes with reminders about using appropriate patterns to prevent unnecessary complexity in evolving software projects.

00:00:18.480 I'd like to begin with a bit of a group therapy session. I want you to think back to the last time you worked on a completely Greenfield application—one with no technical debt, no legacy code, and a comprehensive test suite that ran very quickly. Picture that; just you and a list of features that you needed to implement in your editor. Can you give me a word for the feeling you might have had during that experience? Fantasy? Anyone else? Joy? Yes, high school. This doesn’t happen very often, right?
00:01:28.400 As you think about your application as a green field, however, as development continues, the complexity starts to increase. You begin adding people to the team, and the business requirements begin shifting around. It's kind of like letting a bunch of cows walk out into your green field. The problem with allowing cows into your field is that they have a tendency to quickly turn a green field into a brown field.
00:02:38.160 My name is Bryan Helmkamp, and I'm the founder of Code Climate. Today, we're going to discuss some ways to change that equation—giving ourselves ways to get back into a green field, or if you are fortunate enough to have a green field project where you still feel that joy, ways to maintain it longer. I’ll be presenting some very specific patterns for managing complexity in the domain layer. Before we begin, a quick warning: I’m going to cover seven patterns and a few other points, and it’s going to move quite fast. Please save your questions for the end. If we don’t have time, I’ll be here during the break and we can address them then.
00:03:23.840 Let’s start with a quote: "Rails makes it natural and easy to build large, well-designed object-oriented systems." Does anyone know who said that? I want to examine why that is. In that quote, the context for Rails mostly refers to ActiveRecord. If you think about the ActiveRecord pattern, we need to go back to where it was first documented, which is in Martin Fowler's book, "Patterns of Enterprise Application Architecture," which features a section on ActiveRecord. That served as an inspiration for Ruby on Rails' implementation of the pattern.
00:04:07.600 If you read Fowler's work, he articulates that the big advantage of ActiveRecord is simplicity. We feel this every day whenever we need to add a new model to our application and set up some sort of CRUD-like behavior. We even have generators for this sort of thing, making it very easy to get that done. However, the downside Fowler outlines—and which we also experience daily—is that ActiveRecord, by definition, tightly couples your domain layer to your database schema. This isn't just an issue because your database schema might change or you might want to stop using a relational database. The truth is that the tools you have to manage complexity in your domain are limited by the number of tables you have. If you have an application that has only a few tables but a lot of behavior layered on top of them, and you strictly apply the ActiveRecord pattern, you're going to struggle.
00:05:19.910 Sometimes, people refer to well-designed object-oriented code as "ravioli code." This can have a positive or negative connotation depending on the context, but in this case, I’m using the positive connotation. It means you've got independent units of code that are loosely coupled, just like ravioli sliding around in a bowl, collaborating together to feed you dinner. David Chelimsky coined the phrase "calzone code," which is more like what Rails often leads you into. You can only really consume one calzone at a time, and you're not going to feel very good afterward. In fact, this can lead to the creation of
00:06:25.920 God Objects. This is a common problem with Rails codebases. If I opened up any of your applications today—and you told me what your application did—I could probably tell you where your God Objects are. The number one is the User object; number two is the main core model of your domain. So, if you are an e-commerce company, your God Objects are probably the Order and User models. It varies by application, but that’s what tends to happen with ActiveRecord.
00:07:03.640 Solutions sometimes suggested to manage complexity in Rails applications involve phrases like "skinny controller, fat model." This approach aimed to create maintainable controllers, making them easy to work with. However, I think this has caused perhaps as much harm as good because you still end up with fat models, which are not particularly appealing. On the other hand, we should strive to combine skinny controllers with skinny models—some of which will use ActiveRecord, while others will simply be domain models that do not inherit from ActiveRecord. Today, we are going to look at ways to achieve that.
00:08:20.480 If you can read the line at the bottom that says "Can you read this?" then you’ll be able to read the code. If not, you can visit that URL, and you’ll find the code samples. It’s github.com/codeclimate and refactoring fat models should be one of the first repositories. Before we discuss patterns, let’s talk about an anti-pattern.
00:09:03.999 This topic has come up during the conference, and while I don't have an opinion on DCI because I've never used it, I do have a strong feeling about extracting mixins: I would advise against it. The reason is that, as we've seen and heard, mixins are basically a form of inheritance, which can create a subtle and complex inheritance hierarchy in your application, resulting in multiple inheritance that isn't very obvious. This method is akin to sweeping the mess in your models under a bunch of rugs.
00:09:40.840 For example, let’s say you have a User, and he has friends because it’s a social network. You might think to extract a mixin called "Friending," adding some associations and dealing with that logic. I don’t favor that approach because it makes it harder to see the areas where you can extract objects from the system, as you've basically spread it out. I've said for a while that any application with an app concerns directory is concerning. However, I've been told that Rails 4 now generates app concerns directories by default, so it seems I may have been getting trolled.
00:10:48.360 The first pattern I recommend considering is Value Objects. Value objects are small, encapsulated objects whose quality is based on their value rather than their identity and are generally, though not always, immutable. In the Ruby standard library, examples of this include `Pathname` and `URI`; some more complex examples would involve simple things like a `Fixnum`. Let’s look at some ActiveRecord code and identify potential problems, along with solutions using Value Objects.
00:11:41.440 Take a class called `Constant`, which is part of Code Climate. This class grades your other classes and gives them a rating based on what we call a remediation cost. You’ll notice a number of methods that repeat the word rating in their names. This is one way to identify missing objects: if you find repeated prefixes or suffixes in method names, it likely indicates there's another object to extract. In this case, we’re missing a `Rating` object. Currently, the rating logic is tied to the ActiveRecord object, and there's no way to operate with a rating instance outside of its associated Constant class. By creating a value object called `Rating`, we can encapsulate this rating logic.
00:13:05.600 We can define a factory method that creates a suitable Rating instance given an associated cost. Moreover, we can define methods that allow for clean string interpolation, add functionality for checking the next worst rating (where ‘B’ gives you ‘C,’ and ‘C’ gives you ‘D’), and even employ expressive predicate methods to check if one rating is better or worse than another. We also define `equal?` and `hash`, allowing us to use ratings as keys in Ruby hashes, which can be useful since we group classes by their ratings in several places.
00:13:38.960 To use these ratings, we simply define a method within our ActiveRecord class that instantiates the Value Object. If you're familiar with the `composed_of` method in ActiveRecord, it's similar to this concept—though it might be deprecated, partly due to its simplicity to implement manually. I prefer doing things by hand until they become cumbersome and then consider implementing more sophisticated tools. The implications of introducing the Value Object include comparisons, making them sortable, usable as hash keys, and easy to work with in strings. Whenever I have logic that tends to accompany an attribute, such as names (always consisting of a first and last part), street addresses (which include numbers and street names), or instances of primitive obsession, I use Value Objects.
00:14:25.600 The second pattern is Service Objects. Service Objects encapsulate a single, standalone operation within your codebase. This is a general term. I highly recommend the book "Domain-Driven Design" if you're interested. Service Objects should ideally have a short lifespan, often being stateless because they just instantiate and execute.
00:14:55.920 Let’s look at an example involving a User class that has two different methods for authentication. You can authenticate with a password using bcrypt or with a token coming over an API, necessitating a secure comparison method to prevent timing attacks. In practical code, the secure compare function typically resides quite far down the class, often buried within other methods. When you consider the context, it doesn’t make sense to have a user method for secure comparison, which is about comparing strings. Instead, we can introduce service objects – one for password authentication that manufactures a bcrypt password object for comparisons and one for token authentication that integrates the secure compare logic.
00:16:06.080 Now, we have a dedicated class that focuses on authenticating tokens and contains the secure compare method. Conceptually, that makes much more sense. When you implement the service object pattern, you effectively simplify your model, potentially eliminating what I refer to as callback hell, which marks situations where multiple models interact through an elaborate network of callbacks. Furthermore, we can dry up controllers with service objects. For instance, let's say we have both an API version of a controller and a standard user controller that share post-conditions to execute after saving a user. This common behavior can lead to duplication between the versions.
00:16:49.440 Instead, we can make behaviors opt-in rather than opt-out. If one of these behaviors is only sometimes required, you can choose to make use of the service object when you need that behavior and simply go back to using the model directly when you do not, such as for tests. I apply the service object pattern in various scenarios, especially when I have multiple strategies for performing similar tasks, like placing an order in an e-commerce application, which typically involves a lot of behaviors. I would always have a service object for that.
00:17:40.799 Using a service object becomes invaluable when coordinating between multiple models—for instance, creating an order that needs to align with saved credit card and shipping address tables, or when interacting with external services, such as a payment gateway. After someone purchases something, you might need to post it on Facebook to embarrass them in front of their friends. When you have an ancillary operation not core to the lifecycle of a model, like a job that cleans up old data in your application, that doesn’t warrant being in the model itself, I recommend moving it to a side service, letting your model stay clean.
00:18:37.680 Moving on to the third pattern—Form Objects. Form Objects address the issue of managing a single form that processes multiple models. You may receive a design mockup from your designer where the error messages are not Rails-style, which quickly triples the amount of work needed to address the design. If the form needs to write to two different tables, the effort multiplies even further.
00:19:31.360 So how do Form Objects help? They are particularly effective during create and update flows, such as new user sign-ups or profile updates. Let's look at an example of this in action. I've witnessed numerous cases that illustrate using the ActiveRecord object directly to achieve this, like using an `user` object for a text field where a user’s company name is input. In this scenario, you could have a callback trigger to create the company before you create the user. However, this raises several issues—like creating an object that’s sometimes treated as a string and just as often as an object, causing confusion.
00:20:56.160 Another concern is what happens if the user saving fails after the company has been saved. While it’s expected that this process runs in a transaction, you shouldn't have to worry about or question it. Moreover, testing a user without worrying about the company also presents challenges. So, the solution here is to create a Form Object. Using the Vertis gem for constructing Form Objects allows you to define attributes and apply behaviors from the ActiveModel library, including validations. You can create an initializer that accepts a hash, making everything work seamlessly. You can also integrate an `attr_reader` for the user within the form object and make it act like an ActiveRecord object with methods that handle attribute retrieval.
00:22:37.920 This Form Object allows construction of a company before a user through a clear persistence flow. The controller can handle it as if it were an ActiveRecord itself, with an initializer that establishes attributes and a save method that validates and either persists the data or returns false.
00:23:48.880 From this approach come several advantages: it layers aggregation around individual work units and separates the cases of single vs. multiple units. We’ve alleviated the responsibility of our ActiveRecord models and provided a venue for contextual validation. Most validations I encounter are tied to context, denoting rules to apply for new sign-ups rather than retroactively validating all previous sign-ups. In other words, using a signup object will validate creation of both user and company with aggregated models.
00:24:56.080 The fourth pattern is Query Objects. Essentially, query objects are Ruby objects that encapsulate a single way to retrieve data from your database. Let’s further explore this concept through an example. It’s common to have a class with methods that might be considered scopes or class methods in Rails to define accounts ready to be imported from a third-party system, where each account needs to run queries.
00:26:09.440 However, you'll notice how this can lead to duplication in queries and make it difficult to refactor. Moreover, when I open the account class, having a plethora of these SQL-heavy methods isn’t particularly insightful into core account behavior. A solution is extracting Query Objects where you create a standalone object representing the accounts needing importation. In this case, to work with importable accounts, you would instantiate the query object and invoke methods to paginate through relevant accounts.
00:27:21.440 This method retains the primary focus of the ActiveRecord on defining what that model must represent while allowing for the composability of behaviors—much like ActiveRecord relations. This encourages placement of behavior in first-class objects rather than packed into class methods that can become overloaded and hard to manage.
00:28:10.880 The fifth pattern involves View Objects. There's much confusion over terminology here, but I use the term View Objects for the objects that back templates. Their relationship may involve zero, one, or multiple models. For example, consider LinkedIn: it offers a widget suggesting steps to complete your user profile, indicating progress along the way. Instead of embedding this logic into the user model, it would fit better into an OnboardingSteps class that can manage the logic necessary for displaying progression.
00:29:16.960 This design would allow clean method names, simplifying the return of messages and progress percentages. The more complicated the logic shifts from the model to the view layer, the greater the need to avoid coupling the two. For example, I would create OnboardingSteps as it allows for separate logic regarding what constitutes progress without conflating other user attributes.
00:30:35.600 The sixth pattern I want to introduce is Policy Objects. Policy Objects encapsulate a singular business rule within a designated object. For instance, determining the email a user should receive requires evaluating several conditions: checking if the user's email produced a hard bounce, if user settings enable notifications of that type, and if the email is tied to a specific project. These conditional evaluations comprise a core domain concept, which shouldn't dwell deeply within the user class.
00:31:45.840 Introducing an EmailNotificationPolicy aids in keeping this logic together, generating clarity around when notifications should be sent. With complex conditionals, clarification through Policy Objects streamlines logic management, allowing easier adjustments as business rules adapt over time.
00:32:58.880 The seventh and final pattern is Decorators. Decorators are utilized to wrap an object, adding additional behavior, often in views, but they can also be beneficial within the model layer. An example would be an Order that must send receipts only under specific conditions. Instead of embedding this logic directly in the Order class, we can create an OrderEmailNotifier that receives the order and invokes its save method, orchestrating the receipt-sending behavior without affecting the core lifecycle of the order.
00:34:36.640 You can implement the decorator in a seamless fashion, allowing the controller to handle context-specific requirements without altering the core model. This separation of concern is powerful, letting you manage objects and their behaviors flexibly.
00:35:02.800 In closing, as your application evolves, the intrinsic complexity of what it needs to deliver increases. Often, if you’re not performing enough refactoring or not fully considering your architecture, you can end up with a tangled mess. Yet, it’s important to carry out this evolution without over-engineering at the start. You don’t necessarily want sophisticated designs for applications that don’t warrant it, and it’s key to scale architecture gradually in accordance with increasing behavior.
00:36:06.720 Remember, though, it’s a balancing act. You want to avoid both extremes—neither a mess of code nor unnecessary complexity. Thank you!
00:37:30.400 Questions? Are we going to run the mics?
00:37:44.640 Great talk, actually. I just wanted to comment on one thing you mentioned.
00:38:06.720 So, we actually have Active Model already going to be something that in before, and we grabbed that from G Master and put it into our configurations.
00:38:32.200 Great, so you can use that today even if you're not on Rails 4. Anything else?
00:39:06.220 Yes, do you have a recommended place for all of those classes?
00:39:36.760 Well, it doesn't particularly matter. The key is having the right objects. Moving classes from one directory to another is a first-world problem. I would suggest keeping them organized as you see fit and then adapt the structure as the project evolves.
00:40:05.020 I'm curious about how code climate can assist in simplifying patterns instead of just measuring cyclomatic complexity.
00:40:29.360 Code Climate aims to guide developers through technical debt but cannot replace engineers. It's essential to continue learning and collaborating with your team to apply suitable patterns.
00:40:48.600 How do you manage dependencies when you have a lot of small objects?
00:41:07.920 In some cases, I handle it directly in the controller, creating a dedicated factory that outlines arrangements of those objects.
00:41:29.600 Others might implement dependency injection through initializers or attributes, allowing for easier substitutions, especially during testing.
00:41:57.520 Thank you all!
Explore all talks recorded at wroc_love.rb 2013
+30