Fridays are antipattern days at The Rails Coach!
Today we’re going to talk about Fat Models. Fat models are what happens to nearly every new Rails developer when they don’t know where logic should go. Having been told it shouldn’t be in the view and shouldn’t be in the controller they stuff it into the models, making them bloated, or fat models. Fat models are almost certainly something you’re going to see and fat models are easy to produce if you’re not careful.
Fat models are an example of a Rails anti-pattern.
What is an antipattern?
Glad you asked. An anti-pattern is a system, process, or pattern typically found in business or software development that isn’t good. In other words, it’s actually counter-productive or works against the actual goal. Another way to think of antipatterns: things that you see when groups of people work together where they repeat the same anti-productive and anti-useful activities again and again.
So, in other words, an “anti- pattern” is something you shouldn’t do.
But antipatterns are unavoidable. They exist because humans are flawed, and structure leads to human agita which, like water, will always look for the shortest way to flow down a mountain.
Antipatterns are literally everywhere in software development. A good example: think of a company culture of meetings. You have endless meetings in which people try to agree on a design for something and go around and around in circles for hours—- or even days or weeks. You think to yourself, “Why are we even doing this?” That’s an antipattern.
That particular antipattern has a specific name: design by committee.
Where do anti patterns come from?
The antipatterns come from the early decades of urban office culture in the American 50s. Some come from the hardware manufacturing cultures of the 70s and 80s (IBM and Hewlett-Packard). Many of them come from the 90s, and new ones have emerged in the age of web development.
You can think of antipatterns typically as business (human) antipatterns and software (code) antipatterns. Although helping companies identify and work with their human antipatterns is an important part of business development, in this series I will focus on coding antipatterns, particularly things that can be found in Ruby on Rails.
Rails comes with powerful tools. These powerful tools can be used to break down the principles of object-oriented programming: to create code that is strongly coupled, not encapsulated, poorly organized, or repeats itself repeats itself. So remember that with great power comes great responsibility.
It is said the road to hell is paved with good intentions. All good antipatterns start with a need of a developer. Typically, they exist because of either that developer’s (1) lazyness or (2) ignorance in seeing the larger picture.
Today we’ll look at Fat Models and what the old-timer object-oriented developers actually mean when they say “prefer composition over inheritance.” I’m sure most developers have heard “prefer composition over inheritance.” It’s one of those things that you hear people in Rails say a lot— but how many people actually understand what that means? Well, it means something, although it can be interpreted in many ways.
Fat Models Skinny Controllers
In the early days of Rails people said “fat models, skinny controllers.”
Like really early when the apps themselves were much smaller. People only actually said “fat models skinny controllers” for a short time (this was around 2007) because then the models got unmanageably large.
Rails was pioneering in the basic concept of splitting Models (persistency logic), Views (view logic) and Controllers (display and response logic).
Notice that the descriptions I used were very intentional and specific. Most people teach the “M” in MVC as “business logic” and the “V” as “view logic or display logic.” Then they try to describe what a controller is. What’s a controller, exactly?
Hmm. Well, for Rails, in the context of a GET request, it:
1) typically prepares a query in anticipation of view being rendered (like setting up a query, may be based on search criteria)
2) Handles authentication, authorization and access control if appropriate. (Arguably, the implemented class Contrtoller doesn’t typically handle this, but the Controller object, as a concept, is where this is responsible)
3) Interacts with the Rails request layer for params, headers, etc.
That’s pretty much it. In other languages, Controllers are sometimes called ViewControllers. You shouldn’t think a Controller as really anything more than a set of code that deals with how to present stuff and respond to things.
That’s why I call it “display logic and response logic”
Breaking out of the MVC Antipattern
If you’re new to Rails or programming, the first thing you learn is MVC (model, view, controller). Then, you should unlearn it.
I like to refer to the M (model) as “business logic.” Think of your Models as only what is necessary to talk to your database: save, read, basic query operations, etc.
If you are working for a company that has a rule X that thing Y should go before thing Z, then you should be thinking about other objects. These other objects are the code that represents your business rules. These other objects are not your Rails models.
The other objects deal with finding, searching, and other operations related to your business domain. Here, instead of bloating up our fat model, we’re going to create additional classes that know how to do the finding, searching, and other stuff.
We will then create simple methods on our other objects that will instantiate these service objects when needed. The database backs only the Rails Active Record objects, not the POROs (“plain old Ruby objects”). The service objects come into memory when needed and then disappear (that is, are garbage collected) when your code finishes running on each request.
We then will compose the other objects of multiple smaller objects that will keep the business logic in the business domain layer. I will cover patterns like this in examples #5 and #6 below.
Today is a broad overview of some answers to the question “How do I deal with fat models.” This is a fast, quick overview and these subjects deserve an entire course of their own. As you learn Rails you should learn and unlearn all of these patterns, and relentless question and re-question which is the best for your solution.
I’m going to cover six strategies for addressing fat models in your app.
- Classic Delegation
- Inheritance (bad)
- Composition with Modules (& Using Helpers in Rails View)
- Composition with Rails Concerns
- Service Objects
- Domain Context Interaction
Before I get started let’s review the terms “idempotent” and “destructive” which describe whether or not an operation makes or does not make changes to the objects it receives.
Idempotent vs. Destructive (and Side-Effecty)
The important and most controversial question in Ruby today is: are your service objects idempotent— that is, making no changes, or are they Destructive, that is— make changes to the object they receive. The traditional approach is to pass Rails model objects to service objects, which are not idempotent and perform operations on the objects themselves, but many detractors of object orientation say herein lies OO’s fundamental problem: side effects.
In short, if you do a lot of operations, you have a hard time keeping track of the order in which the objects are updated, and thus you will have “side-effects.”
The standard Ruby way to eliminate side-effects in Rails is to move towards transactions. That is, everything (and I mean everything the user wants to do “in one go”) happens in a transaction that can be rolled back if there’s a failure. You can and should catch for your rollbacks to gracefully handle these exceptions, which I will cover in a future post.
1. Classic Delegation
Ok so the first topic is called delegation.
Delegation is simply we move logic out of a class and delegate it to another class. It is a common pattern and one of the first ones you learn. Consider for example a
Thing object that can export itself to XML, JSON, or CSV
class Thing # no delegation — all export methods are here def as_csv #... end def as_xml #... end def as_json #... end end
As our model gets “fat,” we’ll want to move those specialized methods out of it. Delegation is our first strategy.
Examine our new
Thing object, and another object called
class Thing # delegate to a converter, passing self as the object def converter Converter::Thing.new(self) end end
class Converter::Thing attr_reader :thing def initialize(thing) @thing = thing end def as_xml # ... end def as_json # ... end def as_csv # ... end end
Here we’re simply moving the methods out of the original object and into another object. It is important to note it is a “simple” move.
In other words, we’re just fundamentally moving code around and splitting it out into new objects. We aren’t actually changing anything fundamental about how we think about functionality and objects — we’re just changing where we think about functionality and objects.
If “a class” in Ruby is “level 0” delegation is “level 1.”
As the complexity of your app grows, the more basic solutions (like this one, “delegation”) will only be building blocks. This is fundamentally abstraction.
Let’s move on to pattern #2.
Classical inheritance is what they teach you when learning computer science as, well, classical inheritance. Its name offically means “inherticance using classes,” but as a tounge-in-cheek joke the double entdre is now that it is “classic” as in outdated. The easiest way to describe classical inheritance is this way:
raise "superclass must implement"
class Mammal < Animal def blood_temperature "warm" end end
class Bear < Mammal end
Ok so what have we achieved? We can ask questions about the animal and, for example, if we want to implement a different species or genus, we would know where to implement things like: Does the animal have hair? Does it have skin or fur? How does it reproduce?
Species of animals lend themselves particularly well to the teaching about classical inheritance. It’s a great use case for teaching, but unfortunately, classical inheritance isn’t often as useful or practical in the real world.
Some may think that categorization and graphing of this complex hierarchy is the stuff of OO developers. In some ways it is and in some ways, it isn’t. In some ways, an obsession with over-categorization is what gets OO a bad rap.
Think about a developer who learns a pattern— like inheritance— and then everything they implement is done with inheritance. It’s like they keep repeating the same solution for every problem. Why? Because our brains operate in the mechanism that our brains were just operating.
That is— once you start doing something one way, you are cognitively biased to repeat the same solution to every new problem that same way. This doesn’t actually make sense and you shouldn’t be that developer.
Because more often than categorization and graphing of a complex hierarchy you as the developer are considering how and why external users— that is, an end user— come into play with the data.
Experienced OO developers say “prefer composition over inheritance” so let’s a take a look at composition.