A plugin system using extend

J

Jean-denis Vauguet

Hi.

I tried to design a simple plugin system using :extend called upon
instance (http://ruby-doc.org/core/classes/Object.html#M000335 usual
documented behavior), so as to inject some methods redefinitions while
keeping the ability to call super in order to fall back on the default
behavior if needed.

It looks like this: http://gist.github.com/339025
It works as expected, as one can see by reading the commented output at
the end of the file: the inheritance chain is altered, with the plugin
"subclassing" the original class of the plugin receiver.

Yet, I tried to introduce some modularity in the system, so I went for:
http://gist.github.com/339028
It fails but I don't understand why. The inheritance chain is not
altered.
I tried to use :include instead of :extend, which gives merely the same
behavior (overwriting instance methods), but this time the plugin module
is added as the parent of the original class within the inheritance
chain, so it is useless for the purpose ;)

I'll be glad if someone could give me a hint on this :)
Thank you!
 
J

Josh Cheek

[Note: parts of this message were removed to make it a legal post.]

Hi.

I tried to design a simple plugin system using :extend called upon
instance (http://ruby-doc.org/core/classes/Object.html#M000335 usual
documented behavior), so as to inject some methods redefinitions while
keeping the ability to call super in order to fall back on the default
behavior if needed.

It looks like this: http://gist.github.com/339025
It works as expected, as one can see by reading the commented output at
the end of the file: the inheritance chain is altered, with the plugin
"subclassing" the original class of the plugin receiver.

Yet, I tried to introduce some modularity in the system, so I went for:
http://gist.github.com/339028
It fails but I don't understand why. The inheritance chain is not
altered.
I tried to use :include instead of :extend, which gives merely the same
behavior (overwriting instance methods), but this time the plugin module
is added as the parent of the original class within the inheritance
chain, so it is useless for the purpose ;)

I'll be glad if someone could give me a hint on this :)
Thank you!
Hi, I got it to do what I think you are looking for by having plugins pass
the object to extend. http://gist.github.com/339101
To test it, I had the redefined object say things in reverse. I also added
another module to show that you can activate different behaviours in
different objects.

Then I played around with it a little bit more, trying to make it more
modular, and behave similar to ActiveRecord's scopes
http://gist.github.com/339114

I wanted to try to make the plugin agnostic to the method it was activating
so that you could, for example, make a plugin that could then be applied to
any method of any object. But I just can never seem to get define_method to
work right -.- every time I try to do that, I seem to struggle a lot with
it, and usually end up using eval with a string, because dynamically adding
a method is so difficult.

Oh well, I'm kind of happy with it.

Guess I should pick up PragProg's Metaprogramming Ruby book... or try out
Lisp >:D
 
J

Jean-denis Vauguet

Thank you Josh. Actually I've already tested what you wrote and that's
just fine as long as what you're redefining belongs to the Base::Server
instance (that's the purpose of :extend).

I posted a "mockup" of what I'd like in a single file:
http://gist.github.com/339129

Basically, it's the "same" thing, but the purpose here is to redefine
instance methods of *other classes* than the Base::Server which has the
"include Plugins"; plus, to do so not for a particular instance of these
classes, but for any of their instances.

Problem is: in this situation, one cannot access a particular instance
of the class to be altered within a given plugin. In my example, this
means the Backward plugin should alter any instance of the Base::Speaker
class once loaded. So you've got to work at the class level somehow
(Base::Speaker). Using :extend at this class level seems useless to me
here (it makes plugins redefinitions available as class methods for
Base::Speaker, not instance's); and using :include does not override the
class' instance methods, for the plugin module is added *before* the
class in it's inheritance chain (say: [Base::Speaker,
Base::plugins::Backward::SpeakerRedef, Object, Kernel] once the Backward
plugin is loaded).

A workaround should be to undef (or alias) the original instance method
when the plugin's module is :included, so that a call to the "original"
method force the object to go finding the method in the plugin's module,
but it feels clumsy to me. Maybe that's the only way to achieve this?
After all, that was the point of alias_method_chain, wasn't it?

Enlight me :)
 
J

Jean-denis Vauguet

Another idea I had is the following:
- any loaded lugin registers as a "callback" for the classes it wants to
alter (instances of the classes, actually!);
- through the Plugins module, which is mix-ined within the Base module
using :extend, have any class nested within the base module to be able
(forced?), everytime they're initialized, to have their instances
:extend the plugins which have registered as callback for the class.

Not really sure about it, but...
Theoretically it would allow for per-instance :extend, thus overriding
behavior of any instance without the need for aliasing. Same behavior as
my standalone and Josh codes, but same flexibility as my previous gist
attempted to achieve.

I'll give it a try when I get the time to, unless I'm told this is BS ;)
 
J

James Edward Gray II

Another idea I had is the following:
- any loaded lugin registers as a "callback" for the classes it wants = to=20
alter (instances of the classes, actually!);
- through the Plugins module, which is mix-ined within the Base module=20=
using :extend, have any class nested within the base module to be able=20=
(forced?), everytime they're initialized, to have their instances=20
:extend the plugins which have registered as callback for the class.
=20
Not really sure about it, but...
Theoretically it would allow for per-instance :extend, thus overriding=20=
behavior of any instance without the need for aliasing. Same behavior = as=20
my standalone and Josh codes, but same flexibility as my previous gist=20=
attempted to achieve.
=20
I'll give it a try when I get the time to, unless I'm told this is BS =
;)

If I understand what you are saying correctly, I've used this exact =
strategy in the past.

I allowed plugins to register themselves on the class object (you can =
also do that automatically as they are defined with an inherited hook), =
and then I just extended all instances with the registered plugins as =
they were created.

It worked great and felt very natural to me when using it.

James Edward Gray II=
 
J

Jean-denis Vauguet

James said:
If I understand what you are saying correctly, I've used this exact
strategy in the past.

I allowed plugins to register themselves on the class object (you can
also do that automatically as they are defined with an inherited hook),
and then I just extended all instances with the registered plugins as
they were created.

It worked great and felt very natural to me when using it.

James Edward Gray II

Thank you for this feedback. You understood well I guess. This is good
pattern IMO too: you both get rid of dirty aliasing, and enable clean
inheritance with super. I did not have the time to try it out yet, but I
guess it should just work :) If it performs zell, I'll try to make it
more generic (a kind of "Pluginable" gem?).

By the way, would you have any code snippet to illustrate the pattern?
That could come in handy when refactoring my code, for I'm sure you're
definitely more skilled at writing this kind of stuff than I currently
am ;)
 
J

James Edward Gray II

=20
Thank you for this feedback. You understood well I guess. This is good=20=
pattern IMO too: you both get rid of dirty aliasing, and enable clean=20=
inheritance with super. I did not have the time to try it out yet, but = I=20
guess it should just work :) If it performs zell, I'll try to make it=20=
more generic (a kind of "Pluginable" gem?).
=20
By the way, would you have any code snippet to illustrate the pattern?=20=
That could come in handy when refactoring my code, for I'm sure you're=20=
definitely more skilled at writing this kind of stuff than I currently=20=

Sure. I added some code like that to Prawn a while back (though I don't =
think they kept it):

=
http://github.com/JEG2/prawn/commit/7e25bafe16f508a080a41979bfe25b47f97f0a=
5e

I hope that helps.

James Edward Gray II
 
R

Robert Klemme

If I understand what you are saying correctly, I've used this exact strategy in the past.

I allowed plugins to register themselves on the class object (you can also do that automatically as they are defined with an inherited hook), and then I just extended all instances with the registered plugins as they were created.

It worked great and felt very natural to me when using it.

James, what's the advantage of this over simply using "include"? If all
instances get to use the plugin module then you can as well include it
in the class.

I see it like this: if all instances of a class should be extended with
plugin behavior, then simply use "include". If only some instances
should, then use "extend".

If plugins also need to be removed at runtime, then a more complicated
solution is necessary (e.g. cooking your own version of "extend" and
using method_missing to delegate to plugged in code - access to instance
variables can then be tricky but you can cope by explicitly passing self
to each method).

Kind regards

robert
 
J

James Edward Gray II

module using :extend, have any class nested within the base module to be =
able (forced?), everytime they're initialized, to have their instances =
:extend the plugins which have registered as callback for the class.overriding behavior of any instance without the need for aliasing. Same =
behavior as my standalone and Josh codes, but same flexibility as my =
previous gist attempted to achieve.also do that automatically as they are defined with an inherited hook), =
and then I just extended all instances with the registered plugins as =
they were created.
=20
James, what's the advantage of this over simply using "include"? If =
all instances get to use the plugin module then you can as well include =
it in the class.

There's one massive advantage: the inheritance order. include puts the =
plugin behind the base class, which is much less useful than extend's =
behavior of putting it in front of the base class (behind the singleton =
class, technically). To me, this is the very reason this system is so =
natural for plugins.

James Edward Gray II=
 
J

Jean-denis Vauguet

Robert said:
James, what's the advantage of this over simply using "include"? If all
instances get to use the plugin module then you can as well include it
in the class.

As JEGII stated, the main reason to do so is related to the inheritance
chain order. If you want to be able to redefine an instance method while
keeping access to the original implementation, extend is necessary.
include will make the redefinition available but as it's mixed-in, the
original definition trumps the redef. With extend, the order is reversed
(the original class is kind of subclassed by the module extend receives
as an argument).

This is terrific behavior for callbacks and plugins. It's a easy as pie
to do with classes, and a little bit more tortuous to achieve for class
instances.
I see it like this: if all instances of a class should be extended with
plugin behavior, then simply use "include". If only some instances
should, then use "extend".

I think there's some confusion about what extend and include really are.
We often read extend is to make module methods available as class
methods while include makes them instance methods, but that's not true.

extend is a method of the Object class, and it can handle any object as
a receiver, be it a class, an instance of a class, or a module, a
singleton, etc. If you extend a class, then you'll get class methods. An
instance, instance methods. And what's so great about extend is, once
again, the way it alters the inheritance chain: the class calling extend
for a module gets subclassed by the module-now-a-class. Which means at
least two things: if you redefine the module passed to extend, then the
changes are not propagated, it's made available only for new extending
objects; if you had a method on the object which is redefined into the
extended module, then the module-now-a-class version is the first match,
and you can call super to reach the original (class) definition.

Quite different is include, a keyword not a method, which has only one
behavior: quoting the Pickaxe, "it makes a reference from the class to
the included module. If multiple classes include that module, they'll
all point to the same thing". So you get shared, instance methods. The
mixed-in module is appended right next to the class including it within
the inheritance chain, so that if you call an instance method of the
class, even if it's been redefined by the mixed-in module, the first
match's still the class'.

So, the real difference between extend and include, aside from their
nature, is not really about whether they're talking to classes or
instances, it's more about their behavior: extend is useful for
redefining things (once), include (mixin) is useful for adding (shared)
things. I mean, following Yehuda Katz
(http://yehudakatz.com/2010/02/25/rubys-implementation-does-not-define-its-semantics/),
you can either stick to the implementation, or, you can consider the
semantic/purpose, the latter being more accurate and useful at the same
time IMO :)
 
J

James Edward Gray II

extend is a method of the Object class, and it can handle any object = as=20
a receiver, be it a class, an instance of a class, or a module, a=20
singleton, etc. If you extend a class, then you'll get class methods. = An=20
instance, instance methods. And what's so great about extend is, once=20=
again, the way it alters the inheritance chain: the class calling = extend=20
for a module gets subclassed by the module-now-a-class.
Quite different is include, a keyword not a method, which has only one=20=
behavior: quoting the Pickaxe, "it makes a reference from the class to=20=
the included module. If multiple classes include that module, they'll=20=
all point to the same thing". So you get shared, instance methods.
So, the real difference between extend and include, aside from their=20=
nature, is not really about whether they're talking to classes or=20
instances, it's more about their behavior: extend is useful for=20
redefining things (once), include (mixin) is useful for adding = (shared)=20
things.

This isn't totally accurate. extend() is a stupid simple shortcut that =
really is just an include. This code:

obj.extend(Whatever)

is identical to:

class << obj
include Whatever
end

So it really does all work the same.

The reason it moves the methods in front of the main class though is =
that the singleton class is in front of the main class. Thus including =
the module behind that class still has them in front of the main class.

I talked about this quite a bit in my presentation at LSRC last year:

Video: =
http://lsrc2009.confreaks.com/module-magic-james-edward-gray-ii-28-aug-200=
9.html
Slides: http://grayproductions.net/ruby/module_magic.tar.gz

The spirit of what you said is all right on though.

James Edward Gray II
 
J

Jean-denis Vauguet

James said:
This isn't totally accurate. extend() is a stupid simple shortcut that
really is just an include. This code:

obj.extend(Whatever)

is identical to:

class << obj
include Whatever
end

So it really does all work the same.

That's even better than expected! I did not think about the singleton
class nor did I look at the real implementations (which demonstrates it
*is* important to master along the semantics ;)).

Thanks for the correction.
 
J

Jean-denis Vauguet

I just finished writing my plugins system. It's working like a charm,
thank you for your feedbacks!

I may extract a standalone gem which would allow any module (or class,
but typically it's to be called on a project module) to enable plugins
and project internals redefinition on the fly, without the need for any
aliasing or explicit callback definition. I need to streamline my
current code, though.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,769
Messages
2,569,579
Members
45,053
Latest member
BrodieSola

Latest Threads

Top