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.

-- 
Posted via http://www.ruby-forum.com/.
_______________________________________________
engine-developers mailing list
[email protected]
http://lists.rails-engines.org/listinfo.cgi/engine-developers-rails-engines.org

Reply via email to