What it is and how to use it wrong, maybe.
The use case
I was building out a deploy script that ran a bunch of checks.
class CICheck < Check
def initialize
puts "Checking CI"
end
end
class SensitiveFilesCheck < Check
def initialize
puts "Checking sensitive files"
end
end
And that puts
got annoying. What if I wanted to change that across all checks? Well, I could do something like this:
class Check
def initialize(*)
puts @description
end
end
class CICheck < Check
def initialize(sha)
@description = "Checking CI"
super
@sha = sha
end
end
But that’s not really great. I have to remember to super
in every Check
. Feels like writing class components in React.
I wanted something cleaner, like:
class CICheck < Check
desc "Checking CI"
end
And have that do all the work.
Where there’s a will, there’s probably a Ruby method or keyword
I thought to myself: “there has to be a Ruby way to do this.” And with Matz as my witness, there is.
From the docs
According to RubyDoc[^1]:
Invokes Module.prepend_features on each parameter in reverse order.
Okay but what does that mean?
When this module is prepended in another, Ruby calls prepend_features in this module, passing it the receiving module in mod. Ruby’s default implementation is to overlay the constants, methods, and module variables of this module to mod if this module has not already been added to mod or one of its ancestors. See also Module#prepend.
That’s better.
Prepend to the rescue
Let’s get weird.
class Check
def self.inherited(klass)
klass.extend ClassMethods
klass.prepend InstanceMethods
end
module ClassMethods
def description
@description
end
def desc(value)
@description = value
end
end
module InstanceMethods
def initialize(*)
puts self.class.description
super
end
end
end
You know where this is going:
class CICheck < Check
desc "Checking CI"
attr_reader :sha
def initialize(sha)
@sha = sha
end
end
And now I can safely:
CICheck.new("1234") # "Checking CI"
References
[^1] https://ruby-doc.org/core-2.6.1/Module.html#method-i-prepend