Refactoring

Summarized using AI

Refactoring Fat Models with Patterns

Bryan Helmkamp • February 21, 2013 • Earth

In the presentation "Refactoring Fat Models with Patterns," Bryan Helmkamp discusses the issues caused by "fat models" in Ruby on Rails applications, which violate the Single Responsibility Principle (SRP). As applications grow, models can accumulate excessive complexity, leading to maintenance challenges. This talk highlights patterns to effectively manage this complexity and emphasizes moving towards smaller, encapsulated objects instead of large, unwieldy ones.

Key points discussed in the talk include:
- Greenfield vs. Brownfield Applications: Helmkamp starts by contrasting the ideal state of a greenfield application, where developers can work without legacy issues, with the challenges of brownfield applications, where developers often feel bogged down by legacy code.
- Active Record Pattern: The presentation explains how the Active Record pattern, while beneficial for simplicity, leads to tightly coupled models which may grow too complex as additional behaviors and requirements are added.
- Fat vs. Skinny Models: Helmkamp advocates for the concepts of skinny controllers and skinny models, promoting a structure where both controllers and models maintain clarity and limited responsibilities.

Patterns Introduced:

  1. Value Objects: Immutable objects focused on a value rather than identity, simplifying code and behavior management.
  2. Service Objects: Standalone objects representing operations, which reduce the complexity in models.
  3. Form Objects: Manage multiple models through one form, providing a clean way to validate complex user inputs.
  4. Query Objects: Encapsulate specific database queries, separating SQL logic from the ActiveRecord models.
  5. View Objects: Facilitate the display logic related to views, maintaining clarity and separation from the model layer.
  6. Policy Objects: Focused on encapsulating business rules, enabling easier compliance with notifications and permissions.
  7. Decorators: Provide additional behavior to objects without altering the core functionality; useful for cases where behavior is conditional.

Helmkamp emphasizes using these patterns judiciously; introducing them at the right time can prevent backend chaos while ensuring the application remains manageable. The talk concludes with a reminder that incremental design improvements are crucial to keep pace with application complexity, aligning the architectural evolution with feature growth.

In summary, the takeaway from Helmkamp's talk is to refactor fat models into coordinated sets of responsibility-limited objects, which leads to a cleaner, more maintainable codebase. By adopting patterns such as value objects, service objects, and others, developers can effectively manage the increasing complexity of Rails applications.

Refactoring Fat Models with Patterns
Bryan Helmkamp • February 21, 2013 • Earth

"Fat models" cause maintenance issues in large apps. Only incrementally better than cluttering controllers with domain logic, they usually represent a failure to apply the Single Responsibility Principle (SRP). "Anything related to what a user does" is not a single responsibility. Early on, SRP is easier to apply. ActiveRecord classes handle persistence, associations and not much else. But bit-by-bit, they grow. Objects that are inherently responsible for persistence become the de facto owner of all business logic as well. And a year or two later you have a User class with over 500 lines of code, and hundreds of methods in it's public interface. Callback hell ensues. This talk will explore patterns to smoothly deal with increasing intrinsic complexity (read: features!) of your application. Transform fat models into a coordinated set of small, encapsulated objects working together in a veritable symphony.

Help us caption & translate this video!

http://amara.org/v/FGdK/

LA RubyConf 2013

00:00:24.480 So starting off this morning is Bryan Helmkamp. Did I pronounce that right?
00:00:30.000 All right, so Bryan. A couple of high points. Bryan currently runs a company called Code Climate. How many people are familiar with Code Climate? Okay, if you're not, check it out.
00:00:36.760 It's a great product. I started using it after RubyConf, after he showed me the demo at RubyConf. We started using it for our teams for the projects that we have.
00:00:43.000 In addition to Code Climate, Bryan is also heavily involved and is one of the lead organizers for Goru. I got clearance from him before using his Wi-Fi.
00:00:51.000 For those of you who aren't aware, Goru is a conference that takes place in New York City. It's a similar single-day, single-track conference. It's well worth the trip and a great excuse to go to New York.
00:01:00.000 Oh, one more thing I wanted to mention is that Bryan is also a Ruby Hero as of 2009. If you're not familiar with the Ruby Heroes program, Google it and check it out.
00:01:08.000 It's a community effort where we recognize about half a dozen members of the community each year for their extensive contributions to what we do and to help grow the community.
00:01:16.000 With that, I'm going to turn it over to Bryan for a discussion that is not about Enterprise Rails, if you saw that on the schedule.
00:01:23.000 Refresh your schedule; this morning we're talking about refactoring ActiveRecord.
00:01:29.000 Is this mic on? Can people hear me? Okay, cool. Thank you, Kobe. That was a very nice introduction.
00:01:35.000 I'd like to begin with a little bit of group therapy. So think back to the last time you were working on a completely greenfield application.
00:01:43.000 There was no legacy code, there was no technical debt. You had a test suite that was comprehensive but ran really quickly.
00:01:50.000 You had a backlog of features, and you were just in your editor, tearing through them. Can everyone think back to the last time they had that feeling?
00:01:56.000 Give me an adjective for how you felt when you were working like that. Just toss one out there.
00:02:02.000 Anybody got one? Fun? Productive? Anything else? What's that? Enabled? Yes, great. Sane.
00:02:09.000 So, the greenfield feeling is great, right? The problem is that as your requirements increase and you start adding developers to the project, and the requirements start changing, it's kind of like having cows enter your greenfield.
00:02:15.000 The problem with letting cows in your greenfield is that after a while they have a tendency to turn a greenfield into a brownfield.
00:02:23.000 Then you're sort of in that position where you feel like you're fighting your codebase. Have you ever had that feeling?
00:02:30.000 Where it feels like you have to change everything just to get one thing done, and your app is your adversary?
00:02:37.000 Then you have to beat it into submission. That's a brownfield.
00:02:44.000 So my name is Bryan Helmkamp. I'm the founder of Code Climate. It's a hosted automated service that reviews the Ruby code in your applications.
00:02:50.000 Today, I'd like to talk about one particular aspect of this problem as it relates to Rails applications.
00:02:57.000 We're going to look at ways to deal with complexity in our model layer and how we might manage that complexity more effectively.
00:03:04.000 This is to prevent our greenfields from turning brown.
00:03:10.000 As a quick warning, this talk is going to move really fast. I'm going to try to cover seven patterns in one anti-pattern.
00:03:17.000 With an intro and an outro in time for questions in about 40 minutes, so if you haven't had coffee yet, God help you.
00:03:24.000 I'd like to begin with a quote: Rails makes it natural and easy to build large, well-designed object-oriented systems.
00:03:31.000 Do you know who said that? I don't think anybody has ever said that, at least not to me.
00:03:38.000 But why is that? I think in that context when someone refers to Rails, they're really referring to the Active Record pattern.
00:03:45.000 So we have to go back to what the Active Record pattern is to understand this feeling.
00:03:52.000 Active Record was documented in Martin Fowler's book, Patterns of Enterprise Application Architecture.
00:04:00.000 If you go back to the source text, he clearly lays out some implications of using the pattern.
00:04:06.000 The biggest advantage is simplicity, and we feel this every day when we create a new model and layer on some quick and dirty CRUD sort of behavior.
00:04:14.000 We can plug it into forms and perform reads and writes from our database.
00:04:21.000 However, Active Records are, by definition, directly coupled to your database schema.
00:04:28.000 The problem with this is if you have a small database schema that isn't expanding all the time, and you restrict yourself strictly to the Active Record pattern,
00:04:34.000 the tools that you end up with to manage complexity in your domain and in your model layer are very limited.
00:04:40.000 If you only have five tables, but you have all sorts of behaviors layered on top of them and only five classes,
00:04:46.000 those five classes are going to become pretty problematic pretty quickly.
00:04:55.000 Sometimes when people talk about object-oriented systems, they use the term 'ravioli code'.
00:05:00.000 This can have a positive or negative connotation depending on who’s speaking or the context.
00:05:06.000 However, I'm going to use the positive connotation here: you have these little individual units of code that are loosely coupled.
00:05:11.000 They're all sort of sliding around in the same bowl, coordinating together to feed you dinner.
00:05:17.000 Rails is calzone code. I believe David Chelimsky coined this phrase.
00:05:24.000 If you look at the picture, you can get the feeling for why this fits.
00:05:31.000 Your Active Records tend to be these big chunks. If you eat more than two calzones, you're just going to feel really sick.
00:05:41.000 Today, we're going to look at ways to turn calzones into ravioli.
00:05:48.000 The problem with calzones is that they can grow into God objects.
00:05:54.000 If I were to look at your applications today, I would be fairly confident that there are at least two God objects in your application.
00:06:05.000 These are probably the user class or account class, whatever you want to call it.
00:06:11.000 If you're in any commerce application, I would bet that the user class and the order class are probably the worst code that you've got to deal with.
00:06:18.000 You probably dread going in there and making changes because who knows what's going to fly out the other side.
00:06:24.000 So how do we solve this? In the past, people have talked about the idea of skinny controllers and fat models.
00:06:30.000 They want their controllers to be limited in responsibility to do one thing and one thing well.
00:06:37.000 Then offload that responsibility to someone else so the controllers are very easy to understand and you don't feel like you have to fight with them.
00:06:44.000 Unfortunately, I think 'skinny controller' as a phrase has done perhaps as much damage as it has done good.
00:06:50.000 There's a clear alternative: skinny controllers and skinny models.
00:06:57.000 Notice the pluralization on models. There’s no reason we can't have the benefits of well-encapsulated objects with limited responsibilities in our model layer.
00:07:04.000 Just like we can have them in our controller layer. On to the patterns.
00:07:11.000 There’s going to be a lot of code in the remaining slides. If you can see if you can read the text on the bottom.
00:07:18.000 If you can't read that, you should pull up the GitHub repository which will have all the code samples in it.
00:07:26.000 It's github.com/codeclimate/refactoring_fat_models. If you don't have a laptop, just maybe you can share over someone's shoulder.
00:07:34.000 I want to begin with an anti-pattern. Whenever the topic of refactoring Active Records comes up, one of the first things people want to understand is this idea of extracting mixins.
00:07:40.000 When I say mixin, I mean Ruby modules.
00:07:47.000 So this would be like if you have a user class that's in a social networking context, and it has a couple of associations and some behaviors around knowing whether it's friends or not.
00:07:54.000 In this case, you might include a friending module or a blocking module.
00:08:01.000 Here's the problem: I don't really like this. I think that the behavior you get when you include modules is akin to inheritance.
00:08:08.000 You're essentially creating complex multiple inheritance hierarchies in your application.
00:08:14.000 What's worse is that you've created a junk drawer system.
00:08:20.000 If your user class has a thousand lines in it, it's kind of like sweeping those lines under five separate rugs.
00:08:27.000 You can't really see it now; it's all split up, but what have you really improved?
00:08:33.000 The worst part is, by having your complexity swept under five different rugs, it becomes harder to identify concepts that can be given first-class names.
00:08:40.000 I've been saying this for a while: any application with an 'app' concerns directory is concerning.
00:08:46.000 Come to find out, Rails 4 generates an 'app concerns' directory by default. I think I'm being trolled.
00:08:54.000 So let's go to the actual patterns. The first pattern is value objects.
00:09:01.000 Value objects are simple, plain Ruby objects whose equality is dependent on their value and not their identity.
00:09:08.000 As such, they're often, but not always, immutable.
00:09:14.000 We can look at an example in the Ruby standard library. You've got, in the Kernel, things like Fixnum and Float.
00:09:21.000 In the standard library, you have classes like URI and Pathname. Those are all great examples of value objects.
00:09:27.000 They layer on nice behaviors.
00:09:33.000 In this case, we have a fat ActiveRecord model.
00:09:39.000 This one's from Code Climate, and it’s Code Climate's representation of a class in your system.
00:09:45.000 We understand information about the classes that you have.
00:09:51.000 We might write a road to the database called a constant, which is for your class definition.
00:09:57.000 Every class gets a rating from A to F based on a cost that we associate with the class.
00:10:03.000 The cost is sort of our estimation of how difficult it would be to fix the smells that we've identified.
00:10:09.000 Based on the cost, the rating could be anything from A to F.
00:10:15.000 Now, the problem with this code is that the first thing I would notice is that there are three methods that all have a repeated word in the name or arguments.
00:10:23.000 That's almost always a telltale sign that you're missing an object.
00:10:29.000 When you start having repeated prefixes, suffixes, or words that travel together in method names,
00:10:36.000 a rating can't exist on its own.
00:10:42.000 The only way to do anything with a rating, like figure out if one rating is higher than another, is to have a constant that has that rating.
00:10:49.000 Then ask if the rating is higher than another constant's rating.
00:10:57.000 A refactoring that you could take here is to introduce a value object.
00:11:03.000 We'll introduce a class which is a Rating and give it a little bit of behavior to start.
00:11:10.000 So here, we've added a simple factory method that gives us a nice place to map between costs and the appropriate rating.
00:11:17.000 We've also defined the 'to_s' method so that if you put it into a string interpolation, it'll come back out just like you want.
00:11:23.000 By using Comparable and the spaceship operator, we now have ratings that can be sorted or compared against one another.
00:11:29.000 We can ask a rating what the next worst rating is.
00:11:35.000 So if you have a B, you want to get back C, C to D, and on.
00:11:41.000 We also have a convenience method for asking if one rating is higher than another.
00:11:49.000 By defining the hash and equals methods, we can use ratings as keys in Ruby hashes, and they work just like you would expect.
00:11:56.000 This is really useful for Code Climate because we do a lot of operations where we need to group different data by rating.
00:12:03.000 We want to show you all of the classes that have a D rating.
00:12:10.000 To initialize that, it's very simple.
00:12:16.000 You just define a method called 'rating' in your constant and use the factory method to produce the correct rating object.
00:12:24.000 If you've seen the ActiveRecord feature called composed_of, it's a macro to basically do the same thing.
00:12:31.000 It layers value objects into your models. I think it might be deprecated in part just because it's so simple to do it yourself.
00:12:37.000 Never be afraid to add a simple method like this that just returns an object.
00:12:43.000 It doesn't even need to return the same instance every time because we've set it up so that if you have two instances of the rating B, they'll always be equal.
00:12:51.000 You just create the object, and if the object passes out of scope, it gets garbage collected and everything's fine.
00:12:58.000 The implications are we now have value objects that can be made comparable.
00:13:05.000 They can be used as hash keys with the 'to_s' method, we can make it easy to interpolate them into strings.
00:13:11.000 They also provide a convenient place for factory methods.
00:13:18.000 I like to use them whenever you've got an attribute that tends to have logic that hangs around it.
00:13:25.000 URL, for example, often need to split out the host from the port.
00:13:31.000 Data clumps are a good example if you ever have pieces of data that travel together in your system.
00:13:36.000 It doesn't make sense to have one piece without the other, like a street number without a street name.
00:13:44.000 That's a good place for a value object.
00:13:50.000 If you find yourself using primitives in lots of places and contorting those objects into doing more complex behaviors, that's a good time to look at value objects.
00:13:57.000 The second pattern is fairly broad.
00:14:03.000 This is the idea of service objects, which are defined as objects that represent standalone operations within your code base.
00:14:09.000 They tend to have a short lifecycle, so I like to instantiate new ones and then use them once and throw them away.
00:14:16.000 Sometimes they are stateless, but if they're not stateless, that state will go away pretty quickly.
00:14:22.000 Let's look at an example. We have a user class that has two different ways that authentication can take place.
00:14:29.000 We can authenticate with a password using the BCrypt library. That might be the algorithm that our sessions controller uses.
00:14:37.000 We also have a method for token-based authentication, perhaps from an API.
00:14:43.000 This method, at the bottom, is necessary, as we need to do a secure compare function when validating the API tokens.
00:14:51.000 This ensures we avoid making ourselves vulnerable to timing attacks.
00:14:56.000 The problem is that the user class already has to include n different strategies for doing authentication.
00:15:03.000 If we wanted to introduce a third way, perhaps with a mobile phone, we're just continuing to expand the responsibility of the user object.
00:15:10.000 This secure compare method is right under the token authenticate method in this example.
00:15:16.000 However, in real life, that's likely buried at the bottom of your private method definitions, maybe 100 or 200 lines from the public interface.
00:15:23.000 It doesn't make sense to say what secure compare means in the context of a user.
00:15:30.000 There's a mismatch there. So how could we do it better? We could introduce service objects for the two different authentication strategies.
00:15:38.000 A password authenticator could use BCrypt to authenticate users that come in with sessions.
00:15:44.000 A token authenticator could securely compare the API tokens with the provided tokens.
00:15:51.000 Now, the secure compare function is still a utility function hanging out at the bottom.
00:15:58.000 At least the correlation between token authentication and secure string comparison is made much more direct.
00:16:04.000 There are no 200 lines of code sitting in between those two things.
00:16:10.000 Of course, they're easy to use in the controller.
00:16:17.000 We can simply initialize a password authenticator with the required user and then call 'authenticate' on the authenticator.
00:16:23.000 Instead of calling a method on the user, what do we get out of this?
00:16:29.000 We achieve simpler models—that's what we want. We avoid callback hell, where we have layers of callbacks coordinating across different objects.
00:16:36.000 It's hard to know what will happen with them.
00:16:43.000 We can also clean up duplication in the controller layer.
00:16:50.000 If you've ever had to add an API to an application, you might end up repeating the logic across the API and regular controller versions.
00:16:57.000 For example, if there are side effects after someone places an order, you can extract those into a service object.
00:17:04.000 That way, you can use it in both controllers.
00:17:10.000 When you do so, it has the advantage of making behavior opt-in instead of opt-out.
00:17:17.000 Your ActiveRecord model maintains its simplicity for interacting with the database.
00:17:24.000 You know what's going to happen, but when you need higher-level behaviors, you can reach for the service objects.
00:17:30.000 I like using service objects in this case.
00:17:37.000 That was an example of having multiple strategies for doing the same thing.
00:17:44.000 If you only have one way to do it and it's particularly complex, you might still want to get it out of the Active Record layer.
00:17:51.000 Similarly, coordinate between multiple models which can be a great reason to use service objects.
00:17:58.000 Perhaps placing an order involves saving credit card information and managing shipping addresses.
00:18:05.000 If you're dealing with external services, you might need to query your payment gateway before allowing an order to go through.
00:18:12.000 If you have ancillary concerns, like logic that runs once a month on a cron job, you wouldn't want to give that logic top real estate.
00:18:19.000 You can push it over to the side.
00:18:25.000 The third pattern is form objects.
00:18:32.000 Form objects deal with coordinating multiple models through one form.
00:18:39.000 If you've done Rails programming for a while, you know this feeling of dread.
00:18:46.000 You get the mockup from your designer, and there are four models on this form. How can we do that?
00:18:52.000 Rails has powerful tools for putting applications together very easily, but it doesn't have good solutions for this.
00:19:00.000 If you've got a form object, you're probably going to be using that in a create or update flow.
00:19:07.000 That's when you're most often dealing with multiple models in a single operation.
00:19:15.000 I like to make my form objects quack like Active Record objects. We'll see how that works.
00:19:22.000 Here's an example: I've seen this sort of thing before.
00:19:28.000 This user class has been extended to support sign-ups that require a username, email address, and company name—all on the same form.
00:19:34.000 When the form is submitted, we create both a company record and a user record.
00:19:41.000 The person added an accessor to the user for the company name and a before_create callback to create the company.
00:19:48.000 This is done before the user is created and assigns the association.
00:19:55.000 Have you guys ever seen code like this? It's a little bit hairy and has a few problems.
00:20:03.000 If the user save fails after creating the company, it's unclear what state your database will be left in.
00:20:09.000 You have to start thinking about when, like, is there a transaction here? What's going to be sent when and when's it going to be rolled back?
00:20:17.000 The company method is what I call a shape-shifter method.
00:20:24.000 Sometimes it returns a company, and sometimes it returns a string.
00:20:30.000 If you just want to create a user but not a company, it's not clear how you would do that.
00:20:36.000 Let's refactor it. We're going to introduce a form object called a signup.
00:20:44.000 When I'm doing form objects, I like to use the Virtus library; it's lightweight.
00:20:51.000 It extends a plain Ruby object with attribute-like behavior.
00:20:58.000 So we can declare attributes such as name, email, and company name.
00:21:05.000 We can also expose an accessor for the user and the company, which will come in handy.
00:21:12.000 Next, we make it quack like an Active Record.
00:21:19.000 We define a save method, just like Active Record. If validation succeeds, we proceed to persist.
00:21:27.000 The answer is very clearly laid out in the signup class.
00:21:32.000 First, we create the company, then create the user hanging off that association.
00:21:40.000 We populate instance variables so any coordinating code can access the created user or company.
00:21:48.000 Because it's similar to Active Record, the signup controller becomes simple.
00:21:56.000 We just instantiate the signup and save it as if it were a model.
00:22:02.000 What do we gain? We're layering the concept of aggregation into a single form.
00:22:09.000 We've limited the responsibility of our Active Records, which is always a good thing.
00:22:16.000 We also have a place to style contextual validation.
00:22:23.000 Most of the time, validations on a signup form might change over the life cycle of your business.
00:22:29.000 When you have validations that are only relevant to signup, now you have a natural place to put them.
00:22:36.000 When to use form objects? Whenever you're aggregating multiple models into a form.
00:22:44.000 If you ever see 'accepts_nested_attributes_for,' I hate this method. This is probably my least favorite method in all of Rails.
00:22:51.000 It's had all sorts of bugs because it's so complex.
00:22:58.000 But you can replace uses of 'accepts_nested_attributes_for' with form objects, and you'll be a lot happier.
00:23:05.000 The fourth pattern is query objects.
00:23:12.000 Query objects encapsulate a single way to query the database.
00:23:19.000 Let's look at an example. This might not be unusual in and of itself, but we have an account imported from an external system.
00:23:26.000 We have to run queries to figure out which accounts are ready to be imported.
00:23:35.000 Additionally, which accounts have attempted to be imported but the process has failed?
00:23:42.000 This requires joins against an imports table.
00:23:47.000 The issue here is you're gumming up your ActiveRecord class with a bunch of SQL.
00:23:54.000 If you have a bunch of these, you end up pushing down the important behavior of the object.
00:24:00.000 What do its instances do? That's the key for objects.
00:24:06.000 This gets pushed way down the file, and I understand the benefit of hopping down to SQL for certain types of queries.
00:24:13.000 But if I'm scanning through a Ruby file and see big blocks of SQL, it throws me off.
00:24:20.000 It begs the question of what is this doing, and why is that there?
00:24:26.000 What are better ways to manage that complexity? If you introduce a query object,
00:24:34.000 you can define a class that is responsible for executing one query.
00:24:40.000 You can initialize it with a relation to gain some benefits.
00:24:46.000 Then you move that query out from the top of your ActiveRecord model into its own object.
00:24:52.000 You provide a find_each method. If you have a background job that needs to page through accounts to import, you just call that with a block.
00:24:59.000 What does this get us? We focus the ActiveRecord on core behavior.
00:25:06.000 We get composable objects, which encourages refactoring.
00:25:12.000 This theme will recur, but I love first-class objects.
00:25:18.000 I don't like class methods; they are difficult to refactor.
00:25:23.000 When you get things into an object, your entire OOP toolbox becomes readily available.
00:25:29.000 You can do composition, inheritance, and introduce state naturally.
00:25:36.000 When would I use this? If I have lots of scopes with subtle variations at the top of my ActiveRecord.
00:25:44.000 If I have complicated scopes that take eight lines to run a query, that's a good case for factoring it into a separate object.
00:25:50.000 If I have rarely used scopes, something that is a simple scope but is only relevant for an ancillary operation in the domain,
00:25:56.000 then maybe it should be in an ancillary file in the codebase.
00:26:04.000 The fifth pattern is view objects.
00:26:10.000 View objects are what I like to use to refer to objects that back up a template or a partial.
00:26:17.000 They might depend on zero, one, or multiple models.
00:26:25.000 I've seen all three cases.
00:26:32.000 Picture LinkedIn, which likes to give you a percentage completion of your onboarding process.
00:26:40.000 LinkedIn also provides a next step; a nice touch, but you don't want to stick that in your user model.
00:26:48.000 You might end up with code like this. The problem here? We've got repeated method prefixes.
00:26:55.000 The onboarding prefix is evident in two methods. More importantly, you have this non-critical concern in the user class.
00:27:01.000 Things you tell users to do are usually hints, not critical logic.
00:27:08.000 Instead of putting that logic into the user, we might introduce a view object called onboarding steps.
00:27:14.000 You can initialize the onboarding steps object with the user and provide it two methods.
00:27:22.000 These methods would return a message for what we should tell the user to do next, and indicate their progress.
00:27:29.000 This allows us to remove the prefix and creates a clear mapping between templates and classes.
00:27:36.000 We now have a clear place to put any logic that might otherwise mix into templates or helpers.
00:27:43.000 What did we accomplish? We achieve simpler templates.
00:27:50.000 If you've read about the two-step view pattern, this is kind of an implementation of that.
00:27:58.000 If you're not familiar with this pattern, it’s one that is often underused.
00:28:05.000 You can avoid excessive use of helpers.
00:28:12.000 And again, you can have first-class objects that encourage composition and refactoring.
00:28:20.000 You can inherit different view objects from one another, and use one inside of another.
00:28:27.000 All of this is difficult if that logic is stuck in an ActiveRecord model.
00:28:34.000 Use view objects whenever you have display logic in the model.
00:28:42.000 If something is clearly only related to presentation, consider moving it to a view object.
00:28:49.000 If it's delivery mechanism-specific, you should do the same.
00:28:55.000 For instance, if you can conceptualize the application differently, consider how relevant it still is.
00:29:02.000 Whenever you see a partial, if it's doing anything non-trivial and doesn't map one-to-one to an object that can encapsulate that logic,
00:29:09.000 consider reaching for a view object.
00:29:15.000 The sixth pattern is policy objects.
00:29:22.000 Policy objects encapsulate a single business rule, focusing on reads.
00:29:29.000 They're about making determinations based on data in your system, not updating that information.
00:29:36.000 Everybody has dealt with this problem at one point or another—deciding which email notifications to send.
00:29:43.000 In this case, we have about four conditions to consider.
00:29:50.000 We need to ensure that emails to the user have not hard bounced.
00:29:57.000 We also need to check that the user's preferences allow for them to receive notifications of a given type.
00:30:03.000 If the notification is project-specific, we make sure the user has opted into those notifications.
00:30:09.000 It’s a complex conditional statement.
00:30:15.000 From my perspective, this important domain concept is disguised as a method, floating in a sea of other user object methods.
00:30:23.000 Knowing which emails are sent to which users is critically important in web applications.
00:30:30.000 So, we can promote this into a first-class object, making it easier to answer questions.
00:30:37.000 This way, when a product owner asks what emails we're sending, you now have an object that provides the answer.
00:30:43.000 In this case, we have an email notification policy initialized with a user, notification type, and project.
00:30:50.000 It determines whether an email should be delivered based on Boolean logic.
00:30:57.000 This provides a clean place to extract methods.
00:31:04.000 We could have extracted a method in the user class, but it creates a public interface far removed from the private methods that compose it.
00:31:10.000 This leads to a hodgepodge of private methods at the bottom of your ActiveRecord.
00:31:17.000 Encapsulating a business rule into one object is nice.
00:31:24.000 First-class objects make it easier to build aggregates.
00:31:32.000 Sometimes you have business rules that layer on top of other rules.
00:31:41.000 Authorization is a common case, where you have fine-grained authorization rules built on top of others.
00:31:49.000 Policy objects are great for complicated reads, like our four-branched conditional.
00:31:56.000 For ancillary reads or rules, consider using them.
00:32:03.000 However, if the read is simple and core to the domain, you might not want to extract a policy object.
00:32:10.000 The ActiveRecord pattern shines with simple behaviors.
00:32:15.000 The seventh pattern is decorators.
00:32:23.000 Decorators are about wrapping another object and layering additional behavior.
00:32:30.000 You create an object that responds to either the entire wrapped object's interface or just a segment.
00:32:36.000 For example, we have an order that needs to send email receipts.
00:32:43.000 However, we only want to send email receipts if the order was placed online by a customer.
00:32:50.000 If the order came in over the phone or through fax, we don’t want to send an email.
00:32:57.000 Here, the programmer has added an accessor to the order record called 'placed_online.'
00:33:04.000 They're using this to determine whether to fire the email receipt callback.
00:33:11.000 The problem is 'placed_online' is a hack—it doesn't really make sense as a value to determine sending the email.
00:33:18.000 This callback only fires sometimes, not a core behavior of the object's lifecycle.
00:33:25.000 To resolve this, we can introduce an OrderEmailNotifier, which is a decorator.
00:33:32.000 It's going to wrap the order; we only add logic to the save method.
00:33:39.000 If the order saves successfully, we want to send an email.
00:33:47.000 In the orders controller, which processes orders from customers, we can separate logic for building orders.
00:33:54.000 If we do need to send emails, we layer on relevant logic.
00:34:01.000 We build up the order and its decorators, which means that from the perspective of the create method, it's still an order.
00:34:07.000 We have a nice separation between arrangement and work.
00:34:13.000 This allows us to wire together objects, while maintaining the core behaviors.
00:34:22.000 We've promoted the concept of emailing receipts into a first-class domain object.
00:34:29.000 Additionally, it makes sending email opt-in instead of opt-out.
00:34:36.000 You're never going to accidentally send an email receipt when writing a cron job that just needs to make a small change.
00:34:43.000 I like using this pattern if you have contextual behavior that is only relevant for some orders, depending on how they were placed.
00:34:50.000 If you have behavior that interacts with external web services, you wouldn't want to place that into a callback.
00:34:57.000 If you've put an external service call into an Active Record callback, you know the pain it leads to.
00:35:04.000 Decorators allow you to deal with that concern.
00:35:11.000 Some people prefer to use decorators in the view layer, but I'm focusing on models today.
00:35:18.000 There are gems like Draper designed to make layering additional behavior in the view layer easier.
00:35:25.000 That covers the seven patterns. I want to leave you with one more concept before we move to questions.
00:35:34.000 You have to use your judgment whenever you're applying patterns like this.
00:35:41.000 If you wait too long to introduce good design in your code base, you'll end up in a brownfield.
00:35:48.000 You’ll feel stuck in mud, finding it difficult to get things done.
00:35:55.000 However, you also don't want to over-engineer from the start.
00:36:02.000 Rails allows us to accomplish a lot quickly, but there is conceptual overhead when introducing new patterns.
00:36:10.000 Ideally, your design and architecture will grow incrementally as the complexity of your application increases.
00:36:17.000 It's not a huge step function where you go from no design to perfectly done design.
00:36:25.000 You want to gradually increase as they correlate together.
00:36:32.000 Think about that when applying these patterns to your application. Thank you.
00:36:39.000 Thank you.
Explore all talks recorded at LA RubyConf 2013
+6