Rethinking code reuse with traits

Henning Koch, makandra GmbH

makandra

Modularity?

High level components

High level components don't work

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

What the spec says

red

What the client wants

What is actually needed

The existing component

Clearance

Awesome, but...

Low level components do work

ActiveRecord metaprogramming

        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
      

Use cases for macros

Writing macros is painful

        class Vacation < ActiveRecord::Base
          is_time_period :start_date, :end_date
        end
      

Modules cannot call macros

        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
      

Modules lack parameters

        class Note < ActiveRecord::Base
          include HasMany, :comments
        end
      

No love from the class loader

Modularity

Using a trait

        class User < ActiveRecord::Base
          does 'flag', :active, :default => true
        end
      

Traits live in app

        models
            shared
                flag_trait.rb
                searchable_trait.rb
            user.rb
            friend.rb
            topic.rb
      

Trait implementation

        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
      

Sanitize HTML before validation

        class Post < ActiveRecord::Base
          does 'sanitize_html', :abstract, :content
        end
      

Value choice with humanized label

        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 %>
      

Bookings

        class StockBooking < ActiveRecord::Base
          does 'booking', :for => :article
        end

        class Article < ActiveRecord::Base
          does 'booking_sum', :as => :stock
        end
      

Boring controller

        class UsersController < ApplicationController
          does 'boring_controller'
        end
      
        class UsersController < ApplicationController
          does 'boring_controller',
            :include => [:profile],
            :order => :full_name,
            :show_is_edit => true
        end
      

Partial classes

Interwoven concerns

        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
      

Ordered concerns

        class User < ActiveRecord::Base
          all code concerning A
          all code concerning B
        end
      

Traits can be partial classes

        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 become organized

        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
      

Controllers, too!

        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 become organized

        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
      

Traits as partial classes

Traits vs. method_missing

Getting started with traits

Image credits

Bobbycar by JuergenL
http://commons.wikimedia.org/wiki/File:Bobbycar.jpg
Opel Bahnrennmaschine by Gryffindor
http://commons.wikimedia.org/wiki/File:Opel_Bahnrennmaschine.JPG
Ferrari Enzo Ferrari by Thomas Doerfer
http://commons.wikimedia.org/wiki/File:Ferrari_Enzo_Ferrari.JPG

http://makandra.com/notes

We're hiring!

Spare slides

Testing traits

        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
      

Testing traits in isolation

        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
      

Initializers don't reload

        # 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
      
All 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