I've always been a huge fan of class methods, even before I knew what they actually were. This is mostly because they almost always were readable, tiny-footprint methods that packed a punch. Take validates_presence_of
or has_secure_password
for example, you're mostly interfacing with an otherwise "simple" method but a ton of nice things are happening for you under the hood.
That said, wondering about how to make my own version of validates_presence_of
is how I learnt about class methods.
Quick explainer: class methods are methods that live on and can only be called on the class itself. Class methods are inherited by descendants of the class but aren't available to its instances — use instance methods instead.
class ActiveRecord
def self.validates_presence_of(property)
end
end
class User < ActiveRecord
validates_presence_of :name
def validate_name
validates_presence_of :name # undefined method `validates_presence_of' for #<User:0x0000557920dac930>
end
end
user = User.new
user.validates_presence_of(:name) # undefined method `validates_presence_of' for #<User:0x0000557920dac930>
In the example above, validates_presence_of
lives on the ActiveRecord class itself. Its descendants inherit it and are able to call it.
Defining class methods
There's different ways to define class methods. I'd recommend going with Style 1 or 2 as shown below. Ideally and unlike in Style 3, you shouldn't repeat yourself very often in your code. With Style 3, should you ever rename your class you'd have to also find all of its class methods and update them too.
# Style 1
class User
def self.say_hello
puts "Hello"
end
end
# Style 2
class User
class << self
def say_hello
puts "Hello"
end
end
end
# Style 3
class User
def User.say_hello
puts "Hello"
end
end
Using class methods
I personally like to use class methods to improve the readability of my classes. Usually by hiding "magic" that no one should be directly bothered with.
class User
class << self
def has_secure_password
# self is User
# class_eval allows you to make changes to a class on the fly
# here we're defining a password setter and an authenticate instance method
self.class_eval do
def password=(password)
self.password_digest = BCrypt::Password.create(password, salt: 12)
end
def authenticate(password)
digest = BCrypt::Password.new(self.password_digest)
if digest == password
self
else
false
end
end
end
end
end
end
The code above is a basic implementation of has_secure_password
and it does two things.
- Adds a setter for
password
. This lets you pass in a plaintext password and have it hashed and saved to thepassword_digest
column of your users table. - Sets up an
authenticate
instance method that lets you know if the plaintext password you pass in matches the stored hash digest.
Introducing modules
I know. I talked a lot about "readability" and "small footprint" yet the class above isn't exactly a fine example of it. True and that's where modules come in. You can move all of the class method code above into a module. One benefit is that your class is much cleaner. Another much more important benefit is that the module allows you to reuse the class method(s) across several "unrelated" classes.
module Authorizable
def self.include(base)
# self.include is called when a module is included in a class with 'include <Module>'
# its first parameter is the class the module is being included in
# 'extend' adds the methods defined in a module as class methods on the extending class. it's opposite is 'include'
base.extend AuthorizableClassMethods
end
module AuthorizableClassMethods
def requires_auth(value = true)
if value
self.class_eval do
def ensure_authorized!
# authorization logic
end
def current_user
context[:current_user]
end
end
end
end
end
end
class BaseMutation
include Authorizable
end
class SendMoney < BaseMutation
requires_auth
def resolve(amount:)
ensure_authorized!
SendMoneyJob.perform_async(current_user.id, amount)
end
end
Look at how clean BaseMutation
is with the class method code being in a module. Should you also want to have authorization happen in a different class that requires it, it's now as simple as include Authorizable
.
There's a lot of great use-cases for class methods and I hope this helps you implement them.