On the surface, the dream of components sounds great and cursory overviews of new projects also appear to be "a perfect fit". But they never are. Reuse is hard. Parameterized reuse is even harder. And in the end, you're left with all the complexity of a swiss army knife that does everything for no one at great cost and pain.
DHH ca. 2005
class Topic < ActiveRecord::Base
has_many :comments
validates_presence_of :title
end
topic = Topic.find_or_create_by_title("Ruby UG")
topic.title_changed?
topic.valid?
topic.comments.build
topic.comments.destroy_all
class Vacation < ActiveRecord::Base
is_time_period :start_date, :end_date
end
class User < ActiveRecord::Base
validates_inclusion_of :active, :in => [true, false]
has_defaults :active => true
named_scope :active, :conditions => { :active => true }
end
class User < ActiveRecord::Base
include ActiveFlag
end
module ActiveFlag
validates_inclusion_of :active, :in => [true, false]
has_defaults :active => true
named_scope :active, :conditions => { :active => true }
end
class Note < ActiveRecord::Base
include HasMany, :comments
end
applib or initializersgem install modularity
class User < ActiveRecord::Base
does 'flag', :active, :default => true
end
nilUser.active
models
shared
flag_trait.rb
searchable_trait.rb
user.rb
friend.rb
topic.rb
class User < ActiveRecord::Base
does 'flag', :active, :default => true
end
module FlagTrait
as_trait do |name, options|
validates_inclusion_of name, :in => [true, false]
has_defaults name => options[:default]
named_scope name, :conditions => { name => true }
end
end
class Post < ActiveRecord::Base
does 'sanitize_html', :abstract, :content
end
class Article < ActiveRecord::Base
does 'choice',
:vat_rate,
:default => :standard,
:choices => [
:standard, 'Standard',
:reduced, 'Reduced',
:none, 'No tax'
]
end
<%= form.select :vat_rate, @article.available_vat_rates %>
class StockBooking < ActiveRecord::Base
does 'booking', :for => :article
end
class Article < ActiveRecord::Base
does 'booking_sum', :as => :stock
end
class UsersController < ApplicationController
does 'boring_controller'
end
class UsersController < ApplicationController
does 'boring_controller',
:include => [:profile],
:order => :full_name,
:show_is_edit => true
end
User and inner nodes in your ER modelcomposed_of
class User < ActiveRecord::Base
defaults concerning A
defaults concerning B
callbacks concerning A
callbacks concerning B
validations concerning A
validations concerning B
methods concerning A
methods concerning B
end
class User < ActiveRecord::Base
all code concerning A
all code concerning B
end
class User < ActiveRecord::Base
does 'user/authentication'
does 'user/billing'
does 'user/profile'
does 'user/friends'
end
module User::AuthenticationTrait
as_trait do
include Clearance::User
validates_presence_of :screen_name
after_create :send_welcome_mail
has_defaults :password => lambda { default_password }
def default_password
lambda { ActiveSupport::SecureRandom.hex(14) }
end
end
end
Can't do that in a module because macros are called.
models
user
authentication_trait.rb
billing_trait.rb
profile_trait.rb
friends_trait.rb
shared
choice_trait.rb
deletable_trait.rb
searchable_trait.rb
user.rb
friend.rb
topic.rb
class ApplicationController < ActionController::Base
does 'application_controller/security'
does 'application_controller/context'
does 'application_controller/i18n'
end
module ApplicationController::SecurityTrait
as_trait do
include Clearance::Authentication
protect_from_forgery
filter_parameter_logging :password
before_filter :authenticate
require_permissions
end
end
controllers
application_controller
context_trait.rb
i18n_trait.rb
security_trait.rb
shared
boring_controller_trait.rb
tiny_mce_trait.rb
application_controller.rb
users_controller.rb
topics_controller.rb
#respond_to?, #method, #public_methodsgem install modularityhas_many?
class Post < ActiveRecord::Base
does 'sanitize_html', :content
end
describe Post do
describe 'before_validation' do
it_should_run_callbacks(:sanitize_content)
end
describe 'sanitize_content' do
it 'should close unclosed tags' do
subject.content = 'some <b>text'
subject.sanitize_content
subject.content.should == 'some <b>text</b>'
end
end
end
module SanitizeHtmlTrait
as_trait do |field|
sanitize_field = "sanitize_#{field}"
set_field = "#{field}="
before_validation sanitize_field
define_method sanitize_field do
if send(field)
sanitized = Sanitize.clean(send(field), ...)
send(set_field.to_sym, sanitized)
end
end
end
end
describe ImbueTrait do
subject { Object.new }
it 'should stub the given methods' do
subject.singleton_class.does 'imbue', :foo => 'bar'
subject.foo.should == 'bar'
end
end
# config/initializers/active_flag.rb
ActiveRecord::Base.class_eval do
def self.active_flag
validates_inclusion_of :active, :in => [true, false]
has_defaults :active => true
named_scope :active, :conditions => { :active => true }
end
end
appAll attempts at creating high-level business components that can be re-used and re-configured have failed previously. This failure has not been for technical reasons - it happens because the requirements that yielded the original component interface were sufficiently different from the new requirements so as to require re-writing massive chunks of functionality.
Dan Creswell