Fat or Skinny Models and Preferring Composition Over Inheritance
Today I’m going to talk about Fat Models.
Fat models are what happens to nearly all Rails applications when new developers work on them. That’s because newer Rails developers don’t know where to put business logic in Rails apps.
Having been told it should not be in the views and it also should not be in the controllers neophyte Rails devs stuff business logic into the models, making them bloated, or fat models.
Fat models are almost certainly something you’re going to see in any intermediate or large Rails application. Fat models are easy to produce if you’re not careful.
Today we’ll look at Fat Models and what object-oriented developers mean when we say “prefer composition over inheritance.”
Many people have heard “prefer composition over inheritance.” It’s one of those things that you hear people in OOP (object-oriented programming) say a lot— but how many people actually understand what that means?
Fat Models Skinny Controllers
In the early days of Rails people said “fat models, skinny controllers.”
In the really early days when the apps themselves were much smaller. People only actually said “fat models skinny controllers” for a short time 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 patterns #5 through #9.
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 also relentlessly question and re-question which is the best for your solution.
In particular, Sandi Metz, an important author whom I will come back to, says you must reach for the abstraction at the last possible moment. You do this not at the first possible moment, or else you may create
Today I’m going to cover from a very high-level these nine design patterns:
1. Classic Delegation
2. Inheritance (bad)
Then I’ll talk about the classic composition patterns, including:
3. Composition with Modules (& Using Helpers in Rails View)
4. Composition with Rails Concerns
And I’ll cover what a Rails cocern even is and what it adds to normal Ruby modules.
Then I’ll cover patterns found in larger Rails apps:
5. Service Objects
Then I introduce domain-driven design, and other “out-of-the-box” architecture concepts, like:
6. Domain Context Interaction as per James Coplien in Lean Architecture
A look the Trailblazer gem – a complete domain driven design system that separates business logic from persistence logic.
At this point, you will understand what I mean when I say we are “separating business logic from persistence logic.”
Then we’ll take a quick look at 3 more patterns:
7 . The “pub-sub” pattern: Publish-Subscribe using Whisper
8. The “Interactor” Pattern: Interactor and ActiveInteraction, and U Case gems – to perform complex business operations together
9. The “mutation” pattern: Mutations using a gem called (unsurprisingly) Mutations
It’s a big lesson so be sure to take your time with it. As well, I will return to some of the higher-level concepts in future courses to get more hands-on with these ideas.
Again, these nine types of abstraction are presented here without strong bias (except #2 which is generally considered bad). You should learn them all and learn how to reach for the right abstraction at the right time. I’ll have another lesson in this course about too early abstractions. For now, I’m presenting this as a very high-level overview so that new and intermediate developers understand the landscape of possibilities.
Well, in truth the sky is the limit! But in the real world, these patterns as outlined here are based decades of industry -wide working and re-working of what are commonly known as “design patterns.”
Although the classic “design patterns” in programming languages that predate Ruby are more numerous than these 9, I’ve chosen these 9 to focus on because they are a great way to teach someone who has only learned about MVC what the alternatives are.
Before I get started let’s review the terms “idempotent” and “destructive” we are all on the same page.
Idempotent vs. Destructive (and Side-Effecty)
A important and 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.
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 Apple # no delegation — all export methods are here def to_csv #... end def to_xml #... end def to_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 Apple # delegate to a converter, passing self as the object def converter Converter::Thing.new(self) end end class Converter::Fruit attr_reader :fruit def initialize(fruit) @fruit = fruit end def to_xml # ... end def to_json # ... end def to_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.
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:
class Animal def blood_temperture raise "superclass must implement" end end
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.