Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Responsible Metaprogramming in Rails

Responsible Metaprogramming in Rails

Presented at the first Burlington Ruby meet up on July 17, 2012 by Peter Brown

Peter Brown

July 17, 2012
Tweet

More Decks by Peter Brown

Other Decks in Programming

Transcript

  1. What is Metaprogramming? Magic Code that writes code Code that

    manipulates language constructs at runtime
  2. What is Metaprogramming? Magic Code that writes code Code that

    manipulates language constructs at runtime Just Programming
  3. Ruby Prerequisites class Cat def breed=(breed) @breed = breed end

    def breed @breed end end Classes, objects, methods, variables
  4. Ruby Prerequisites class Cat def breed=(breed) @breed = breed end

    def breed @breed end end Classes, objects, methods, variables cat = Cat.new cat.breed = 'Ceiling Cat' cat.breed # => 'Ceiling Cat'
  5. class CeilingCat < Cat def initialize @breed = 'Ceiling Cat'

    end end Ruby Prerequisites Inheritance cat = CeilingCat.new cat.breed # => 'Ceiling Cat' cat.breed = 'Peeping Tom' cat.breed # => 'Peeping Tom' CeilingCat “is a” Cat
  6. Ruby Prerequisites Class Methods class Cat def self.number_of_legs 4 end

    end class CeilingCat < Cat end Cat.number_of_legs # => 4 CeilingCat.number_of_legs # => 4
  7. Ruby Prerequisites self class Cat def what_am_i? "I am a

    #{breed}" end end class Cat def what_am_i? "I am a #{self.breed}" end end Equivalent
  8. 101: Ruby Object Model Methods and CONSTANTS belong to classes

    Instance variables belong to instances (objects)
  9. 101: Ruby Object Model Method lookup (ignoring mixins) ceiling cat

    object CeilingCat Cat CeilingCat.new Object
  10. 101: Ruby Object Model bacon = "bacon" def bacon.edible? true

    end bacon.edible? # => true avocado = "avocado" def avocado.edible? false end avocado.edible? # => false Singleton class (ghost class/eigenclass/metaclass)
  11. 101: Ruby Object Model Method lookup (ignoring mixins) ceiling cat

    object CeilingCat Cat CeilingCat.new Object
  12. 101: Ruby Object Model Method lookup (ignoring mixins) ceiling cat

    object CeilingCat Cat cat singleton class CeilingCat.new Object
  13. 101: Introspection/Reflection class Dog def initialize @myself = self end

    def what_am_i? @myself end end dog = Dog.new dog.who_am_i? # => #<Dog:0x007fc843428fe8> dog.methods # => [:who_am_id?, :methods, :class, ...] dog.class # => Dog dog.instance_variables # => [:@myself]
  14. 101: Dynamic Methods class Dog attr_accessor :breed attr_reader :age attr_writer

    :birthday end class Dog def breed @breed end def breed=(breed) @breed = breed end def age @age end def birthday=(birthday) @birthday = birthday end end attr_* Macros
  15. class BeerDrinker define_method :drink_beer, lambda { |beer| puts "Mmmm that

    was a great #{beer}!" } def buy_beer(beer) puts "Just bought some #{beer}!" end end pete = BeerDrinker.new pete.drink_beer("Heady Topper") pete.buy_beer("Natty Light") 101: Dynamic Methods define_method equivalent
  16. 101: Dynamic Methods class Numeric def method_missing(method, *args, &block) if

    method.to_s.start_with? 'divide_by_' divisor = method[/\d+$/] self / divisor.to_f else super end end end 9.0.divide_by_3 # => 3.0 10.0.divide_by_2 # => 5.0 method_missing
  17. 101: Dynamic Dispatch class Television def next_channel @current_channel += 1

    end def prev_channel @current_channel -= 1 end private def channels 1..99 end end tv = Television.new tv.next_channel # => 2 tv.send(:next_channel) # => 3 tv.prev_channel # => 2 tv.send(:prev_channel) # => 1 tv.channels # NoMethodError tv.send(:channels) # => 1..99 (range) Dynamically invoking methods - send(:method)
  18. 101: Callbacks/Hooks Capture events at runtime class Animal def self.inherited(klass)

    puts "#{klass} just inherited the Animal class" end def self.method_added(method) puts "The method '#{method}' was just added" end end class Dog < Animal def bark? true end end # => "Dog just inherited the Animal class"" # => "The method 'bark?' was just added"
  19. 101: Context Probe class Dog define_method :bark!, -> { puts

    "Ruff!" } end cat = Class.new cat.class_eval do define_method :meow!, -> { puts "Meow!" } end Dog.new.bark! Cat.new.meow! class_eval and instance_eval
  20. 101: Open Classes class String def palindrome? self == reverse

    end end "kayak".palindrome? # => true "beer".palindrome? # => false
  21. Example: DSL has_attached_file :photo, styles: { overview: "250x250>", popup: "800x600>",

    portfolio: "48x48#" }, default_url: "/images/project_photo_default.png", default_style: :overview, storage: :s3, s3_credentials: "#{Rails.root}/config/s3.yml", s3_protocol: 'https' validates_attachment_size :photo, less_than: 3.megabytes validates_attachment_content_type :photo, content_type: ["image/jpeg", "image/png", "image/x-png", "image/gif"] Domain Specific Language
  22. Example: Configuration RSpec RSpec.configure do |c| c.drb = true c.drb_port

    = 1234 c.default_path = 'behavior' c.before(:suite) { establish_connection } c.before(:each) { log_in_as :authorized } end
  23. Example: DRY [:ac_capacity, :dc_capacity, :dc_nameplate_capacity].each do |capacity| define_method capacity do

    return super() unless super().nil? total_capacity = calculate_total_capacity(capacity) update_attributes(capacity => total_capacity) end end Don’t Repeat Yourself
  24. Responsible Metaprogramming Readable, usable, maintainable Well tested and documented Right

    tool for the job Simplest approach Avoid YAGNI (You ain’t gonna need it)
  25. Protips! Avoid monkey patching and method aliasing Don’t overuse metaprogramming

    Do overuse metaprogramming Adhere to coding standards
  26. What I had: class Movie < ActiveRecord::Base scope :recent, where('released_at

    > ?', 6.months.ago) scope :profitable, where('gross > budget') def self.filter_by_title(title) where('title LIKE ?', "%#{title}%") end def release_year released_at.year end def profitable? gross > budget end end A class that manages collections and instances
  27. What I wanted: A class that only manages collections class

    Movies scope :recent, where('released_at > ?', 6.months.ago) scope :profitable, where('gross > budget') def self.filter_by_title(title) where('title LIKE ?', "%#{title}%") end end
  28. Actual use: Predictable, simple, resistant to Rails changes class Movies

    < Herd::Base model Movie # optional scope :recent, where('released_at > ?', 6.months.ago) scope :profitable, where('gross > budget') def self.filter_by_title(title) where('title LIKE ?', "%#{title}%") end end
  29. Challenges Collection methods cannot simply be delegated to their corresponding

    models and must be “registered” so that ActiveRecord::Relation classes are aware of them. Collections need to “inherit” collection methods from their members
  30. Components of Herd Relation objects have access to collection methods

    Collection has access to model and relation methods Movies.where(released: 2012) # AR::Relation Movies.count # Integer Movies.where(released: 2012).count Movie.where(released: 2012) # AR::Relation Movie.count # Integer Movie.where(released: 2012).count
  31. Code Walkthrough module Herd class Base def self.model(model) @model_class =

    model end end end class Movies < Herd::Base model Movie end Associate Model with Collection
  32. def self.inherited(klass) klass.class_eval do def self.singleton_method_added(method_name) return if @model_class.nil? method

    = self.method(method_name).to_proc @model_class.define_singleton_method(method_name, method) end # ...omitted... end end Code Walkthrough Registering Collection Methods on Model Callback Context Probe Dynamic Method Callback
  33. def self.method_missing(method, *args, &block) if @model_class.respond_to?(method) @model_class.send(method, *args, &block) else

    super end end Code Walkthrough Inheriting Model Methods Dynamic Method(s) Dynamic Dispatch
  34. module Herd module ActiveRecord def herded_by(herd_collection) unless herd_collection.ancestors.include? Herd::Base raise

    ArgumentError, 'collection must inherit from Herd::Base' end herd_collection end end end ActiveRecord::Base.extend Herd::ActiveRecord Code Walkthrough Working with ActiveRecord Introspection Extending ActiveRecord class Movie < ActiveRecord::Base herded_by Movies end
  35. Code Walkthrough Testing and Documentation it 'should set the model

    name implicitly from the collection name' do Directors.create!(:name => 'Richard Donner') Directors.all.should have(1).director end it 'should delegate class methods to model' do Movies.find_by_name('The Goonies').should == movie1 end