Ruby on Rails models tend to grow and grow. When refactoring scopes, it turns out you can move them into their own classes.
Rails models can get out of hand. Over time they get more associations, more methods, more everything. The resulting huge API and visual clutter makes those classes hard to maintain.
Consider these scopes:
1 2 3 4 5 6 7 8 9 10 11 12 13
How do we usually address this?
For me, refactoring often means finding related methods & values that deserve their own class, then moving code out of the model and into the new class. For example:
- moving complex validations into validator classes
- moving complex serialization into serializer classes (I do this with serialization to English, too, not just JSON)
- moving complex calculations into value classes.
Whenever I’m trying to move code out of a model, I visit Code Climate’s great post on the topic.
However, scopes are never on the list. What can we do with those?
I poked around Rails source a bit to see if there were any other options available to me.
I found that the
body passed to
ActiveRecord::Base.scope just has to respond to
:call. I guess that’s why lambdas are a shoo-in for that purpose: they respond to
:call and aren’t picky about arguments.
The other thing I found is that the lambdas you usually pass to
scope aren’t magical. I always assumed that they were
instance_eval’d against other objects at whatever other times, but as far as I can tell, they aren’t magical.
self is always the model class (from lexical scope), just like any other lambda.
Moving Scopes from Lambda to Class
You can make a class that complies to the required API. Make calls on the model class (
CheckIn, in my case), which is usually
self in a
1 2 3 4 5 6
Then, hook up the scope in the model definition:
1 2 3
Since it’s just a plain ol’ class, you can give it other methods too:
1 2 3 4 5 6 7 8 9 10 11 12
You can also initialize it with some data:
1 2 3 4 5
Here’s what I think:
- Less visual noise.
- Your model still reads like a table of contents.
- Theoretically, you could test the scope in isolation (but I’m too lazy, if the existing tests still pass, that’s good enough for me :P).
- If the scope takes arguments, you can’t tell right away.
- It doesn’t actually shrink the class’s API: it’s still a big ol’ model.
- It’s not a known Rails practice.