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
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,
lars, I could say
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 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
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_writer for the same attribute is the same thing as using
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:
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
# 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.