What Is attr_checked?
The attr_checked pattern from Metaprogramming Ruby dynamically defines attribute accessors with validation. Instead of hand-writing setter methods for every attribute, you declare the validation rule alongside the attribute name, and Ruby generates the getter and setter at class-load time.
This pattern is useful in domain models where attributes have business-rule constraints — age must be 18+, email must match a regex, price must be positive — and you want to avoid repetitive boilerplate.
How the Pattern Works
The implementation relies on two metaprogramming techniques: modules with callbacks and dynamic method definition.
When a class includes CheckedAttributes, the included callback fires and extends the class with ClassMethods. This makes attr_checked available as a class-level macro, similar to Ruby’s built-in attr_accessor. The macro calls define_method to create a customized setter at runtime, capturing the validation block in a closure.
Implementation Walkthrough
The first version uses a conditional check in the setter:
module CheckedAttributes
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def attr_checked(attr_name, &validation)
define_method("#{attr_name}=") do |value|
result = validation.call value
if result
@attr_name = value
else
raise "value is not valid"
end
end
attr_reader attr_name.to_sym
end
end
end
The second version raises immediately on invalid input and uses instance_variable_set/instance_variable_get instead of relying on attr_reader:
module CheckedAttributes
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def attr_checked(attribute, &validation)
define_method("#{attribute}=") do |value|
raise 'Invalid attribute' unless validation.call(value)
instance_variable_set("@#{attribute}", value)
end
define_method attribute do
instance_variable_get "@#{attribute}"
end
end
end
end
The second approach is more direct: it manages the instance variable explicitly rather than delegating to attr_reader. This gives you finer control over default values and initialization.
Usage Example
class Person
include CheckedAttributes
attr_checked :age do |v|
v >= 18
end
end
me = Person.new
p me.age = 39 # OK
p me.age = 39 # OK
p me.age = 12 # not OK — raises RuntimeError
The validation block receives the incoming value and must return a truthy or falsy result. You can replace the block with any callable, making validators reusable across attributes.
Beyond Basic Validation
Extend the pattern with composeable validators:
module Validators
Age = ->(v) { v.is_a?(Integer) && v >= 18 && v <= 120 }
Email = ->(v) { v =~ /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i }
end
class Employee
include CheckedAttributes
attr_checked :age, &Validators::Age
attr_checked :email, &Validators::Email
end
For production code, consider extracting the validation logic into a dedicated validation object or using Rails Active Model validations. The attr_checked pattern works best when you need lightweight, self-contained attribute constraints without pulling in a full validation framework.
Resources
- Metaprogramming Ruby Book
- Ruby define_method Documentation
- Ruby Module.included Documentation
- Instance Variables in Ruby
Comments