Have you ever wondered how you would create your own Ruby attr_reader and attr_writer.
Just so we are on the same page.
class Cat attr_reader :name attr_writer :name def initialize(<span class="hiddenGrammarError" pre=""><span class="hiddenGrammarError" pre=""><span class="hiddenGrammarError" pre=""><span class="hiddenGrammarError" pre="">name) @name</span></span></span></span> = name end end cat = Cat.new("Tobias") cat.name #=> "Tobias" cat.name #= "New Tobias" cat.name #=> "New Tobias"
attr_reader in the Cat class above is the same as if we create a method inside of it that the object’s the attribute name.
and attr_writer, as you have guessed but now allows the attribute to be written or assigned
class Cat def initialize(name) @name = name end def name @name end def name=(value) @name = value end end
Build Your Own
Because attr_reader and attr_writer are pieces of code that creates code, we’ll have to enter the realm of metaprogramming.
For attr_reader, we will create an instance method that return its corresponding instance variable. And for attr_writer, we will code an instance method that assigns a value to an instance variable and returns it.
module Attrs def my_attr_reader(attr_name) define_method(attr_name) do instance_variable_get("@"+attr_name.to_s) end end def my_attr_writer(attr_name) define_method("#{attr_name}=") do |value| instance_variable_set(“@“+attr_name.to_s, value) end end end
In the module above I am using Ruby methods that let me define a method(define_method), get an instance variable(instance_variable_get) and set it(instance_variable_set). Don’t you love Ruby?
class Cat extend Attrs my_attr_reader :name my_attr_writer :name def initialize(name) @name = name end end cat = Cat.new("Tobias") cat.name #=> "Tobias" cat.name = "New Tobias" cat.name #=> "New Tobias"
Voilá
Notice that I am using extend rather than include with the module Attrs. This is so my_attr_reader and my_attr_writer can become class methods.
Also, these are defining my_attr_reader and my_attr_writer once. In order to define multiple readers and writers at once like attr_reader and attr_writer you can do
module Attrs def my_attr_reader(*attr_names) attr_names.each do |attr_name| define_method(attr_name) do instance_variable_get("@"+attr_name.to_s) end end end def my_attr_writer(*attr_names) attr_names.each do |attr_name| define_method("#{attr_name}=") do |value| instance_variable_set("@"+attr_name.to_s, value) end end end end
*attr_names lets you pass in zero or more attribute names. For each of those, a corresponding method is defined.
In case you are curious, the actual attr_reader and attr_writer in Ruby are implemented with functions written in C. When we simulate this behavior with our own Ruby methods we don’t get the advantage of these C functions.
And that is how you can replicate Ruby attr_reader and attr_writer with metaprogramming.
We used define_method, instance_variable_get, and instance_variable_set to define a method on a caller, get and set an instance variable.
This is just the tip of the metaprogramming iceberg.
Happy metaprogramming!