- Define what a macro is
- Use macros to abstract away the explicitly defined setter and getter methods in a Ruby class
Normally when you write a method, you are manipulating and returning some kind of data. This (kind of useless) method takes an integer and returns an integer:
def plus_three(num)
num + 3
end
By the same token, Ruby's #map
method is called on an enumerable and returns
an array:
mantra = ["Don't", "Repeat", "Yourself"]
mantra.map do |word|
word[0]
end
# => ["D", "R", "Y"]
In Ruby, a macro is similar to a method; it's just some code that, instead of returning a Ruby datatype, returns more Ruby code! When you run your program, this code will get executed along with all the other code you have written. In this lesson we will learn how to use macros to write the definitions of other methods — crazy!
The implementation of macros is considered metaprogramming — the writing of programs that operate on other programs. Metaprogramming can make our lives easier by automating repetitive tasks or providing something like a mini-language inside of another programming language that has the benefits of being concise and descriptive. Metaprogramming is a powerful tool, but one of the dangers of using it is that it can result in very hard to follow code that obscures what is actually happening.
If this sounds complicated, the answer is it can be — though it doesn't need to be. Below we'll implement some simple macros to abstract away the manual, explicit definition of setter and getter methods in a Ruby class.
In object-oriented Ruby, there is a strong convention to have both a setter and
a getter method that pertain to the same attribute. For example, a #name
getter and a #name=
setter on our Person
class. Because this pattern is so
common, we find ourselves making these manual setter and getter definitions
again and again. As Rubyists, we are lazy, and this is a virtue. If we can
achieve the same result with less code, we'll do it.
Check out the example below:
class Person
attr_reader :name
attr_writer :name
end
The attr_reader
macro, followed by the attribute name :name
, created a
getter method for us.
The attr_writer
macro, followed by the attribute name :name
, created a
setter method for us.
These two methods effectively write Ruby code within our classes!
class Person
attr_reader :name
# def name
# @name
# end
attr_writer :name
# def name=(value)
# @name = value
# end
end
Note: What we're actually doing in each of those two lines of code is
calling the attr_reader
and attr_writer
methods and passing in the symbol
:name
as an argument. Recall that in Ruby, calling methods works with or
without parentheses, so attr_reader :name
is the same as attr_reader(:name)
.
We can now do the following, without explicitly defining a #name
or #name=
method ourselves:
jay_z = Person.new
jay_z.name = "Shawn Carter"
jay_z.name
# => "Shawn Carter"
Let's compare a Person
class that uses macros to one that uses explicit setter
and getter methods:
Using macros:
class Person
attr_writer :name
attr_reader :name
end
Using explicit method definitions:
class Person
def name=(name)
@name = name
end
def name
@name
end
end
If we need to have both a getter and a setter method for an attribute on our
Person
class (which isn't always the case — we'll learn more about that later),
we can use another macro called an attribute accessor.
So this code...
class Person
attr_writer :name
attr_reader :name
end
...can now be written like this:
class Person
attr_accessor :name
end
...which is still equivalent to this:
class Person
def name=(name)
@name = name
end
def name
@name
end
end
We now have access to reader and writer methods from one macro!
In most cases, the usage of macros is preferred over the explicit definition of setter and getter methods. To understand why, simply look at the code above. Which class would you rather write? The longer one, or the shorter one? Furthermore, seeing these macros at the top of your class definition allows you to get a high level overview of the class right from the start: you can immediately see what attributes the class defines, and which ones have getter and/or setter methods, without having to scroll down through the code.
While you should be using these macros in most cases, the one exception is if we
need to customize the implementation of a getter or setter method. Let's
consider our Person
example:
class Person
attr_accessor :name
end
jay_z = Person.new
jay_z.name = "Shawn Carter"
jay_z.name
# => "Shawn Carter"
Let's say that instead of storing the full name in a single name
variable, we
decided we want to store the first and last name separately. We could simply
replace our attr_accessor
for name
with attr_accessor
s for first_name
and last_name
, but then we'd need to set two separate attributes when we
create a new instance of the Person
class.
To get around this, we can create a custom setter method that will enable us to continue passing the full name when we create an instance, but will store the first and last name as separate attributes:
def name=(full_name)
first_name, last_name = full_name.split
@first_name = first_name
@last_name = last_name
end
With this code, we assign a name to an instance in the usual way, passing in the full name as the argument. Our custom setter method splits the full name into the first and last names and assigns them to the corresponding instance variables.
We can also create a custom getter method for name
that will return the full
name even though it is no longer stored in an instance variable:
def name
"#{@first_name} #{@last_name}".strip
end
Putting it all together:
class Person
attr_reader :first_name, :last_name
def name=(full_name)
first_name, last_name = full_name.split
@first_name = first_name
@last_name = last_name
end
def name
"#{@first_name} #{@last_name}".strip
end
end
jay_z = Person.new
jay_z.name = "Shawn Carter"
jay_z.first_name
# => "Shawn"
jay_z.last_name
# => "Carter"
jay_z.name
# => "Shawn Carter"
With our custom setter and getter, we can still assign and return the full name,
just as before. Furthermore, by adding the attr_reader
s for first_name
and
last_name
, we can also access the first and last names separately.
NOTE: The example above also provides a good illustration of why it's a good
idea to use instance methods rather than accessing instance variables directly.
If, instead of using the #name
getter, we had been calling the @name
instance variable directly in multiple places in our code, we would then need to
find all of those places and refactor our code to handle the separate
first_name
and last_name
variables. By encapsulating our desired change in
our custom setter and getter methods, we have ensured that we can continue to
call our #name
instance method without breaking our application.
Run the test suite to get started. You'll be writing your code in the
lib/meowing_cat.rb
file.
- Define a class called
Cat
. - Use the
attr_accessor
macro to create a setter and getter method for a cat's name. - Write a method,
#meow
, that outputs "meow!" to the terminal using theputs
method when called on an instance ofCat
.