A few thoughts on engines first.
When you install Rails, it does not do anything. This is what I like. To
make it do anything, you need to write code.
It is extermely simple to write simple code in RoR. I like this too. You
can start with scaffold generator, then adjust some methods, add some
relations etc. Rails supports you on your way. If you need a list, you
have acts_as_list. If you need date/time fields on your forms, there’s a
date helper.
However doing complex things is, of course, not that easy. There’s no
acts_as_customer_info_form_with_tax_reports_and_employment_histories.
This should NOT be done with one line of code. To summarize, Rails
provides building blocks, and it is you who assembles your app.
Now, engines are completely opposite to this. You add a LoginEngine and
your application gets very complex login logic at once, without you
having a slightest idea on how it works or how it can be customized.
What’s more, there are no building blocks for this â?? if you don’t like
anything lying deeply inside LoginEngine, your only option is to rewrite
all or most of it. This is too bad.
LoginEngine makes 2 mistakes. First, it makes very complex things
(salted login support with email confirmation, password recoveries,
security tokens etc) too simple. Second, it does not make “non-default”
simple things enough simple.
Of course, engines are perfectly suitable when you really want to reuse
some business logic, e.g., in a series of similar applications.
Probably, Wiki or forum engine is enough self-contained to be an engine.
(Or, maybe, not. The key is reusability. It’s very hard to make reusable
business logic. It’s much more useful to provide reusable building
blocks. If one wants a Wiki, he should spend a day rolling out his own
Wiki using Wiki building blocks, rather than getting an uncustomizable
monster at once.)
Testing is not addressed either. There’s no point in testing existing
functionality of an existing engine: it’s authors should have tested it
well (after all, they are the authors of the testsuite, so the engine
already passes it probably). What should really be tested are your
customizations and their relations to standard parts of the engine.
However nothing helps you to do this.
Now I introduce a concept of “modular methods”. These are class methods
for your models, controllers and testcases that are delivered via
plugins, support reflection (if you know what this means) and serve as
building blocks rather than complete solutions.
An example is:
class Person < ActiveRecord::Base
stores_encoded_representation_of :password, :salt => “Foo123”
end
Observe that stores_encoded_representation_of modular method is called.
There is nothing unusual here. In fact, I do not offer any new ideas,
the key point is just to think in terms of building blocks.
Modular methods can interact with each other:
class Person < ActiveRecord::Base
generates_per_record_salt
stores_encoded_representation_of :password, :salt => “Foo123”
end
Here, stores_encoded_representation_of method detects that
has_per_record_salt has been called and makes use of per-record salts to
make passwords non-interchangable between records (just like LoginEngine
does).
Yet another example:
class Person < ActiveRecord::Base
has_per_record_salt :salt
stores_encoded_representation_of :password, :salt => “Foo123”
validates_length_of :password, :within => 6…20, :if =>
:password_changed?
validates_confirmation_of :password, :if => :password_changed?
end
Here standard methods validates_length_of and validates_confirmation_of
perform validation only if a new password is being set.
“password_changed?” instance method is generated by “tracks_changes_of”
modular method, which is being invoked by
“stores_encoded_representation_of”. Tracks_changes_of is clever enough
to ignore multiple invocations with the same field. (Yes, all those
communications are possible because of reflections. Rails already
supports reflections on aggregations and assosications, and modular
methods add support for reflections on methods.)
There’s a modular_methods plugin which defines some support classes.
Probably the most complex support is provided for testcase modular
methods. Given a model like this:
class PersonForEncodedRepresentation < ActiveRecord::Base
validates_length_of :password, :if => :password_changed?, :within =>
3…10
validates_length_of :salted_password_1, :if =>
:salted_password_1_changed?, :within => 3…10
validates_length_of :salted_password_2, :if =>
:salted_password_2_changed?, :within => 3…10
generates_per_record_salt :salt => ‘WOO-HOO!’
stores_encoded_representation_of :password, :use_per_record_salt =>
false
stores_encoded_representation_of :salted_password_1, :salt => ‘xxx’,
:use_per_record_salt => true
stores_encoded_representation_of :salted_password_2, :salt => ‘yyy’
end
it allows to write testing code like this:
class PersonForEncodedRepresentationTest < Test::Unit::TestCase
fixtures :people_for_encoded_representation
OPTIONS = {
:encoded_length => 40,
:valid_values => [‘secret’, ‘topsecret’],
:invalid_values => [‘x’, ‘verylong’ * 10],
:base_fixture => :bob,
:samples => [:david, :andrey]
}
tests_encoded_representation_of :password, OPTIONS
tests_encoded_representation_of :salted_password_1, OPTIONS
tests_encoded_representation_of :salted_password_2, OPTIONS
tests_encoding_incompatibility_of :password, :salted_password_1,
:salted_password_2,
:base_fixture => :bob, :value => ‘secret’
ATTRS = [:name, :password, :salted_password_1, :salted_password_2]
tests_encoding_compatibility_across_records_of :password,
:sample => :david, :sample_attrs => ATTRS
tests_encoding_incompatibility_across_records_of :salted_password_1,
:sample => :david, :sample_attrs => ATTRS
end
Note all this fixtures and “samples” stuff. It’s needed. Suppose you’ve
got a LoginEngine, and you add a new column to your users table and a
corresponding validation to your model. How can LoginEngine save any
rows now? To pass validation, it must fill in your new field, but it
does not have a slightest idea on how to do that.
With modular testing methods, it’s you who writes the fixtures, so you
can provide all necessary fields there. You then give the names of your
fixtures, and they get loaded automatically. Also you sometimes provide
a set of valid and invalid values, for the methods to use them when they
need a valid or invalid record. (For example,
tests_encoded_representation_of uses invalid values to check that, after
an unsuccessful validation, the value of the column does not get encoded
even if it has been changed.)
(Internally, modular methods are implemented using corresponding
classes, like StoresEncodedRepresentationOf. There is some syntatic
sugar for modular method writers, for example, options are automatically
parsed and assigned to instance attributes. There are some helper
methods, and some activities are automated.)
I’m currently working to implement this idea. I don’t have much time,
though. Anyway, feedback and discussions are welcome! (Anyone willing to
help me is especially welcome.)
You can look at the code via SVN:
svn://82.146.42.23/webdevel/rails/plugins/modular_methods/trunk
(the plugin itself)
svn://82.146.42.23/webdevel/rails/plugins/modular_methods_test/trunk
(a test application for the plugin, has plugin in svn:externals)
(Sorry for IP’s, have not bought a domain yet. Also available as
andreyvit.firstvds.ru instead of IP if somebody cares. Sorry, cannot run
Apache 2 now so cannot provide http access to the repository.)
The code is far from being ready for real-world usage, but the idea
should be clear.
Andrey.