Lesson Monday

We know that everything in Ruby is an object. Not only that, but every object is an instance of a class. So far, we’ve used Ruby’s built-in classes, including String and Integer. We’ve also defined all of our methods on main, the top-level of a Ruby application.

Utilizing Ruby’s built-in classes is standard practice — we really shouldn’t be defining methods on main. Instead, any new methods we define should be scoped to a class.

If we wanted to, we could add a method to a built-in Ruby class. Here’s an example. Remember how we created a scramble() method that allows users to reverse and upcase a string? We had to define it like this:

def scramble(string)
  string.reverse().upcase()
end

Instead of calling this method on main, we could do this:

class String
  def scramble(string)
    string.reverse().upcase()
  end
end

Ruby will reopen the String class at runtime and add this method to the String class. We could then call scramble() on any string.

However, adding methods to existing Ruby classes is almost always a bad practice and should be avoided. These methods can clash with other methods and cause bugs that are difficult to find and fix, especially in larger applications.

So if we can’t call a method on main or modify an existing class, what should we do?

Creating Custom Classes


Let’s create our own class from scratch. We’ll create a new Word class that has the scramble() method. Here’s how we do that:

class Word
  def scramble
    reverse().upcase()
  end
end

Classes always start with an uppercase letter. If a class name has multiple words (like NilClass), we should capitalize the first letter of each word. This is called upper camel case (in contrast to lower camel case in JavaScript, where all words are capitalized except for the first one). We also need to make sure that each class is closed with end.

The scramble() method defined above is only available on instances of the Word class. If we try to call the method on a plain old string, we’ll get an error: undefined method 'scramble' for String.

So how do we call this method? We can instantiate a new instance of a word by doing this:

word = Word.new()

However, we still can’t call scramble() on it; reverse() and upcase() are both String methods and we don’t actually have a string to call our method on! We can see this if we try to call word.scramble(). We’ll get the following error: undefined local variable or method 'reverse' for #<Word>.

So how do we actually pass a string into this class so we can scramble() it?

The Initialize Method


All classes in Ruby recognize a method called initialize. Any code we put inside the initialize method will run as soon as an object is created. Let’s add an initialize method to our Word class:

class Word
  def initialize
    puts "word initialized!"
  end
end

When we create a new instance of Word, initialize will be called and we’ll get a word initialized! message in the terminal.

initialize also takes arguments. Let’s modify the code above to demonstrate:

class Word
  def initialize(word)
    puts "This is the value of word: #{word}!"
  end
end

Now we can pass an argument into an instance of Word like this:

word = Word.new("hello")

We’ll see the following in the terminal: This is the value of word: hello! We use string interpolation to pass the value of word into a string; if you need a refresher on this concept, see Defining Methods in Ruby.

However, we still can’t use this inside our scramble() method because word is a local variable. It’s scoped only to the initialize method and will be cleared from memory as soon as the method finishes running. If we try to access the value inside our scramble() method, we’ll get an undefined local variable error.

Instance Variables


In this case, we need to use an instance variable instead of a local variable. Instance variables are the most commonly used variables in Ruby outside of local variables, and you’ll be using them very regularly.

We’ve already briefly discussed instance variables in Scope in Ruby, but let’s go over them again. We use the @ symbol for instance variables like this: @word. The @ symbol isn’t just for readability; it’s necessary for the Ruby interpreter, too.

Each object has its own instance variables and its instance variables are only available to that object. These variables are a way to save information, or attributes, related to an object.

Here’s an example. Let’s say we have a Cat class and each cat has its own unique name. We can use the initialize method to save unique names to an instance of Cat. Here’s how:

class Cat
  def initialize(name)
    @name = name
  end
end

When we create a new instance of class Cat, we pass in a name. We then save the value of the name variable (which is local) by assigning its value to @name, which is an instance variable. Now we can create several instances of class Cat, each with a different name:

> cat1 = Cat.new("James")
=> #<Cat:0x007fca048236f8 @name="James">
> cat2 = Cat.new("Jasmine")
=> #<Cat:0x007fca048134d8 @name="Jasmine">

IRB returns the value of each Cat’s @name value when it’s instantiated. cat1 has its own name; so does cat2. If we stored each Cat’s name as a local variable, it would fall out of scope as soon as the object finished initializing and the object would “forget" its name. Once we save name as an instance variable, though, the object “remembers" it.

Reader methods


However, we still don’t have a way to retrieve the value of a Cat’s name; for instance, we can’t call cat1.name(). That’s because the Cat class doesn’t have a name() method. Let’s add one now:

class Cat
  def initialize(name)
    @name = name
  end

  def name
    @name
  end
end

We’ve added a name() method that uses Ruby’s implicit return to return the stored value of @name. We can now do the following:

cat1.name()
> "James"
cat2.name()
> "Jasmine"

name is a reader method. The purpose of a reader method is to "read" an attribute of an object. You’ll use reader methods regularly throughout Ruby, and you’ll be learning a shorthand way of writing these methods later in this section. Note that you should always test all of your methods; this includes reader methods as well!

Using Instance Variables with Instance Methods


Just as we have instance variables, we also have instance methods. In fact, all methods that we define inside a class using def some_method are called instance methods because they are called on an instance (or object) of the class.

Let’s apply what we’ve learned so far to our Word class and its scramble() method. We’ll create an instance variable which we can then pass into our scramble() instance method.

class Word
  def initialize(str)
    @word = str
  end

  def scramble
    @word.reverse().upcase()
  end
end

When we initialize a new Word, we pass it a single argument, which is saved as @word. While an instance variable (in this case @word) and the argument passed into the initialize method (in this case str) are usually named the same thing, this example is intentionally structured to demonstrate that they don't have to match.

Now we can access @word in our scramble() method:

> word1 = Word.new("hippo")
=> #<Word:0x007fca048a0658 @word="hippo">
> word2 = Word.new("rhino")
=> #<Word:0x007fca04891cc0 @word="rhino">
> word1.scramble()
=> "OPPIH"
> word2.scramble()
=> "ONIHR"

Once again, this example illustrates how each instance of an object has access to its own instance variables and only its own. While word1 and word2 both have a @word instance variable, they each have their own unique values.

Remember that Ruby is strict about arity (the number of arguments a method can take). If we pass arguments into initialize, then we need to pass exactly that many arguments into the new() method. We can no longer do the following: Word.new() because we’ll get the following error: wrong number of arguments (given 0, expected 1). It's very common for objects to have multiple attributes. For instance, a Car class might have a year, color, make and many others.

Summary


Let’s do a quick recap on instance variables because they are so important in Ruby.

Instance variables are only visible to the object they belong to, not to the class itself or anywhere else in your application. We can never call @word unless we are “inside" an object with that instance variable.

If we do try to call @word outside of an object that has that instance variable, it will return nil. When we call an instance method like scramble() or name() on an object, we have access to that object’s self as well as its instance variables. We are "inside" the object.

From now on, we’ll be storing our methods inside of custom classes, using the initialize method to save an object’s attributes, and then calling methods on an instance of a class.

Terminology


  • Attributes: The properties of a Ruby object.

  • Initialize method: When instantiating a new object, it will trigger the initialize method in the class that object belongs to. Any code we want to run when we call the .new() method goes in here. This method is often used to assign values to instance variables.

  • Instance method: Methods that are called on an instance of a class. They are the most common type of method in Ruby.

  • Instance variable: Along with local variables, instance variables are the most commonly used variables in Ruby. They are scoped to an instance of a class.

  • Reader method: A method to read the value of an instance variable. Without a reader method, we can't access the value of an object's attributes outside the class.

  • Upper camel case: A naming convention where all words in a string are capitalized. Custom Ruby classes use this convention. For example, we could name a class ThisIsACustomClass.

Custom Classes


class CustomClass

  def initialize
    # Code that runs when a new instance of CustomClass is created.
  end

  def this_is_an_instance_method
    # Instance method code goes here.
  end
end

To instantiate a new CustomClass:

custom_class_object = CustomClass.new()

To call an instance method on an instance of CustomClass:

custom_class_object.this_is_an_instance_method()

Lesson 6 of 22
Last updated August 7, 2022