These free mini-courses will give you a strong foundation in web development. Track your progress and access advanced courses on HTML/CSS, Ruby and JavaScript for free inside our student portal.
Scroll down...
When you create an object by instantiating a class, that object will have its own state and functionality. Its state is determined by all the values of its instance variables, which are unique to that object. Its functionality will be determined by the instance methods that are specified in its class blueprint.
In this lesson, we'll look at how these instance variables and methods are created, accessed, and used to bring your classes to life. In the following lesson, we'll zoom out a bit and focus on the class variables and methods, which provide a more general functionality for a given class.
This stuff is bread and butter OOP, so make sure you're able to understand it. Build a conceptual model and ask for help if you need it!
We've talked about how instances of classes (ie. objects) share their methods (and will cover it in detail below), but what about the variables representing their attributes? You don't want all your Vikings to have the same strength, so we use instance variables to take care of that. That allows the Viking Sven to have a different @health
value than Oleg.
You designate an instance variable using the @variable_name
notation, and you'll be able to use it the same way for every instance of Viking but it will have a unique value for each. These instance variables are part of setting up your object's state. When your instance is destroyed, you lose access to its instance variables as well.
You will usually set up the instance variables for the first time in your initialize
method so they're ready for you right away:
class Viking
def initialize(name, age, health, strength)
@name = name
@age = age
@health = health
@strength = strength
end
end
# IRB
> oleg = Viking.new("Oleg", 19, 100, 8)
#=> #<Viking:0x007fc20b9bcec0 @name="Oleg", @age=19, @health=100, @strength=8>
So what was that random string in the Terminal output <Viking:0x007fc20b9bcec0 ...>
? That's the position in the computer's memory that the viking object is stored. We already learned how objects are really just references to places in memory, and this helps illustrate that as well.
You might get tired of remembering exactly which position the "age" variable is supposed to go in the new
method when you're instantiating the Viking. It's too easy to forget and create a Viking with strange attributes:
> oleg = Viking.new(100, "Oleg", 19, 8)
#=> #<Viking:0x007fc20b9bcec0 @name=100, @age="Oleg", @health=19, @strength=8>
# ...oops!!!
Instead of passing in four explicit variables, a common pattern is to pass in a single hash instead (where the attributes can be declared in any order) and then just unpack it inside your initialize
class. You'll see this used by all kinds of methods in Rails, not just for instantiating new classes.
class Viking
def initialize(attrs) # assume attrs is a hash
@name = attrs[:name]
@age = attrs[:age]
@health = attrs[:health]
@strength = attrs[:strength]
end
end
# IRB
> oleg = Viking.new(:name => "Oleg", :age => 19, :health => 100, :strength => 8)
#=> #<Viking:0x007fc20b9bcec0 @name="Oleg", @age=19, @health=100, @strength=8>
If you want your Viking to be able to do anything, you need to give it some methods. Since these methods get called on an individual instance of the Viking class, they're called Instance Methods.
Some examples of instance methods that you've already seen include your old friends each
and sort
and max
etc. We just usually don't call those other ones "instance" methods so maybe it wasn't obvious but they all get called on instances of Arrays.
Back to our Vikings, here we've added another instance method for attacking:
class Viking
def initialize(name, age, health, strength)
# code to initialize
end
def attack(victim)
# code to fight
end
end
Now, if I had two Vikings, oleg
and lars
, I could say > lars.attack(oleg)
.
So if we've had a couple of Vikings get in a fight, you probably want to know what oleg
's health is:
> oleg.health
#NoMethodError: undefined method 'health' for #<Viking:0x007ffc0597bae0>
Woah! The instance variables are a part of oleg
but you can't access them from outside him because it's really nobody's business but his. Ruby prevents you from viewing the instance variables of an object unless you're explicitly allowed to.
To get around this, you have to create a method specifically to get that variable, called a getter method, and just name it the same thing as the variable you want:
class Viking
def initialize(name, age, health, strength)
# code to initialize
end
def health
@health # Implicitly returned
end
def attack(victim)
# code to fight
end
end
### IRB
> oleg.health
#=> 87
That was easy! You can consider the "Getter" method to be a tunnel into the object's otherwise hidden core.
What if you decide that you want to set that variable yourself? You need to create a setter method, which is similar syntax to the getter but with an equals sign and an argument:
class Viking
def initialize(name, age, health, strength)
# code to initialize
end
def health
@health
end
def health=(new_health)
@health = new_health
end
def attack(victim)
# code to fight
end
end
### IRB
> oleg.health
#=> 87
> oleg.health = 100
#=> 100
It may feel a bit weird at first to think of health=
as a different method from health
but it is. In this case, we've again tunneled into our object but now we're actually messing around with things inside of it.
There's nothing magical at all about Getters and Setters -- they're just a pattern that's become standard practice in the OOP world to selectively expose parts of objects. They are therefore an important concept to understand.
Think of an instance of your class as a completely sealed bunker. No one from the outside can see in or make it do anything. It doesn't matter if that instance is a Viking warrior or a Dog or a blog Post -- it's sealed to the outside world until you start giving it functionality via methods.
You let the outside world interact with your class by adding instance methods. If you add a walk_forward
method to your Viking, it opens up a little window into that instance so now other people can tell it to walk_forward
(but that's it!). Each method that you give it (which isn't explicitly marked private
) opens up a tiny little interface for you to use that method on the instance.
If you want to know about the instance's state, you presumably want to access its instance variables. These are also hidden from you. You don't know your Dog's age
or your Post's body
. To give the outside world the ability to view these things, you need to expose them (open up the little window) with an instance method.
In this case, you create a publicly-available method which does nothing except return that particular instance variable. That's all a "getter" is -- just a simple method which opens up a little interface so you can access an instance variable.
def age
return @age
end
Setters are the same idea -- since the outside world isn't allowed to mess around with your Dog's instance variables, they need to do so via an instance method which opens up the window. Instead of a method which simply returns the instance variable like a "getter", this method needs to actually modify that variable.
def age=(new_age)
@age = new_age
end
Well, you can imagine that you'll probably be writing a whole lot of getters and setters, so Ruby gives you a helper method called attr_accessor
.
attr_accessor
creates those getters and setters for you. Just pass it the symbols for the variables you want to make accessible and POOF! Those getters and setters will now exist for you to use:
class Viking
attr_accessor :name, :age, :health, :strength
# codecodecode
end
# IRB
> oleg.strength
#=> 8
> oleg.age = oleg.age + 1
#=> 20
attr_accessor
isn't magical, it just uses Ruby's ability to create methods from within your script (part of "metaprogramming") to set up name
and name=(new_name)
and age
and age=(new_age)
etc.
These are identical:
# With an attr_accessor
class Dog
attr_accessor :age
end
# With explicit getter and setter
class Dog
def age
@age
end
def age=(new_age)
@age=age
end
end
You shouldn't make anything readable and certainly not writeable without a good reason. If you only want one or the other, Ruby gives you the specific attr_reader
(for getters) and attr_writer
(for setters). They should be pretty self explanatory. attr_reader
and attr_writer
for the same attribute is the same thing as using attr_accessor
.
class Dog
# the shortest way to have read/write access
attr_accessor :age
# the specific ways to grant access
attr_reader :age
attr_writer :age
# The longer equivalent of attr_reader
def age
@age
end
# The longer equivalent of attr_writer
def age=(new_age)
@age = new_age
end
It's good to only show what's absolutely necessary. That way you keep your class's interface as need-to-know as possible. This should be familiar from our discussions on creating good modular systems.
Because of your getters and setters, there are two different ways to access an instance variable from inside your class:
@age
.self
.Before, we said that self
represented whatever object called a particular method. Whenever you call an instance method inside a class, the instance becomes self
. That lets you call instance methods from within other instance methods:
class Viking
...
def take_damage(damage)
self.health -= damage
# OR we could have said @health -= damage
self.shout("OUCH!")
end
def shout(str)
puts str
end
...
end
# IRB
> oleg.take_damage(12)
OUCH!
#=> nil
Interestingly enough, the self
is actually optional because Ruby assumes if you just type shout("OUCH!")
that you're trying to run the method shout
on the self
.
# This is okay too
...
def take_damage(damage)
@health -= damage
shout("OUCH!")
end
...
When you do this, Ruby goes on a scavenger hunt to see if the shout
method or a variable called shout
exists on the self
object and then runs what it finds.
The important bits of code from this lesson
# "Getter" to return instance variable
def health
@health
end
# Create the getter the easy way
attr_reader :health
# "Setter" to set the value of an instance variable
def health=(new_health)
@health = new_health
end
# Create the setter the easy way
attr_writer :health
# Create both the easy way
attr_accessor :health
Hopefully you've got a solid understanding of how a class instance achieves its state and functionality now. Just stick with the bunker analogy -- that nothing is accessible from outside an instance except its methods -- and you'll be fine.
In the next lesson, we'll zoom out to look at class-wide behaviors. In general, most of your objects will get their state and functionality from instance variables and methods. Sometimes, though, it makes sense to offload certain generic or shared elements to the class itself by using class variables and methods. You'll learn all about that next.