Today I’m gonna talk about Fat Models.
Fat Models are unavoidable when you are learning Rails. That’s because when they teach you MVC— that is model, view, controller— they tell you: Don’t put your business logic into your view. And then they tell you don’t put your business logic into your controllers. So where do you put your business logic?
Unfortunately, you’re sometimes taught or encouraged to put them into the models. Thus, the models get very large. And when I say large I mean hundreds and hundreds of lines of code.
This is not something you should do. Instead, you should understand your models as the bare-bones operations that read or write to your database only. Then you should learn and use the other patterns I’m going to talk about today to build what we call a business domain.
This lesson can be considered an Introduction to Domain Driven Design, which will discuss explicitly later on.
Today we’ll look is meant when we say “prefer composition over inheritance.”
Perhaps you have heard the term: “prefer composition over inheritance.” But what does it really mean understand what that means?
Well, give me a few minutes of your time and I’m going to take you in this lesson into a broad overview of 9 different patterns you can use to break out of the MVC cycle.
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.
In domain-driven design, which really I won’t even get to until pattern #6 below, we separate out Rails models from the business domain. That is, we think the ‘M’ in the traditional Rails sense as only what is necessary to read to and write from the database. This way, we still get to use all of the good parts of ActiveRecord while working towards what is fundamentally a domain layer that doesn’t live in any of the models, views, or controllers.
That, in a nutshell, is the bird’s eye view of what I’m going to talk about today.
We might do this by creating simple methods that will instantiate these service objects when needed.
Service objects have the unique property of coming into memory when needed and then disappearing when your code finishes running on each request.
The name of the game here is to compose the other objects of multiple smaller objects that will keep the business logic in the business domain layer.
Today is a broad overview of some answers to the question “How do I deal with fat models?” We’re gonna look at
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) and Composition with Rails Concerns
(And I’ll cover what a Rails cocern is and how it adds to normal Ruby modules.)
Then I’ll cover patterns found in larger Rails apps:
4. Service Objects
Then we have the aforementioned Introduction to domain-driven design, and other “out-of-the-box” architecture concepts, like:
5. 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:
6. The “pub-sub” pattern: Publish-Subscribe and we’ll have a quick look at a Gem called Whisper
7. The “Interactor” Pattern: Interactor and ActiveInteraction, and U Case gems – to perform complex business operations together
8. 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.
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 Converter::Thing
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.
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.
2. Inheritance
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:
models/animal.rb
class Animal
def blood_temperture
raise "superclass must implement"
end
end
models/mammal.rb
class Mammal < Animal
def blood_temperature
"warm"
end
end
models/bear.rb
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 take a look at composition.