Skip to content

Instantly share code, notes, and snippets.

@svenfuchs
Created July 7, 2010 11:19
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save svenfuchs/b7733d5fea89d2dd37ca to your computer and use it in GitHub Desktop.
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
@svenfuchs
Copy link
Author

Doh. Just accidentally closed the browser instead of posting a long comment.

Jose: yes, I believe it absolutely is. Actually, it's the only reason for me to ask these questions :) Our scenario is:

  1. Devise contributes class User (I know it doesn't, just for the sake of this example)
  2. My Engines (e.g. blog-engine, tickets-engine) use User and need to add a few things to it.
  3. Custom Rails 3 applications (e.g. which we are building for clients) both need to use User and Blog and both need to add a few things to it.

By installing Devise I basically opt into using whatever Devise provides by default. But because Devise is pretty modular there's no huge issue here. Even if Devise defines something that I do not want I could still overwrite it from the custom app.

@drogus
Copy link

drogus commented Jul 7, 2010

I also like the Engine.require, I will think more on that topic later today :)

UPDATE: I haven't refreshed the browser before writing this.

@svenfuchs: by saying "Devise contributes class User" do you mean that in your example Devise would be the one to create it?

With that in mind Engine.require is not so cool anymore ;-)

@pixeltrix
Copy link

Wouldn't the simplest solution be to use include to mix in the appropriate functionality like so:

# vendor/plugins/blog/app/models/blog/user.rb
require 'active_support/concern'
module BlogEngine
  module User
    extend ActiveSupport::Concern
    included do
      # stuff
    end

    def method_a
      # stuff
    end
  end
end

# vendor/plugins/devise/app/models/devise/user.rb
require 'active_support/concern'
module AuthEngine
  module User
    extend ActiveSupport::Concern
    included do
      # stuff
    end

    def method_b
      # stuff
    end
  end
end

# app/models/user.rb
class User < ActiveRecord::Base
  include BlogEngine::User
  include AuthEngine::User

  # Don't want this method
  remove_method :method_b
end

@josevalim
Copy link

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. :(

@jeremy
Copy link

jeremy commented Jul 7, 2010

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

@jeremy
Copy link

jeremy commented Jul 7, 2010

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.

@svenfuchs
Copy link
Author

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.

@rkh
Copy link

rkh commented Sep 21, 2010

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.

@svenfuchs
Copy link
Author

Thanks Konstantin, that sounds interesting. Will look into that!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment