Here's a regular Ruby class with two properties and a constructor. To create a new instance of the class, you have to pass the arguments expected by the constructor, in the same order.
class Person
attr_accessor :name, :age
def initialize(name, age)
@name = name
@age = age
end
end
# create a new object
akshay = Person.new('Akshay', 31)
However, if you've worked with ActiveRecord models in Rails, you must have noticed that they don't include the initialize
method. Still, you can create them using the new
method, passing in a hash of attributes in any order.
class Post < ApplicationRecord
end
# create a new post
post = Post.new(title: 'hello world', body: 'how are you?')
If you try this with a plain Ruby class, it throws an error.
akshay = Person.new(name: 'Akshay', age: 31)
# wrong number of arguments (given 1, expected 2) (ArgumentError)
So the question is: How does Rails make it work? 🤔
The AttributeAssignment Module
The answer lies in the ActiveModel::AttributeAssignment
module, which includes the assign_atributes
method. It allows you to set all the attributes by passing in a hash of attributes. The hash keys must match the attribute names.
class Language
include ActiveModel::AttributeAssignment
attr_accessor :title, :author
end
ruby = Language.new
ruby.assign_attributes(title: "Ruby", author: "Matz")
ruby.title # => 'Ruby'
ruby.author # => 'Matz'
ruby.assign_attributes(author: "Yukihiro Matsumoto")
ruby.title # => 'Ruby'
ruby.author # => 'Yukihiro Matsumoto'
Under the hood, the assign_attributes
method calls the assign_attribute
method for each key-value pair.
Here's the internal implementation of this method.
def _assign_attribute(k, v)
setter = :"#{k}="
if respond_to?(setter)
public_send(setter, v)
else
raise UnknownAttributeError.new(self, k.to_s)
end
end
Let's try to understand what's going on when we pass name: 'Akshay'
when initializing the object.
- The
setter
will be set to:name=
- The
respond_to?
method checks if our class includes thename=
method, which sets the name. If it does, it calls that method. - Since our class uses the
attr_accessor :name
directive, thename=
method is present. Â Hence it calls the setter method, setting the value 'Akshay' for thename
attribute. - For attributes not defined via
attr_accessor
,attr_writer
or via overridden methods, it raises theUnknownAttributeError
.
That's nice, but how can you create new objects?
On its own, the AttributeAssignment
module won't allow you to call the new
method to create new objects. For that, you need the ActiveModel::API
module.
The API
module includes the AttributeAssignment
module and provides an initialize
method. This method takes an attributes
hash as argument and calls the assign_attributes
method, passing the hash.
module ActiveModel
module API
include ActiveModel::AttributeAssignment
def initialize(attributes = {})
assign_attributes(attributes) if attributes
super()
end
end
end
If you include the ActiveModel::API
module in your plain Ruby classes and remove the constructor, you can create new instances like Active Record models.
class Person
include ActiveModel::API
attr_accessor :name, :age
end
# akshay = Person.new(name: 'Akshay', age: 31)
=> #<Person:0x000000010d045b60 @age=31, @name="Akshay">
How Do Rails Models Get This Behavior?
All Active Record models inherit from the ApplicationRecord
class, which inherits from the ActiveRecord::Base
class. The Base
class includes the ActiveRecord::AttributeAssignment
module. Finally, this module includes the ActiveModel::AttributeAssignment
module.
Now, the ActiveRecord::AttributeAssignment
module provides its own internal implementation for the private methods, but that's a topic for another blog post.
I hope this post helped you gain a deeper understanding of the AttributeAssignment
module. You can use it with plain Ruby classes to simplify your code. You can even use this feature outside Rails by only using the ActiveModel framework and including the ActiveModel::API
module.
If you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I look forward to hearing from you.
Please subscribe to my blog if you'd like to receive future articles directly in your email. If you're already a subscriber, thank you.