Classes & Instances
What are you going to learn?
- Define classes
- Understand the difference between a class and instances
- Add methods to classes to provide behavior
- Understand how to create
set
andget
methods in classes
What is a Class?
A class, just like any other object-oriented programming language is used to model to things,state and behavior.
- State is what a something is. If for example we were to describe a person, we can model it with some attributes, like
name
,birthdate
,last_name
. - Behavior is what the person is able to do, such as
run
,eat
,swim
. These would be translated as methods.
If you recall from other lessons, an instance is a representation of a class. We have been working with this since the beginning, for example when we
create an empty array, what is actually happening behind scenes is Array.new
.
Another way to think of a class is like a blueprint of a plane. This describes how the plane is supposed to be build. An instance would be the actual plane, and you can have many planes based on that blueprint.
Defining a class
This is how you define an empty class in Ruby:
class YouClassName end
Keep in mind a couple of things here:
- All class names in Ruby are written in camel case
- The definition starts with the
class
word - They are commonly created in separated files, with the name of the class in snake case.
Now that you have defined your class, you can strat using it, even though nothing else has been added. For example:
class Pokemon end Pokemon.new #=> #<Pokemon:0x0000555fff229f98>
As you can see, anytime you want to create an instance of a pokemon, just use the class name, with the new
method.
Class Constructor
Now that you know how to define a new class, it is time to model a state through an initialize
method. The initialize
method is part of the object creation cycle
and it allows you to set initial values for the class. In other programming languages, this is called a "constructor".
Through out this lesson, we will be working with a Pokemon
class, so buckle up trainers. To define a class with a constructor
class Pokemon def initialize(name, trainer) @name = name @trainer = trainer end end Pokemon.new("Pikachu", "Ash Ketchum") #=> #<Pokemon:0x00005560ecb20a00 @name="Pikachu", @trainer="Ash Ketchum">
There are a couple of thins happening here, first we added the initialize
method along with two arguments, name
and trainer
. When working with classes, this are also
known as attributes, which will set an initial state for any instance.
The @
before each variable indicates that is an attribute or instance variable. You tipically want to add this on the constructor, as we want those attributes to persist
throughout the object's lifetime.
Remember that the order of the arguments is important, so if we pass an empty string as the name
, the pokemon name would be that.
Passing arguments to the initialize
method is a very common pattern, but this is not the only way to go, sometimes you may want to set default values to the class, let's
say every pokemon starts at level
1, so we may need to add that instance variable with a default value.
class Pokemon def initialize(name, trainer) @name = name @trainer = trainer @level = 1 end end Pokemon.new("Pikachu", "Ash Ketchum") #=> #<Pokemon:0x00005560ecb20a00 @name="Pikachu", @trainer="Ash Ketchum", @level=1>
Accessing Attributes
Now that we have a pokemon instance, you may want to access its name. We certainly can do that through the dot-notation
syntax.
pokemon = Pokemon.new("Pikachu", "Ash Ketchum") pokemon.name
If you ran the code above, you probably encounter this error:
NoMethodError (undefined method `name' for #<Pokemon:0x00005560ecb9fa80>)
This happens because the instance variables are not accessible by the instance, in other words, they are private and only accessibble from within the class. This is really easy to achieve:
class Pokemon def initialize(name, trainer) @name = name @trainer = trainer @level = 1 end def name @name end end pokemon = Pokemon.new("Pikachu", "Ash Ketchum") pokemon.name #=> "Pikachu"
As you can see, we enable the access for the @name
instance variable through a method called name
, but it can actually be anything we want. This way of reading
instance variables follows the same pattern across class definitions. Ruby hates to repeat code, and we can clean our class like so:
class Pokemon attr_reader :name, :trainer, :level def initialize(name, trainer) @name = name @trainer = trainer @level = 1 end end pokemon = Pokemon.new("Pikachu", "Ash Ketchum") pokemon.name #=> "Pikachu"
attr_reader
is a class method provided by Ruby, and it helps you create those access methods for the instance variables. Take this into consideration:
- There is no limit on how many arguments the
attr_reader
can receive, in this case,name
,trainer
,level
- If you plan to access the instance variables, the method names, should match them.
There is also other ways to access or even modify instance variables from within the class, for example to increase the pokemon level:
class Pokemon #... def increase_level @level += 1 end end pokemon = Pokemon.new("Pikachu", "Ash Ketchum") pokemon.level #=> 1 pokemon.increase_level pokemon.level #=> 2
But what if you want to change the trainer name for another one, let's say from "Ash Ketchum"
to "Gary Oak"
. One way to do it, would be:
class Pokemon #... def change_trainer(trainer) @trainer = trainer end end pokemon = Pokemon.new("Pikachu", "Ash Ketchum") pokemon.change_trainer("Gary Oak") pokemon.trainer #=> "Gary Oak"
Even though this would make sense and it actually works, when you want to change an instance variable value, the pattern is called a setter:
class Pokemon #... def trainer=(trainer) @trainer = trainer end end pokemon = Pokemon.new("Pikachu", "Ash Ketchum") pokemon.trainer = "Gary Oak"
As you may already been thinking, Ruby offers a solution for this, the same as readers
:
class Pokemon attr_reader :name, :trainer, :level attr_writer :level #... end pokemon = Pokemon.new("Pikachu", "Ash Ketchum") pokemon.trainer = "Gary Oak"
There is also another way to add both of type of patterns with a single method, such as attr_writer
and attr_reader
:
class Pokemon attr_accessor :name, :trainer, :level #... end pokemon = Pokemon.new("Pikachu", "Ash Ketchum") pokemon.trainer = "Gary Oak"
The attr_accessor
would create both methods, the reader and writer. This is the preferred way in Ruby to create both patterns. You can read a bit
more on this here.
Adding behavior & self
In order to add behavior to a class, as we stated before, is achievable through methods. In Ruby we have two types of methods that can be defined within a class.
- Instance methods are the ones that work on instances of the class
- Class methods are the ones, that don't need an instance to work, such as
attr_reader
for example. This is not a very common pattern.
class Pokemon attr_reader :name def initialize(name) @name = name end # This is an instance method def description "#{Pokemon.description} named #{@name}" end # The keyword 'self' refers to the class, in this case Pokemon def self.description "A Pokemon" end end pokemon = Pokemon.new("Pikachu") pokemon.description #=> "A Pokemon named Pikachu" Pokemon.description "A Pokemon"
As you can see, we have two methods named the "same", in this case self.description
and description
. But the difference here is the self
keyword prefixed on the method. This
tells Ruby that this method acts at the class level, not te instance. Do not worry to much about this, it is just important you identify this.
self
can also be used at the instance level:
class Pokemon attr_reader :name, :level def initialize(name) @name = name @level = 1 end def who_am_i puts self end def move if self.legs? walk else self.fly end end def walk end def fly end def legs? true end end
You can use the keyword self
to refer to the instance, so for example if you instantiated a Pokemon, you can either use self or not inside the instance
method, and it will work. In other programming languages, this is known as this
.
You can actually use the methods created by the attr_reader
inside any other method by using the self
keyword or the instance variable. If you want to read more this,
here is a good article.
Working with multiple classes
As our code grows, it is inevitable to interact with multiple classes, as well as the communication between them. Maybe is the need to have more layers of abstraction, better test our code, follow a design principle or any othe reason.
Take for example the idea of having a Trainer
class, because maybe just working with a name is just not enough:
class Trainer attr_reader :pokemons, :name def initialize(name) @name = name @pokemons = [] end def catch(pokemon) @pokemons << pokemon end end
This way we are not longer working with a bare string, but a custom made Trainer
class:
trainer = Trainer.new("Ash Ketchum") pokemon = Pokemon.new("Pikachu", trainer) #read the trainer name pokemon.trainer.name #=> "Ash Ketchum" #to catch a pokemon trainer.catch(pokemon) trainer.pokemons #=> [#<Pokemon:0x00005560ecb20a00 @name="Pikachu", @trainer="Ash Ketchum", @level=1>]
This is a super simple example on how to work with two classes, but there are some techniques to identify layers of abtractions or responsibilities. For now you don't have to worry, we will catch up in further lessons.
Class Overwrite
There is a powerful feature in Ruby, that allows you to overwrite methods from an existing class. This has to handle with care, as it may have unexpected side effects.
class Pokemon attr_reader :name, :level attr_accessor :trainer def initialize(name, trainer) @name = name @trainer = trainer @level = 1 end def name "This overrides the method name created by attr_accessor or any other" end end pokemon = Pokemon.new("Pikachu", trainer) pokemon.name #=> "This overrides the method name created by attr_accessor or any other"
The ability to do this is really powerful, as you can actually open standard classes and override a particular method, for example:
class String def upcase "No Upcase support!" end end puts "Hello World".upcase #=> "No Upcase suppoort!"
Exercises
Remember we have provided a repository with a bunch of exercises for you to complete. You can find it here
You can finde them under /ruby-exercises/Module1/classes-and-instances
.