Tracking inheritance via Class#inherited
Yesterday we used Class#subclasses to find subclasses of a class and used it to implement a CLI command registry. However, this approach had a limitation in that it only tracked direct subclasses, and wouldn't include deeper descendants.
While we could implement a recursive search to find all descendants, that wouldn't give me an opportunity to show off the inherited hook, so here we go!
Let's start with some simple examples of the inherited hook in action:
class Parent
def self.inherited(subclass)
puts "#{subclass} is inheriting from #{self}"
end
end
class Child < Parent
end
# Output: Child is inheriting from Parent
AnotherChild = Class.new(Parent)
# Output: AnotherChild is inheriting from Parent
# Since the hook itself is inherited, it triggers on the Child class as well:
class GrandChild < Child
end
# Output: GrandChild is inheriting from Child
Now let's use it to implement a command registry in a way which also includes deeper descendants:
class Command
@commands = {}
def self.commands
@commands
end
def self.inherited(subclass)
# E.g. {"greet" => Greet, "add" => Add }
Command.commands[subclass.name.downcase] = subclass
end
end
class Greet < Command
def description = "Greet someone"
def usage = "cli.rb greet <name>"
def call(name) = puts "Hello, #{name}!"
end
class Greet_Enthusiastically < Greet
def description = "Greet someone enthusiastically"
def usage = "cli.rb greet_enthusiastically "
def call(name) = super "❤️ #{name.upcase} ❤️!!"
end
class Add < Command
def description = "Add two numbers"
def usage = "cli.rb add <x> <y>"
def call(x, y) = puts x.to_i + y.to_i
end
help = ARGV.delete("--help")
if ARGV.empty?
puts "Usage: cli.rb <command> [args...]"
puts
puts "Available commands:"
Command.commands.keys.each do |name|
puts " #{name}"
end
exit
end
command_name = ARGV.shift
command_class = Command.commands[command_name]
abort "ERROR: Unknown command - #{command_name}" unless command_class
command = command_class.new
if help
puts "Command: #{command_name}"
puts command.description
puts "Usage: #{command.usage}"
exit
end
command.call(*ARGV)
There we have it, we can now inherit to our heart's content!
Note that while the inherited method gets inherited by subclasses, the @commands instance variable does not, hence we refer explicitly to Command.commands to ensure subclasses register on the same hash.
How it works in practice:
$ ruby cli.rb
Usage: cli.rb <command> [args...]
Available commands:
add
greet
greet_enthusiastically
$ ruby cli.rb greet Kabir
Hello, Kabir!
$ ruby cli.rb greet_enthusiastically Kabir
Hello, ❤️ KABIR ❤️!!!
Couldn't have wished for a more enthusiastic greeting ❤️🔥
History
The first version of Class#inherited shipped in Ruby 1.2 in 1998, with some tweaks following in later versions:
Ruby 1.6 made Class.new(superclass) trigger inherited as well.
Ruby 1.8 tightened the timing so the hook runs after the class has been assigned to its constant, but before the class body is evaluated.