Instance Variables and Methods

How to maintain state and provide functionality to class instances using variables and methods.

Scroll down...

Content

Resources

Comments

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!

Instance Variables

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.

Options Hashes

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>

Instance Methods

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).

Getters and Setters

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.

Conceptualizing Getters and Setters

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.

Bunker with methods

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

Attr_accessor

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

A Word of Caution: Privacy is Good

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.

Working with Instances

Because of your getters and setters, there are two different ways to access an instance variable from inside your class:

  1. Calling it normally using @age.
  2. Calling the getter or setter method on the instance using 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.

Code Review

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

Wrapping Up

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.



Sign up to track your progress for free

There are ( ) additional resources for this lesson. Check them out!

There are no additional resources for this lesson just yet!

Sorry, comments aren't active just yet!

Next Lesson: Class Variables and Methods