Slide 1

Slide 1 text

RESPONSIBLE METAPROGRAMMING* *in Rails Burlington Ruby - July 2012 - Peter Brown

Slide 2

Slide 2 text

Introduction Ruby Metaprogramming Overview Responsible Metaprogramming Introducing Herd w/ code walkthrough

Slide 3

Slide 3 text

What is Metaprogramming?

Slide 4

Slide 4 text

What is Metaprogramming? Magic

Slide 5

Slide 5 text

What is Metaprogramming? Magic Code that writes code

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

What is Metaprogramming? Magic Code that writes code Code that manipulates language constructs at runtime Just Programming

Slide 8

Slide 8 text

What is Metaprogramming? The constant balance between code readability, usability, and maintainability

Slide 9

Slide 9 text

Ruby Prerequisites Classes, objects, methods, variables

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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'

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

Metaprogramming 101 Ruby Object Model Introspection/ Reflection Dynamic Methods Dynamic Dispatch Callbacks/Hooks Open Classes Context Probe

Slide 16

Slide 16 text

101: Ruby Object Model Everything is an object

Slide 17

Slide 17 text

101: Ruby Object Model Methods and CONSTANTS belong to classes Instance variables belong to instances (objects)

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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)

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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.methods # => [:who_am_id?, :methods, :class, ...] dog.class # => Dog dog.instance_variables # => [:@myself]

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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)

Slide 27

Slide 27 text

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"

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

101: Open Classes class String def palindrome? self == reverse end end "kayak".palindrome? # => true "beer".palindrome? # => false

Slide 30

Slide 30 text

Metaprogramming Uses

Slide 31

Slide 31 text

Metaprogramming Uses Domain Specific Languages (DSLs)

Slide 32

Slide 32 text

Metaprogramming Uses Domain Specific Languages (DSLs) Application and utility configurations

Slide 33

Slide 33 text

Metaprogramming Uses Domain Specific Languages (DSLs) Application and utility configurations DRYing repetitive code

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

Responsible Metaprogramming Readable, usable, maintainable Well tested and documented Right tool for the job Simplest approach Avoid YAGNI (You ain’t gonna need it)

Slide 38

Slide 38 text

Protips! Avoid monkey patching and method aliasing Don’t overuse metaprogramming Do overuse metaprogramming Adhere to coding standards

Slide 39

Slide 39 text

Introducing Herd Organize ActiveRecord collection functionality into manageable classes https://github.com/ beerlington/herd

Slide 40

Slide 40 text

ActiveRecord is awesome... ...sometimes too awesome. Introducing Herd

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

Requirements: Predictable interface Simple design Resistant to internal Rails changes

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

Code Walkthrough All together now https://github.com/beerlington/herd/blob/master/lib/herd.rb

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

Additional Resources Metaprogramming Ruby http://pragprog.com/book/ppmetr/metaprogramming-ruby http://www.youtube.com/watch? v=s1MJh4VhrKM&feature=endscreen&NR=1 http://ducktypo.blogspot.com/2010/08/ metaprogramming-spell-book.html http://www.slideshare.net/burkelibbey/rubys-object- model-metaprogramming-and-other-magic

Slide 54

Slide 54 text

Thanks! @beerlington github.com/beerlington