Classes and Instances in Ruby
Tags: ruby, beginners, oop
Published: 2020-07-13 21:40:07

When I was first learning Ruby, one of the concepts many of my classmates struggled with was object orientation. Ruby is a pure object-oriented language which means that everything in Ruby is an object. Even the term 'nil' that Ruby uses to represent nothing/null is an object. Objects in programming can include multiple variables and methods. For example, whenever we create a string in ruby, we are actually creating an object that is an instance of the class String.

```ruby
string_instance = "table"
string_instance.chars.sort.join #=> "abelt"
```

We have access to the chars method because it is an instance method of the String class. The chars method creates an instance of Array from our string so that we can access the method sort which is an instance method of the class Array. The instance method join is also from the class of Array which then creates an instance of String again. If you're having trouble following, don't worry, this will make a lot more sense when we discuss how classes and instances are created.

When we want to represent something, we can create our own classes to do so. The class itself is an object and the instances of that class are also objects. Now, what is a class?

<h2>Classes in Ruby</h2>

When we define a class in Ruby, we're really creating a category with which we can create new objects. For example, let's say we have an application where we want to store users. We can create a class called User.

```ruby
class User
    # code relating to the user class goes here
end
```

Now that we created a class, we can create new users using this class.

```ruby
molly = User.new()

p molly #=> #<User:0x00007f8c738acbc0>
```

Now Molly is an <em>instance</em> of our User class. However, we can't do much with Molly since our USer class doesn't have any extra functionality. So, let's give all our users a name.

```ruby
class User
    def initialize(name)
        @name = name
    end
end

molly = User.new("Molly")

molly #=> #<User:0x00007fdafab1e428 @name="Molly">

molly.name #=> NoMethodError (undefined method `name' for #<User:0x00007fdafab1e428 @name="Molly">)
```

When we create a class in Ruby, we can include the initialize method. This method takes the number of arguments we give and is called when we create a new instance. When we call the .new() method, initialize will be called. Ruby will check that we've provided the proper number of arguments and will throw an argument error now if we try to call User.new() without a name passed in.

We are assigned the instance variable name to the name passed into the new function. By using the '@' symbol before a variable, we are saying that variable is an instance variable in our class. An instance variable is one that is stored under an instance of our class (such as molly) rather than the class itself. I'll include more on this later so don't worry if you're confused.

However, we can also see that trying to access Molly's name directly doesn't work. This is because we don't have a 'getter' method.

<h2>Setters and Getters</h2>

In order to read the name value of our User instances, we need to create a getter method.

```ruby
class User
    def initialize(name)
        @name = name
    end
    
    # getter method
    def readName
        @name
    end
end

molly = User.new("Molly")
molly.readName #=> "Molly"
```

Our getter method just returns the name property. I used 'readName' as the function name to illustrate that we can call it whatever we want. However, since we actually want to call molly.name, we can also write:

```ruby
class User
    def initialize(name)
        @name = name
    end
    
    # getter method
    def name
        @name
    end
end

molly = User.new("Molly")
molly.name #=> "Molly"
```

Setter methods allow us to 'set' or write variables.

```ruby
class User
    def initialize(name)
        @name = name
    end
    
    # getter method
    def name
        @name
    end

    # setter
    def catch_phrase=(catch_phrase)
        @catch_phrase = catch_phrase
    end

    # getter
    def catch_phrase
        @catch_phrase
    end
end

tony = User.new("Tony Montana")

tony.catch_phrase #=> nil

tony.catch_phrase = "Say hello to my little friend!"

tony.catch_phrase #=> "Say hello to my little friend!"
```

In order to define a setter method, we declare a method with an equals operator. When we call tony.catch_phrase =, we are actually using shorthand for:

```ruby
tony.catch_phrase=("Say hello to my little friend!")
```

<h2>Attr Writers, Reader, and Accessors</h2>

Coding every variable we want to be able to set and get for each instance could be incredibly code-intensive. The code would also be very repetitive. Luckily, Ruby has a macro we can use to define setters and getters!

```ruby
class User
    attr_reader :name, :catch_phrase
    attr_writer :catch_phrase

    def initialize(name)
        @name = name
    end
end
```

The code above is the exact same as before, the attr_reader writes the 'getter' methods and the attr_writer writes the 'setter' methods. If we have a variable using both the reader and writer, we can use attr_accessor to write both methods for us.

```ruby
class User
    attr_reader :name
    attr_accessor :catch_phrase

    def initialize(name)
        @name = name
    end
end

tony = User.new("Tony Montana")

tony.catch_phrase #=> nil

tony.catch_phrase = "Say hello to my little friend!"

tony.catch_phrase #=> "Say hello to my little friend!"

tony #=> #<User:0x00007fc5d2949a40
 @catch_phrase="Say hello to my little friend!",
 @name="Tony Montana">
```

<h2>Other Instance Methods</h2>

Our instance methods don't need to be limited to setters and getters. We can create other methods as well. To illustrate this, let's look at dogs and cats as an example.

```ruby
class Dog
    attr_accessor :name

    def initialize(name)
        @name = name
    end

    def speak
        "WOOF!"
    end
end

class Cat
    attr_accessor :name

    def initialize(name)
        @name = name
    end

    def speak
        "Meeeoooww."
    end
end

chewy = Dog.new("Chewy")
lucy = Dog.new("Lucy")
mittens = Cat.new("Mittens")
gretchen = Cat.new("Gretchen")

chewy.speak #=> "WOOF!"
lucy.speak #=> "WOOF!"
mittens.speak #=> "Meeeoooww."
gretchen.speak #=> "Meeeoooww."
```

<h2>Inheritance</h2>

Let's take our animal classes and expand on them. We are going to create a model for a vet office. For this, we're going to have a class of Pet, Dog, Cat, and SmallAnimal.

```ruby
class Pet
end

class Dog < Pet
    attr_accessor :name

    def initialize(name)
        @name = name
    end
end

class Cat < Pet
    attr_accessor :name

    def initialize(name)
        @name = name
    end
end

class SmallAnimal < Pet
    attr_accessor :name

    def initialize(name)
        @name = name
    end
end
```

Now, we know all our animal instances are examples of the Pet class, so we can have SmallAnimal, Dog, and Cat inherit from Pet. What this allows us to do is create methods in our Pet class that are relevant to all animals. For example, we can set up an appointment reminder method that returns a string with a reminder and that pet's name.

```ruby
class Pet
    def appointment_reminder
        "Time to make an appointment for #{@name}"
    end
end
```

Now whenever we create a new instance of Dog, SmallAnimal, or Cat, we can access this appointment_reminder method.

```ruby
spot = Dog.new("Spot")

spot.appointment_reminder #=> "Time to make an appointment for Spot"
```

<h2>Class Methods</h2>

Let's say our vet office needed to be able to access a list of all Pets that have been created. We want this list specific to the type of pet. We can use a class method in order to achieve this.

```ruby
class Dog
    attr_accessor :name

    @@all = []

    def initialize(name)
        @name = name
        @@all << self
    end

    def self.all
        @@all
    end
end

Dog.new("Spot")
Dog.new("Lucy")
Dog.new("Jitta")
Dog.new("Sully")
Dog.new("Chewy")

Dog.all #=> [#<Dog:0x00007fe584171f58 @name="Spot">,
 #<Dog:0x00007fe5841a3e40 @name="Lucy">,
 #<Dog:0x00007fe584299ac0 @name="Jitta">,
 #<Dog:0x00007fe58430be40 @name="Sully">,
 #<Dog:0x00007fe58435aec8 @name="Chewy">]

Dog.all[0] #=> #<Dog:0x00007fe584171f58 @name="Spot">
Dog.all[3] #=> #<Dog:0x00007fe58430be40 @name="Sully">
```

Using '@@' signifies a <em>class</em> variable. 

First, we declare a class variable equal to an empty array. 

Then, each time a new Dog is created, we push that Dog instance into our @@all array. Self in Ruby refers to whatever we are currently calling a method on. When we call the initialize method, it is on the new instance of Dog we created.

```ruby
spot = Dog.new('Spot')
# => calls this: spot.initialize('Spot')
```

Therefore, 'self' in the initialize method represents spot in this case. Spot is pushed into the @@all class variable. 

Finally, we declare a getter method so that we can access this class variable. We use self to declare a class variable because, within this scope, self represents the class Dog. So, def 'self.all' means 'Dog.all'.

```ruby
class Dog
    def self.all
    end

    # technically can also be written
    def Dog.all
    end
end
```

<h3>Final Thoughts</h3>

Let me know:
<ol>
  <li>What are you confused about with object orientation?</li>
  <li>Are there concepts about classes in Ruby that I didn't go over that could use some clarification?</li>
  <li>What is your favorite part about OOP (object-oriented programming)?</li>
  <li>As always, any corrections or critiques?</li>
</ol>

Happy coding!