This lesson continues with metaprogramming: how Ruby looks up methods, what include, extend and prepend do, and how ActiveSupport::Concern and our Modularity gem build on that.
Important
Work on this lesson in
buildermode.
Learning goals
- You can explain how Ruby looks up a method: the ancestor chain of a class, including singleton classes, and where
include,extendandprependinsert a module into it. - You can explain why extending an object with a module is the same as including the module into the object's singleton class.
- You can find where a method is defined even when metaprogramming defined it, e.g. with
Method#source_location, the Rails API docs or your IDE. - You can write a monkey patch that wraps an existing method and calls the original, and you can explain why
Module#prependis cleaner than aliasing and overriding. - You can forward arbitrary positional and keyword arguments to another method (e.g. with
(...)) and explain what changed about arguments in Ruby 3. - You can explain what
ActiveSupport::Concernadds to a plain module and when it is worth using. - You can build a parameterized module with our Modularity gem and explain what its bracket syntax does.
- You can compare a plain module, a Concern and a Modularity trait for the same task and argue which one fits.
Resources
Read what's new to you, skim what's familiar, skip what you already master. Stop when you can meet the learning goals.
Your agent can also generate an overview, a tutorial or an explanation for anything here, tailored to what you already know. Just ask.
- 📄 How Ruby method lookup works — our card: the ancestor chain, singleton classes,
include,extend,prepend - 📄 The Ruby Object Model — our card, if the lookup rules feel confusing
- 📄 Ruby docs:
Module#prependShow archive.org snapshot and calling methods Show archive.org snapshot — the reference for prepending and for forwarding arguments with... - 📄 Changes to positional and keyword args in Ruby 3.0 — our card: why argument forwarding needs care
- 📄
ActiveSupport::ConcernShow archive.org snapshot — Rails API docs: what it adds to a plain module - 📄 Modularity Show archive.org snapshot — our gem for parameterized modules ("traits"); read the README
- 📄 How to organize monkey patches in Ruby on Rails projects — our card
Exercises
Finding method definitions
Because we have metaprogramming, it might not be apparent where a method is defined. There are several ways to find a source:
- Learn about
Method#source_locationShow archive.org snapshot . - Look up a method like
save!in the Rails API docs Show archive.org snapshot . The result should have a link to the source location on GitHub. - CTRL+click on a method call in RubyMine. This will open a list of possible source locations.
Note
Given that Ruby is a dynamic language, RubyMine can only guess. RubyMine rarely knows the type of an object.
Monkey patch
Write a monkey patch that patches the #attributes= methods of all your models.
The monkey patch should print the number of attributes that will be set, then call the original method:
movie = Movie.find(1)
movie.attributes = { title: 'Sunshine', year: '2007' }
# Console prints "Setting 2 attributes"
# Check that the original #attributes= method is still called:
movie.title # => 'Sunshine'
movie.year # => 2007
Write two versions of that patch:
- Using
Module#prepend. - By aliasing the existing method and then overriding it.
Hint
- The base class for all your models is
ApplicationRecord. Prepend your patch withApplicationRecord#prepend(YourPatch).- A monkey patch is usually an initializer Show archive.org snapshot .
- We have some tips for organizing monkey patches.
- When you add or change an initializer you need to restart your Rails server or console for the changes to be picked up. Only changes in
appare picked up automatically.
Virtual attributes
With plain Ruby
Look at the following "mini Active Record" interface:
class Login
include Attributes
attribute :email
attribute :password
attribute :remember_me, default: true
end
login = Login.new
login.email = "me@example.org"
login.password = "mypassword"
login.email # => "me@example.org"
login.password # => "mypassword"
login.remember_me # => true
login.attributes # => { email: "me@example.org", password: "mypassword", remember_me: true }
login.remember_me = false
login.remember_me # => false
Implement the Attributes module in plain ruby, so it supports the API above.
With ActiveSupport::Concern
Add a second implementation AttributesConcern that uses ActiveSupport::Concern.
It should work identical to the first implementation.
With Modularity
Add a third implementation DoesAttributes that uses Modularity.
For modularity we often use an alternative interface:
class Login
include DoesAttribute[:email]
include DoesAttribute[:password]
include DoesAttribute[:remember_me, default: true]
end
Implement this new interface. For ruby 3 make sure that you use at least modularity 3.1.0 Show archive.org snapshot .
Compare implementations
Now compare your implementations. Which one do you like best?
Does modularity offer features the other approaches don't?