Skip to content

Commit 5c57b8e

Browse files
committed
Rails classic autoloading post
1 parent 4d8f195 commit 5c57b8e

File tree

1 file changed

+100
-0
lines changed

1 file changed

+100
-0
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
---
2+
layout: post
3+
author: Tony Schneider
4+
title : Farewell Rails Autoloading
5+
date : 2020-01-23
6+
tags : software
7+
published: false
8+
---
9+
10+
## First some Ruby
11+
12+
Rails autoloading works by hooking into the ruby hook const_missing.
13+
You run some code, Ruby’s like what’s that, rails is like, “I got you, I’ve implemented const_missing” and walks up the module hierarchy and requires the file that constant should be defined in by rails convention.
14+
15+
However, the `class << self` syntax isn’t simply an alternate syntax for defining class methods.
16+
While ultimately it results in a class method, you’re doing so by opening the class’s "eigenclass".
17+
Eigen just means self in german or whatever.
18+
I think it's easier to reason about when referred to as a "singleton class".
19+
20+
In the ruby object model you have classes and instances of those classes.
21+
A user defined class is just an instance of the class Class (whew).
22+
So, if you're curious you might wonder how ruby keeps track of class methods vs instance methods.
23+
24+
The answer is that each class has an anonymous singleton class.
25+
You can actually see it by doing `YourClass.singleton_class`.
26+
The class methods you define (regardless of how) end up as instance methods of the singleton class, as opposed to instance methods on the class you defined.
27+
28+
```
29+
# should be the same list
30+
YourClass.singleton_class.instance_methods(false)`
31+
YourClass.singleton_methods
32+
```
33+
34+
As the name implies, you cannot (thank goodness) create instances of the singleton class `YourClass.singleton_class.new #=> NOPE`.
35+
36+
## Back to Rails
37+
38+
So... back to autoloading :upside_down_face:
39+
40+
```ruby
41+
class A
42+
class << self
43+
def foo
44+
Module.nesting
45+
end
46+
end
47+
def self.bar
48+
Module.nesting
49+
end
50+
def baz
51+
Module.nesting
52+
end
53+
end
54+
55+
A.foo # => [#<Class:A>, A]
56+
A.bar # => [A]
57+
A.new.baz # => [A]
58+
```
59+
60+
In Rails 5, here is roughly how the "classic" autoloading algorithm works: https://guides.rubyonrails.org/autoloading_and_reloading_constants_classic_mode.html#generic-procedure
61+
62+
So let's go to an example you might see in the wild:
63+
64+
```
65+
module SomeEngine
66+
module SomeNamespace
67+
class PolicyService
68+
class << self
69+
def create_policy
70+
RatingService.create_rate
71+
end
72+
end
73+
74+
def self.create_policy2
75+
RatingService.create_rate
76+
end
77+
end
78+
end
79+
end
80+
```
81+
82+
As defined, the nesting inside `PolicyService.create_policy` is `[#<Class:SomeEngine::SomeNamespace::PolicyService>, SomeEngine::SomeNamespace::PolicyService]`.
83+
84+
Same as the earlier example, the nesting for `PolicyService.create_policy2` is `[SomeEngine::SomeNamespace::PolicyService]`.
85+
You might see where I'm headed ;)
86+
87+
When trying to resolve a constant, rails uses the `Module.nesting` from the scope in which the constant is missing.
88+
89+
If the constant is already loaded by something else, great, no issue.
90+
However, relying on this can result in flakey tests since depending on the order of how the tests were run, it may or may not have been already loaded. Sound familiar? :smiling_imp:
91+
92+
In the flakey scenario, the constant has not already been loaded.
93+
In the first method, the nesting is anonymous (the singleton class has no name).
94+
Looking at the algorithm above, when const_missing is fired from an anonymous nesting, Rails will attempt to load the constant from the top level name space.
95+
And you'll get a `NameError` because (let's pretend) `::RatingService` isn't a thing)
96+
97+
In the second example, the module nesting is `SomeEngine::SomeNamespace::RatingService`.
98+
As a result, `PolicyService.create_policy2` works as expected, first checking for `SomeEngine::SomeNamespace::RatingService` then `SomeEngine::RatingService` and finally `::RatingService`.
99+
100+
Rails 6 reworks how autoloading is done using a library called Zeitwerk and I'm really excited for it! (https://github.com/fxn/zeitwerk)

0 commit comments

Comments
 (0)