27
loading...
This website collects cookies to deliver better user experience
1.day.ago
.descendants
. This method returns all the subclasses of the called class. For example, ApplicationRecord.descendants
will return the classes in your app that inherit from it (e.g., all the models in your application). In this article, we'll take a look at how it works, why you might want to use it, and how it augments Ruby's built-in inheritance-related methods.class BaseClass
def base
"base"
end
def overridden
"Base"
end
end
class SubClass < BaseClass
def overridden
"Subclass"
end
end
SubClass.new.overridden
gives us "SubClass"
. However, SubClass.new.base
is not present in our SubClass definition, so Ruby will go through each of the ancestors to see which one implements the method (if any). We can see the list of ancestors by simply calling SubClass.ancestors
. In Rails, the result will be something like this:[SubClass,
BaseClass,
ActiveSupport::Dependencies::ZeitwerkIntegration::RequireDependency,
ActiveSupport::ToJsonWithActiveSupportEncoder,
Object,
PP::ObjectMixin,
JSON::Ext::Generator::GeneratorMethods::Object,
ActiveSupport::Tryable,
ActiveSupport::Dependencies::Loadable,
Kernel,
BasicObject]
SubClass
is at the top, with BaseClass
below it. Also, note that BasicObject
is at the bottom; this is the top-level Object in Ruby, so it will always be at the bottom of the stack.module PrependedModule
def test
"module"
end
def super_test
super
end
end
# Re-using `BaseClass` from earlier
class SubClass < BaseClass
prepend PrependedModule
def test
"Subclass"
end
def super_test
"Super calls SubClass"
end
end
[PrependedModule,
SubClass,
BaseClass,
ActiveSupport::Dependencies::ZeitwerkIntegration::RequireDependency,
...
]
PrependedModule
is now first-in-line, meaning Ruby will look there first for any methods we call on SubClass
. This also means that if we call super
within PrependedModule
, we will be calling the method on SubClass
:> SubClass.new.test
=> "module"
> SubClass.new.super_test
=> "Super calls SubClass"
class BaseClass
def super_test
"Super calls base class"
end
end
module IncludedModule
def test
"module"
end
def super_test
super
end
end
class SubClass < BaseClass
include IncludedModule
def test
"Subclass"
end
end
[SubClass,
IncludedModule,
BaseClass,
ActiveSupport::Dependencies::ZeitwerkIntegration::RequireDependency,
...
]
IncludedModule
if they are not present in SubClass
. As for super
, any calls to super
in the SubClass
will go to IncludedModule
first, while any calls to super
within IncludedModule
will go to BaseClass
.> SubClass.new.test
=> "Subclass"
> SubClass.new.super_test
=> "Super calls BaseClass"
class SubClass < BaseClass
include IncludedModule
include IncludedOtherModule
end
class SubClass < BaseClass
include IncludedOtherModule
include IncludedModule
end
super
would be resolved to. Personally, I'd avoid having methods that overlap each other like this as much as possible, specifically to avoid having to worry about things like the order the modules are included.include
and prepend
for modules, I think a more real-world example helps to show when you might choose one over the other. My main use-case for modules like these is with Rails engines.def password_digest(password)
Devise::Encryptor.digest(self.class, password)
end
# and also in the password check:
def valid_password?(password)
Devise::Encryptor.compare(self.class, encrypted_password, password)
end
Devise::Encryptable::Encryptors
, which is the correct way to do it. For demonstration purposes, however, we'll be using a module.# app/models/password_digest_module
module PasswordDigestModule
def password_digest(password)
# Devise's default bcrypt is better for passwords,
# using sha1 here just for demonstration
Digest::SHA1.hexdigest(password)
end
def valid_password?(password)
Devise.secure_compare(password_digest(password), self.encrypted_password)
end
end
begin
User.include(PasswordDigestModule)
# Pro-tip - because we are calling User here, ActiveRecord will
# try to read from the database when this class is loaded.
# This can cause commands like `rails db:create` to fail.
rescue ActiveRecord::NoDatabaseError, ActiveRecord::StatementInvalid
end
Rails.application.eager_load!
in development or add a Rails initializer to load the file. By testing it out, we can see it works as expected:> User.create!(email: "[email protected]", name: "Test", password: "TestPassword")
=> #<User id: 1, name: "Test", created_at: "2021-05-01 02:08:29", updated_at: "2021-05-01 02:08:29", posts_count: nil, email: "[email protected]">
> User.first.valid_password?("TestPassword")
=> true
> User.first.encrypted_password
=> "4203189099774a965101b90b74f1d842fc80bf91"
include
and prepend
would have the same result, but let's add a complication. What if our User model implements its own password_salt
method, but we want to override it in our module methods:class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :posts
def password_salt
# Terrible way to create a password salt,
# purely for demonstration purposes
Base64.encode64(email)[0..-4]
end
end
password_salt
method when creating the password digest:def password_digest(password)
# Devise's default bcrypt is better for passwords,
# using sha1 here just for demonstration
Digest::SHA1.hexdigest(password + "." + password_salt)
end
def password_salt
# an even worse way of generating a password salt
"salt"
end
include
and prepend
will behave differently because which one we use will determine which password_salt
method Ruby executes. With prepend
, the module will take precedence, and we get this:> User.last.password_digest("test")
=> "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3.salt"
include
will instead mean that the User class implementation takes precedence:> User.last.password_digest("test")
=> "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3.dHdvQHRlc3QuY2"
prepend
first because, when writing a module, I find it easier to treat it more like a subclass and assume any method in the module will override the class' version. Obviously, this is not always desired, which is why Ruby also gives us the include
option.#descendants
method comes in. The method is quite short and easily duplicated outside Rails if needed:class Class
def descendants
ObjectSpace.each_object(singleton_class).reject do |k|
k.singleton_class? || k == self
end
end
end
ObjectSpace#each_object
, when passed a module, returns only objects that match or are subclasses of the module; the block here also rejects the top level (e.g., if we call Numeric.descendants
, we don't expect Numeric
to be in the results).Class
and returns a list of descendant classes, or you may think of it as the "family tree" of that class' children, grandchildren, etc.#valid?
test.#descendants
is the answer:# Ensure all models are loaded (should not be necessary in production)
Rails.application.load! if Rails.env.development?
ApplicationRecord.descendants.each do |model_class|
# in the real world you'd want to send this off to background job(s)
model_class.all.each do |record|
if !record.valid?
HoneyBadger.notify("Invalid #{model.name} found with ID: #{record.id}")
end
end
end
ApplicationRecord.descendants
gives us a list of every model in a standard Rails application. In our loop, then, model
is the class (e.g., User
or Product
). The implementation here is pretty basic, but the result is this will iterate through every model (or, more accurately, every subclass of ApplicationRecord) and call .valid?
for every row.#ancestors
with #descendants
. This method is seldom used as far as I've seen, but once you know it's there, you'll probably find more and more uses for it. Personally, I've used it not just for checking model validity, but even with specs to validate that we are correctly adding attribute_alias
methods for all our models.