Erik Trautman logo crest shield seal

Erik Trautman

“Everything you can imagine is real.”
-- Pablo Picasso

Ruby Explained: Classes

This post will cover the basics of creating and working with classes in Ruby

When you start solving larger problems organization is key. You don't want 100 different methods and variables that do overlapping things. If you're trying to keep track of data (like a bunch of bad guys in a game you're building), you want to do so in the most organized way possible so you can recycle methods and only need to update that data in one place at a time.

Classes are here to make your life easier on that front and you'll learn about how to organize and use them here. In the process, you'll also need to understand which of their methods get inherited by who and the difference between instances of a class and the class itself.

You've written a lot of methods so far but they've generally been independent of each other. When you find that you want the same method to be run on a bunch of different objects without having to make a bunch of different if/else or case statements, you should start thinking about using a class.

We've been over storing data in hashes, but what happens when you want to treat that data like a real object and make it move? Or if you want to handle 10,000 different instances of it? When you just store your Viking's name, age, health, and strength, it just kind of sits there. What about when you want to make an army of Vikings who can do stuff like #eat, #travel, #sleep and, of course, #attack? For that, you need a slightly more complex structure to make your Viking out of, so you give it its own Viking class:

class Viking
    # put your methods and variables here
end

We've been dealing with classes since the very beginning, when it became clear that everything in Ruby is an object, whether strings, hashes, arrays or numbers, and these objects are instances of some type of class.

> 1.class
=> Integer
> [].class
=> Array

Just like being a part of Array gives [1,2,3] the #each method, you can create your own classes that have access to shared methods as well.

To be able to amass a horde of 100 Vikings, you need a way to create new ones. Each time you do that, it's called Instantiating a new Viking. You use a special ::new method to do that. You've done it many times for the Array class already:

> my_arr = Array.new
=> []

::new is a Class Method, which means that you call it on the class (Array here) and not the specific instance of that class (which would be my_arr here). It's also why we designate it with the two colons :: when talking about it here. When you call that method, it creates a new instance of that class and then runs a special method in the class called #initialize, which will set up that class to be ready for use. If you pass variables to your class when you create it with the ::new method, they will be available to the #initialize method. Your Viking class would start by looking like:

class Viking
    def initialize(name, age, health, strength)
        # set up your new viking however you want to
    end
end

Classes share their methods, but what about variables? You don't want all your Vikings to have the same strength, so we use instance variables to take care of that. 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'd usually set them up in your #initialize method:

class Viking
    def initialize(name, age, health, strength)
        @name = name
        @age = age
        @health = health
        @strength = strength
    end
end

> oleg = Viking.new("Oleg", 19, 100, 8)
=> #<Viking:0x007ffc0597bae0>

Note that the class name is always capitalized and, for multiple words, uses CamelCase (capitalized with no spaces) not the snake_case (lowercase with spaces as underscores) you've typically seen for variables.

What was that random string in #<Viking:0x007ffc0597bae0>? That's the position in the computer's memory that the viking object is stored (don't worry about it).

If you want to give your viking some actions it can do, give it some methods. Since these methods get called on an individual instance of the Viking class, they're called Instance Methods, just the same as all your old friends like #each and #sort and #max etc. We just usually don't call them "instance" methods so maybe it wasn't obvious.

class Viking
    def initialize(name, age, health, strength)
        # codecodecode
    end
    def attack(enemy)
        # code to fight
    end
end

Now, if I had two Vikings, oleg and lars, I could say > lars.attack(oleg) and it would run that method.

So now you 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. So 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:

def health
    @health
end

> oleg.health
=> 87

That was easy! 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 taking an argument:

def health=(new_health)
    @health = new_health
end

Well, you can imagine that you'll probably be writing a whole lot of those methods, so Ruby gives you a helper method called attr_accessor, which will create those getters and setters for you. Just pass it the symbols for whatever variables you want to make accessible and POOF! those methods will now exist for you to use:

class Viking
    attr_accessor :name, :age, :health, :strength
    # codecodecode
end

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.

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 similar attr_reader and attr_writer methods. They should be pretty self explanatory.

Because of your getters and setters, there are two different ways to access an instance variable from inside your class, either calling it normally using @age or calling the method on the instance using self, which we learned about previously. Before, we said it represented whatever object called a particular method. Since the original method (below it's #take_damage) is being called on an instance of the class, that instance becomes self. An example is clearer:

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

You can also call methods from within other methods, as we saw with #shout above. In that case, the self is actually optional because Ruby assumes if you just type shout("OUCH!") that you're trying to run the method #shout and Ruby will see if the method exists. That works 90% of the time, unless you've done something that overrides Ruby's assumption that you're trying to run a method, like using the = assignment operator:

...
    def sleep
        health += 1 unless health >= 99   # ! FAIL !
    end
...

Here, Ruby assumes you're trying to set up a new health variable using #health= instead of accessing the one that currently exists as @health. Just an edge case to watch out for if you start eliminating your self's.

Let's zoom away from the instance level and back to the class level for a second. Just like you've got instance variables and instance methods, you also get class variables and class methods. Class variables, denoted with TWO @@'s, are owned by the class itself so there is only one of them overall instead of one per instance.

In this example, we assume that all Vikings start with the same health, so we don't make it a parameter you can pass in:

class Viking
    @@starting_health
    def initialize(name, age, strength)
        @health = @@starting_health
        # ...other stuff
    end
end

What about class methods? You define a class method by preceding its name with self (e.g. def self.class_method) or, identically, just the name of the class (e.g. def Viking.class_method). There's a less common method that puts the line class << self ahead of your method definitions (which won't use self anymore).

There are two good times to use class methods: when you're building new instances of a class that have a bunch of known or "preset" features, and when you have some sort of utility method that should be identical across all instances and won't need to directly access instance variables.

The first case is called a factory method, and is designed to save you from having to keep passing a bunch of parameters manually to your #initialize method:

class Viking
    def initialize(name, health, age, strength)
        #... set variables
    end
    def self.create_warrior(name)
        age = rand * 20 + 15   # remember, rand gives a random 0 to 1
        health = [age * 5, 120].min
        strength = [age / 2, 10].min
        Viking.new(name, health, age, strength)  # returned
    end
end

> sten = Viking.create_warrior("Sten")
=> #<Viking:0x007ffc05a79848 @age=21.388120526202737, @name="Sten", @health=106.94060263101369, @strength=10>

It's pretty handy of IRB to list out the instance variables for you. It's almost identical to the output if you were to type olga.inspect (only the strings show up a bit differently).

The second case above is more mundane. Often, there are things you need all Vikings to "know" or be able to do:

class Viking
    ...
    def self.random_name      # useful for making new warriors!
        ["Erik","Lars","Leif"].sample
    end
    def self.silver_to_gold(silver_pieces)
        silver_pieces / 10
    end
    class << self           # The less common way
        def gold_to_silver(gold_pieces)
            gold_pieces * 10
        end
    end
end

> warrior1 = Viking.create_warrior(Viking.random_name)
=> #<Viking:0x007ffc05a745c8 @age=22.369775138257097, @name="Lars", @health=111.84887569128549, @strength=10>

Quick Basics

  • Classes are useful to use when you want to give methods to your data or have multiple instances of your data
  • Class methods have access to other class methods and class variables but don't have access to instance methods or instance variables
  • Instance methods can call other instance methods, instance variables, class methods, or class variables

If you're thinking that class variables seem pretty similar to constants, they are only similar in that all instances have access to them. If you've got something that will never, CAN never change, use a constant. If you might ever change it, stick with a class variable. At the very least, it makes your code much more legible.

We've previously learned about modules, the nice packages of methods that you can mix into classes. But if you often create a class so it can use methods, what's the difference? Basically, a class can be instantiated but a module cannot. A module will never be anything other than a library of methods. A class can be so much more -- it can hold its state (by keeping track of instance variables) and be duplicated as many times as you want. It's all about objects. If you need to instantiate something or otherwise have it exist over time, that's when you need to use a class instead of a module.

Important thought question:

If a hash (good data storage) and a module (good methods) had a love child, would it be a class (object with methods)?

Inheritance is the ability of one class to be a child of another class and therefore inherit all its characteristics, including methods and variables. We saw that early on with the demonstration of using the ::superclass method to see what a particular class inherits from, for instance the number 1 is a class FixNum which inherits from Integer which inherits from Numeric which inherits from Object which inherits from BasicObject.

> 1.class.superclass.superclass.superclass.superclass
=> BasicObject

Why do all this inheritance? To keep our code as DRY as possible. It lets us not have to repeat a bunch of methods (say, #to_s, which is implemented in the Object class) for every different subclass.

In Ruby, a class inherits from another class using the < notation. Unlike some other languages, a class can only have ONE parent.

class Viking < Person

Now Viking has access to all of Person's methods. You say that Viking Extends Person.

You've previously seen us add methods to another existing class, like we did several times with Array when you built your own implementation of #each and #map. You can use the same tecnhique to overwrite existing methods. It would cause all kinds of problems here, but we could do:

class Array
    def each
        puts "HAHA no each here!"
    end
end

> [1,2,3].each {|item| puts item }
HAHA no each here!
=> nil

If Viking extends Person, you similarly have the option to overwrite any of Person's methods. Maybe Vikings #heal twice as fast as normal people. You could write:

class Person
    MAX_HEALTH = 120
    ...
    def heal
        self.health += 1 unless self.health + 1 > MAX_HEALTH
    end
end

class Viking < Person
    ...
    def heal
        self.health = [self.health + 2, MAX_HEALTH].min
        puts "Ready for battle!"
    end
end

That's one way... but we could also do it by calling the parent's #heal method directly a couple of times using the special #super method. #super lets you call the superclass's version of your current method.

class Viking < Person
    ...
    def heal
        2.times { super }
        puts "Ready for battle!"
    end
end

You will often use that in your #initialize method when you want to use the parent's #initialize but just add a tweak or two of your own. You can pass in parameters as needed:

class Viking < Person
    def initialize(name, health, age, strength, weapon)
        super(name, health, age, strength)
        @weapon = weapon
    end
end

Again, it saves you the trouble of having to rewrite (and overwrite!) all those lines of code that were already taken care of by your parent class.


The "Ruby Explained" posts are designed to be a sort of "In-Plain-English" version of key Ruby concepts which are usually covered in other introductory texts but rarely for free and often incompletely. When I'm learning a new thing, I usually want someone to explain it to me like I'm a five year old because that's the best way to make sure nothing gets missed. This is my attempt to pass that same sentiment on to you. Let me know if there's anything I can improve.

If you're just getting interested in this stuff, check out The Odin Project for a free curriculum to learn web development.

Tags:   Ruby Explained