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.
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?
Classes in Ruby
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.
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.
molly = User.new()
p molly #=> #<User:0x00007f8c738acbc0>
Now Molly is an instance 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.
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.
Setters and Getters
In order to read the name value of our User instances, we need to create a getter method.
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:
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.
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:
tony.catch_phrase=("Say hello to my little friend!")
Attr Writers, Reader, and Accessors
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!
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.
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">
Other Instance Methods
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.
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."
Inheritance
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.
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.
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.
spot = Dog.new("Spot")
spot.appointment_reminder #=> "Time to make an appointment for Spot"
Class Methods
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.
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 class 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.
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'.
class Dog
def self.all
end
# technically can also be written
def Dog.all
end
end
Final Thoughts
Let me know:
- What are you confused about with object orientation?
- Are there concepts about classes in Ruby that I didn't go over that could use some clarification?
- What is your favorite part about OOP (object-oriented programming)?
- As always, any corrections or critiques?
Happy coding!
Top comments (0)