-
-
Save svenfuchs/b7733d5fea89d2dd37ca to your computer and use it in GitHub Desktop.
# Vertical application slices | |
The Rails < 2.3 engines plugin defined the term "engine" as a "full vertical | |
application slice". When Rails 2.3 included engines it went a few steps back | |
and only implemented most of the core features, making an "engine" rather a | |
"pimped plugin". Today in Rails 3 we have engines that are way more powerful | |
and flexible than what we had before but (apart from what Piotr's plans are | |
[1]) there's still been one feature in the original engines plugin that I miss | |
today: the ability to mix first-class-citizen code slices from various engines | |
automatically without having one engine know much about other engines. | |
Let's say we have a bunch of engines that contribute small applications (say, | |
a blog and a ticket tracker) and only share a few things, maybe a User model. | |
So, we'd have the engines: user, blog, tickets. Obviously users have many blog | |
posts and many tickets. Now, when the blog engine is installed the User.should | |
have_many(:posts). When the tickets engine is installed the User.should | |
have_many(:tickets). But obviously this needs to be defined in the blog and | |
tickets engines, not in the user engine where the User class is defined. | |
So, the blog and tickets engines somehow need to reopen the User class and add | |
that association: | |
# somewhere in the blog engine | |
User.has_many :posts | |
# somewhere in the tickets engine | |
User.has_many :tickets | |
Traditionally there were two approaches (to my knowledge) to place these code | |
bits in the blog and tickets engines: | |
1. Initializers. It was easy to hack Rails plugins to have their own | |
initializers, so they'd all get loaded at startup. Placing all of these things | |
into tons of initializers smelled a lot and also required to use to_prepare | |
hooks all over the place. | |
2. App folder. One could do something like: | |
# [blog-engine]/app/models/user.rb | |
load 'relative/path/to/user-engine/app/models/user.rb' | |
User.has_many :posts | |
But this obviously requires the blog-engine to preceed the user-engine in the | |
load path. With engines being Rails plugins one could mess with the plugin | |
load order to enforce this but obviously this isn't a nice and reliable | |
option, too. | |
I believe this should be a first-class framework capability. There should be a | |
way to tell the framework: "Here's a file that relates to the User class but | |
it's only a slice of it. You have to look elsewhere to find the file defining | |
the User class. Once you did please load this slice, too." | |
Maybe this could be accomplished introducing an after_constant_load hook in | |
ActiveSupport::Dependencies. Once Dependencies has found the User class in | |
[user-engine]/app/models/user.rb it would call the after_constant_load hook | |
where we could then look for files named something like models/user_slice.rb | |
and load them. Obviously this approach would require a naming convention or | |
special subfolder. | |
Another approach could be to have Dependencies be able to work recursively (I | |
have no idea how much of the current implementation could support that): | |
When const_missing for the User class is triggered Dependencies would first | |
find the file in the blog-engine: | |
# [blog-engine]/app/models/user.rb | |
User.has_many(:posts) | |
... which again uses the not yet defined User constant which again would | |
trigger Dependencies const_missing. Now Dependencies would skip looking at the | |
file [blog-engine]/app/models/user.rb and instead look for the next one in the | |
load path and so forth until it finds: | |
# [user-engine]/app/models/user.rb | |
class User | |
end | |
Dependencies would also need to continue looking for user.rb files once it has | |
found one ... so that engines that come later in the load_path will be able to | |
contribute their stuff, too: | |
# [xyz-engine]/app/models/user.rb | |
User.has_many(:xyzs) | |
So, basically this approach would change Dependencies: | |
- from looking for a single file defining a missing constant | |
- to loading all files according to the naming convention | |
(constant_name.underscore) and configuration (app folders etc). | |
--- | |
[1] http://piotrsarnacki.com/2010/07/06/rsoc-status-briging-engine-closer-to-application |
Andrew, this goes in the opposite direction to mountable apps. Where you can use an application mounted in another or stand alone.
This would also give problems with Devise. Sometimes people want to change the behavior of Devise::SessionsController and they cannot simply reopen app/controller/devise/sessions_controller.rb. They need to create a new controller and inherit from it. However, if we adopt your solution, we wouldn't have a controller in the first place. :(
Explicitly mixing in plugin behavior makes more sense to me than a highly-engineered autoextending mechanism. Big +1 to Andrew's last example.
A mountable app trying to work standalone or as a hybrid, using models from another app, would be well-off configuring that explicitly. For example,
module Blahg
User = ::User
end
# or
Blahg.user_model = User
To subclass a controller or model provided by a plugin, create a base class and stub subclass in the plugin. An app subclass of the plugin's base class will appear first in $LOAD_PATH, displacing the stub.
For the record, we've now solve this issue like this: http://gist.github.com/518869
I.e. we look for files named foo_slice.rb in controller and model directories in both the application and our own engines. Then we require the class the "slice" is referring to and all the slices. Each slice can then class_eval the already loaded actual class.
Even though this works fine for now I'd rather liked to have seen a "lazy" approach where classes and their "slices" get loaded when necessary. Obviously our current approach preloads all these classes on all requests all the time - which slows down dev mode.
Since you probably almost always have to load the User model, this is no additional overhead compared to what AS::Dependencies would do. You could also use my RSoC fork, that has real dependency tracking. That way you only would have to load your patch once, do a require_dependency
in there and everytime User is loaded, it will load your patch immediately afterwards.
Thanks Konstantin, that sounds interesting. Will look into that!
Wouldn't the simplest solution be to use include to mix in the appropriate functionality like so: