Extending classes on inclusion via Module#included
Earlier this week we looked at how to include and extend classes with modules.
For simple cases, that's all we need. However, a common pattern is to provide both instance methods and class methods from the same module. Such modules commonly define the included hook, which is called when the module is included in a class. This allows the module to extend the class with additional methods at the moment of inclusion:
module Discountable
def self.included(base)
base.extend ClassMethods
end
def sale_price(discount_percent = self.class.discount_percent)
price * (1 - discount_percent / 100.0)
end
module ClassMethods
def discount_by(percent)
@discount_percent = percent
end
def discount_percent
@discount_percent || 0
end
end
end
class Tea
include Discountable
discount_by 10
attr_reader :name, :price
def initialize(name, price)
@name = name
@price = price
end
end
class Vinyl
include Discountable
discount_by 25
attr_reader :artist, :price
def initialize(artist, price)
@artist = artist
@price = price
end
end
Tea.new("Chai Latte", 5.50).sale_price
# => 4.95
Vinyl.new("The Dark Side of the Moon", 32.00).sale_price
# => 24.0
In this new take on the Discountable module, we can set a default discount percentage for each class that includes it, and the instance method sale_price will use that percentage when calculating the sale price.
Note the idiomatic pattern of defining a ClassMethods module inside the main module, and then extending the base class with it in the included hook, keeping the class methods nicely separated from the instance methods.
History
The included hook shipped in Ruby 1.8.0, released in August 2003.
The companion extended hook followed in Ruby 1.8.1, released in December 2003.