YOU HAVE to KNOW CLASS
The concept of class in Ruby appears simple and straightforward, but when newcomers attempt to put the information into action, the code quite often fails to produce the desired result. So, I’ll explain class in a very simple relatable way, but be forewarned, you’ll probably be back again, confused by the fact that you actually missed something during your previous time through.
Object Oriented Programming is an attempt to use code to recreate things from the real world. So how is that done? With class.
What’s class?
Think of a class as a blueprint, template, or factory for making instance objects. Objects like a house, or a book, or a car. I, however, consider class more like a mother because mothers are the ultimate creators. A mother can create a child or many children. Each child will have things in common, eyes, hands, legs, bellybutton, etc. However, they each will have a name unique to them. But I’m getting ahead of myself so let me slow down a bit.
To make this analogy work, instead of using the assistance of a father, the mother will create these people (instance objects) all on her own. And since she’ll be making more than one gender, let’s give her the title of person maker or person for short. Now, let’s take a look at what she looks like as a class:
Not very impressive visually but, I can already create a person with just this bit of code:
Person.new
=> #<Person:0x00007fa5b125c4a8>
Look at that! A brand new person! But wait, this person exists, however, all they have is a unique number to their storage space on my computer. I don’t know what gender this person is, or what their name is, or what they can do. We refer to these things as attributes and behaviors. Let’s create another person but this time write a method that will show their gender attribute. In order to do this, I’ll save the new person in a variable (x):
x = Person.new
=> #<Person:0x00007fcd479fbe30>
x.gender
=> “female”
Great! My gender method shows that my person’s gender attribute is female. This kind of method is known as a reader or a getter because it reveals the attribute.
So now, I can only make females but I want the ability to make more than one gender. To do this, I need an additional method that will allow me to assign gender instead of just hardcoding it. This kind of method is known as a writer or a setter because it allows you to assign or re-assign an attribute. Let’s try making two people with different genders:
x = Person.new
=> #<Person:0x00007ff24724bf80>
x.gender = “female”
=> “female”
x.gender
=> NoMethodError: undefined method `gender’ for #<Person:0x00007ff24724bf80>
Hold up! I created a new person, then I used my writer method (gender=) to assign female to my person, but now when I try to see their gender, I get an error message. That’s because I still need my reader method (gender) to reveal or read or see the gender. Notice the NoMethodError states that I no longer have my `gender’ method (inside of my Person class). Note the difference between the writer (gender=) and reader (gender) methods in my Person class below:
Okay, let’s try it now:
x = Person.new
=> #<Person:0x00007ff24724bf80>
x.gender = “female”
=> “female”
x.gender
=> SystemStackError: stack level too deep
from `gender’
Another error message!? But this is a different error, SystemStackError. I was able to create a new person and assign my person a gender of female. But this time, when my `gender’ method was called on to reveal the gender, I got a complaint on `gender’.
This is a scope problem. I’m using a local variable inside of each of my methods. Local variables in separate methods don’t know about each other! So when I assigned “female” in my writer (gender=) method, my reader (gender) method had no clue that that had happened. So when it came time to reveal, it had nothing to reveal so it threw an error.
Let’s fix it so my methods are aware of the changes to the gender variable by assigning it to an instance variable. How? Like so; @gender = gender. Instance variables can be seen by all the methods inside of its class. Let’s try this again, creating 2 people with different genders:
x = Person.new
=> #<Person:0x00007fd6f11332e0>
x.gender = “female”
=> “female”
x.female
=> “female”
y = Person.new
=> #<Person:0x00007fd6f4b63320>
y.gender = “male”
=> “male”
y.gender
=> “male”
Finally! Two people with different genders. But I’m still not satisfied. Currently, I have to assign a gender to a person AFTER they’ve already been created. I want to reflect the real world and create people who have their gender assigned at creation. And I can do that with the initialize method.
The initialize method is a special method that allows me to assign attributes to my people as soon as they’re created. Let’s initialize the gender attribute (remember to assign it to an instance variable so that all other methods will know what’s happening):
x = Person.new
=> ArgumentError: wrong number of arguments (given 0, expected 1)
from `initialize’
Another error! This error is complaining about the number of arguments. Now that I’m initializing each new person with a gender parameter, I need to provide a gender argument when I create a person, like so:
x = Person.new(“female”)
=> #<Person:0x00007fb4fb376128 @gender=”female”>
x.gender
=> “female”
y = Person.new(“male”)
=>#<Person:0x00007fb4fb3bed38 @gender=”male”>
y.gender
=> “male”
Success! Both genders had a gender upon creation and my reader method was able to confirm that.
Let’s recap, because of class, I can create as many people as I like. Because of my initialize method, I can assign attributes when a person is created. Because of instance variables (@gender) and my reader method, I can see that attribute. Now my writer method is no longer needed, unless I want to change the gender. To reflect the real world, I’ll take away a person’s ability to change their gender by deleting the writer method. I’ll still be able to initialize each person with a gender, I just won’t be able to change it once it’s initialized.
Let’s initialize another attribute that will keep it’s writer method so it can be changed; name. Now we’ll have to create new people with both name and gender arguments:
x = Person.new(“Terry”, “female”)
=> #<Person:0x00007fc6e89683c0 @gender=”female”, @name=”Terry”>
x.name
=> “Terry”
x.gender
=> “female”
y = Person.new(“Jules”, “male”)
=> #<Person:0x00007fc6e4189978 @gender=”male”, @name=”Jules”>
y.name
=> “Jules”
y.gender
=> “male”
Great! Both people are created with a name and a gender which the reader methods (name and gender) reveal. Let’s see what happens when we try to change (re-assign) Terry’s name and gender.
x.name = “Chris”
=> “Chris”
x.name
=> “Chris”
x.gender = “male”
=> NoMethodError: undefined method `gender=’ for #<Person:0x00007fc6e89683c0 @name=”Chris”, @gender=”female”>
x.gender
=> “female”
The name reader reveals that the name Terry was changed to Chris. However, the gender change was not successful because there is no longer a gender writer (gender=) method. So Terry, I mean Chris, still has a female gender.
In my Person class, there’s a lot of repetitive code and as programmers, we strive for DRY (Don’t Repeat Yourself) code. So let’s refactor the code so that it does the same thing but without all the code repetition; cue attr_reader, attr_writer, and attr_accessor.
These 3 attr’s are forms of metaprogramming (code that writes code). Logically, it’s pretty apparent that an attr_reader writes (replaces) a reader method and an attr_writer writes (replaces) a writer method. The attr_accessor writes (replaces) both! Watch and be amazed:
Voilà! This class does exactly the same thing as the previous class. The 2 attr lines add 3 methods. The attr_accessor writes 2 methods for name (reader and writer) and the attr_reader adds only the reader method for gender (the writer method was dropped so a person can’t change their gender).
Everything we’ve covered so far has been related to attributes and not behavior. So let’s give our people some action! How about the miracle of speech? Yes, let’s give each person the ability to say their own name. Try to figure out how self works in the greeting method below:
x.greeting
=> “Hello, I’m Chris!”
y.greeting
=> “Hello, I’m Jules!”
Did you figure it out? If you didn’t, no worries. That class is doing the most! Before I clarify self, let me point out that our people now have behavior. Each person can give a greeting in which they give their own name. We can add more behavior methods like walking, cooking, sleeping, working, playing, etc., so that our people can live active productive lives!
Now, about self, it refers to the instance object that called the method. For example, when the x person instance object called the greeting method with x.greeting, “Hello, I’m Chris!” resulted because the name reader method was called on it(self) revealing Chris as its name.
Then when the person named Jules (y) called the greeting method with y.greeting, the method produced the greeting “Hello, I’m Jules!” because now self refers to the person (instance object) that called it this time, and that self’s name (self.name) is Jules.
I know, it’s a lot. And there’s more! But that’s all I’m giving in this blog post. And yes, real world people are way more complex than how I’ve represented them to be here. But this was about using a simple construct to reveal the complex inner workings of class. Now you see why you’ll be back, again, and again. It’s deep, very deep. So don’t be shy, jump in as often as you need to, because, YOU HAVE to KNOW CLASS!