Writing your own Classes
We've been spending a lot of time writing complex procedures. We've spent a lot of time telling program objects how to get to the answer:
# set up some scores
scores = [19, 17, 12]
# make an accumulator
accumulator = 0
# start a loop
scores.each do |score|
# accumulate each score
accumulator += score
end
# divide the accumulator, as a float, by the number of scores
accumulator.to_f / scores.length
- Telling an object how to get to the answer might look like: 'Create an accumulator. Add the scores to it. Then divide them.' This is called imperative programming – literally, 'ordering the computer to do things'.
In Chapter 9, we started seeing how we could take these imperative procedures and hide them behind method names. This means that our code looks more like telling an object what we want, by interacting with abstractions:
# average some scores, please
average(scores)
- Telling an object what we want might look like: 'average these scores.' This is called declarative programming – literally, 'saying what we would like to happen'.
When we're writing complex procedures, we're often working in a very imperative world. We're handling things like control flow with conditionals and loops, working with simple objects like arrays, strings, hashes, and integers. It's a tough world, and it's quite unfriendly to non-programmers (and unfriendly to programmers who didn't write the procedure themselves).
Therefore, as programmers, we try to abstract our work to a declarative style wherever possible. This is why, back in Chapter 1, we were first introduced to the 'program world' because it's a declarative world, where you can ask 1 if it's an integer:
1.integer?
And it'll just tell you the answer. Sure, there's probably some imperative procedure going on under the hood. In fact, there definitely is. But we don't have to know about it. That frees us up to think about more high-level program concerns, like how to meet a particular requirement.
These are the kinds of worlds we strive to create as programmers. One very popular route to doing so is to use Object-Oriented Programming (OOP). We've met a bunch of this already, but here's a quick refresher:
# String is a class – an object in the program world.
# When we send the message 'new' to it, the class figures out how to make an instance
# my_string is an instance of the String class. It's also an object in the program world: a new one
my_string = String.new("Hello World")
# We can tell my_string to do things, declaratively
# Here, we'll tell my_string to return its text in uppercase
# We don't really have to care about how the computer makes this happen
# (maybe it iterates through the list of characters and upcases each one?)
# All we care about is that it's easy to do
my_string.upcase
In this chapter, we'll learn how to move from an imperative to a declarative world, by:
- defining new methods on object interfaces, then
- defining whole new objects.
Back to the program world
Remember our view of the program world:

In particular, remember that procedures are running inside methods. We learned how to write methods in Chapter 9 – but the methods we defined weren't obviously part of any given object's interface. So where have they been living?
Global scope
In Chapter 3, we learned that whenever you send a message to an object without specifying the object, that message is sent to the main program object:
+(3)
So, for the past few chapters we've been defining all of our procedures on the main program object. When we write:
players = [
{ :name => "Alice", :sport => "tennis" },
{ :name => "Simo", :sport => "squash" },
{ :name => "Katerina", :sport => "tennis" },
{ :name => "Alex", :sport => "football" }
]
players_by_sport = {}
players.each do |player|
sport = player[:sport]
if players_by_sport[sport] == nil
players_by_sport[sport] = []
end
players_by_sport[sport].push(player[:name])
end
players_by_sport
...we're asking the main program object to do all this work. We're instructing the universe directly.
In Chapter 9, when we moved the averaging procedure into the method named average:
def average(scores)
scores_accumulator = 0
scores.each do |score|
scores_accumulator += score
end
scores_accumulator.to_f / scores.length
end
We defined this average method on the main object.
In Ruby, there are no 'objectless methods'.
Defining methods on objects
Remember that the main program object is an instance of the Object class. Here's how we'd define the average method on the main object formally:
class Object
def average(scores)
scores_accumulator = 0
scores.each do |score|
scores_accumulator += score
end
scores_accumulator.to_f / scores.length
end
end
average([1, 2, 3])
Notice that we're opening up the class to fiddle with its insides. We're essentially augmenting the gods so they can create slightly different kinds of things - giving the gods new powers, if you will.
The above is simply a more formal way of writing the method named average because, by default, methods are defined on the main object.
How about if we wanted to define a method on another object? Say, something that would allow us to do:
> my_object = "A string!"
> my_object.say_hi
=> "Hi there!"
That is, when we send the message say_hi to a string, it should return "Hi there!".
say_hi is not, by default, part of the instance methods available to strings. So, we have to define it, just like we did the average method on Object instances:
class String
def say_hi
"Hi there!"
end
end
my_object = "A string!"
my_object.say_hi
Again, we're opening up the String class to fiddle with the kinds of objects it can create. In this case, we're telling the
Stringclass: 'when you create instances, make sure they have this method too'.
- Define another method on the string above, called
say_hi_to. It should take one argument, a name. It should return "Hi, #{name}!"
Using object state in methods
Let's do something useful with this information. Our average method would be even more ace if we could just go:
[1, 2, 3].average
[1, 2, 3].averagefeels far more declarative thanaverage([1, 2, 3]). It uses the familiar dot syntax, too.
To do this, we need to define a new method on Array instances:
class Array
def average
# Somehow, do the averaging in here
end
end
But we have a problem. In order to average an array of number elements, we need access to those elements. Instances of the Array class know about their own elements, but how can we access them?
Enter the Ruby keyword self. self refers to the 'current object we're inside'. So, when we're messing around defining methods on Array instances, self refers to the instance.
class Array
def average
return self
end
end
[1, 2, 3].average
Notice that we get the array instance itself back when we call
self.
We can use self to access the array instance's own elements, and average them:
class Array
def average
accumulator = 0
self.each do |element|
accumulator += element
end
accumulator.to_f / self.length
end
end
[1, 2, 3].average
- Define a method on string instances, called
shoutify. This method should return the string text in uppercase, with an extra exclamation mark on the end. In other words,"hello world".shoutifyshould return"HELLO WORLD!".
Defining your own classes
We've just seen how to use the class keyword to 're-open' existing classes and mess around with the instances they make. We can also use this class keyword to define entirely new classes – new gods – each of which can create whole new instances.
We're going to try and build the following program:
fido = Dog.new
fido.bark
# We should see "Woof!"
We can actually let our error messages tell us what to do at each stage. This is a helpful way to figure out what we need to do next, step-by-step. Let's try running our program as-is:
fido = Dog.new
fido.bark
OK, we have an error: uninitialized constant Dog. What's this telling us? It's saying "I couldn't find a name 'Dog'". Let's make one!
Dog = "a dog"
fido = Dog.new
fido.bark
I did the simplest thing, and we have a new, explanative error. Apparently the String instance I made, "a dog", doesn't respond to new. It sounds like we need Dog to be some kind of object that responds to new.
What objects respond to new? Classes! It turns out that Ruby has a class called Class. It's the god of classes! Instances of the Class class are, well, classes. We can send new to it, and get a new class in response:
Dog = Class.new
fido = Dog.new
fido.bark
We've just defined a new class, Dog. However, we haven't added any methods to it. Let's do that, using the technique we used before with Object, Array, and String:
Dog = Class.new
class Dog
def bark
return "Woof"
end
end
fido = Dog.new
fido.bark
And that's how we define new kinds of object, and new methods on those objects.
We've seen so far that the class keyword is shorthand for "open up this class, and do things with it". It's actually a bit cleverer than this. If the class given to class doesn't exist, it will be made on the fly. That means we can tighten the example above down to:
class Dog
def bark
return "Woof"
end
end
fido = Dog.new
fido.bark
This is a more common way to define new kinds of object (and methods on them) in Ruby.
- Add another method to the
Dogclass.
Object properties
Remember how 1 knows about its value (1)? And how instances of String know about some text? These things are both examples of object state. In fact, a string's internal characters are referred to as properties of the string.
Let's upgrade our Dog class. We'd love to be able to do something like this:
fido = Dog.new
fido.colour = "brown"
fido.observe
# returns "You see a brown dog."
Moreover, we'd love to be able to do something like this:
fido = Dog.new
chelsea = Dog.new
fido.colour = "brown"
chelsea.colour = "white"
fido.observe
# returns "You see a brown dog."
chelsea.observe
# returns "You see a white dog."
fido.observe
# returns "You see a brown dog."
That is, we want instances of the Dog class – dog objects – to:
- Respond to a method
colour =by setting a property on themselves - Respond to a method
observewith a string - Vary that string according to the colour property
- Remember what colour they are, so they always return 'their' string
We already know how to define new methods on Dog, so let's allow our errors to guide us:
class Dog
def bark
return "Woof!"
end
end
fido = Dog.new
fido.colour = "brown"
fido.observe
We get a useful error: colour=: undefined method 'colour=' for #<Dog:0x1b210>. (Your memory address, '0x1b210' bit, will probably differ from mine.)
In Chapter 5, we found out that the
0x1b210(or whatever number you have) is a hexadecimal number referencing the memory address of the dog instance.
We can fix this by simply doing as we're told, defining a method colour= on dog instances:
class Dog
def bark
return "Woof!"
end
def colour=
end
end
fido = Dog.new
fido.colour = "brown"
fido.observe
Now we have a new error: undefined method 'observe' for #<Dog:0x1b210>. We can fix this in the same way:
class Dog
def bark
return "Woof!"
end
def colour=
end
def observe
end
end
fido = Dog.new
fido.colour = "brown"
fido.observe
Notice that we're not doing anything with the methods, yet. We're just defining them to get rid of the errors.
OK! Now we don't see any more errors. But – at the moment, observe just returns nil.
Remember, empty method procedures just return
nilin Ruby.
Let's fix that with the string we want:
class Dog
def bark
return "Woof!"
end
def colour=
end
def observe
return "You see a brown dog."
end
end
fido = Dog.new
fido.colour = "brown"
fido.observe
Awesome! Does this work for chelsea, the white dog, too?
class Dog
def bark
return "Woof!"
end
def colour=
end
def observe
return "You see a brown dog."
end
end
chelsea = Dog.new
chelsea.colour = "white"
chelsea.observe
Ah, the program still suggests that Chelsea is a brown dog. We need to make the program vary according to the colour we provide. We need some way for the Dog instance to remember the colour we give it.
Here's how Ruby objects can be told to remember values:
class Dog
def bark
return "Woof!"
end
def colour=(colour)
@colour = colour
end
def observe
return "You see a " + @colour + " dog."
end
end
chelsea = Dog.new
chelsea.colour = "white"
chelsea.observe
The above code will work for both Fido and Chelsea. That's because we're saving the colour property, passed as an argument to the colour= method, to the object state. To save things to the object state, we use an instance variable definied with the @ symbol.
Let's look more closely at what the colour= method is doing:
def colour=(colour)
@colour = colour
end
- In line 1, we define a new method
colour=. It takes one argument, which we namecolour. - In line 2, we set up a new name,
@colour, and assign it to thecolourparameter of thecolour=method. - In line 3, we close the
colour=method.
Let's give another example of this.
class Person
def give_a_name(name)
@my_name = name
end
def introduce
return "Hello, I'm " + @my_name
end
end
woman = Person.new
woman.give_a_name("Yasmin")
woman.introduce
In the example above, we:
- define a
Personclass with some methods. - assign a new instance of
Personto the variablewoman. - Set the property
@my_nameon thePersoninstance referenced bywomanto a new string,"Yasmin". - Call the
introducemethod on thePersoninstance referenced bywoman, which returns a string,"Hello, I'm Yasmin".
Look closer at the method
give_me_a_name. We can call the parameternamewhatever we want, so long as we reference that same parameter in the method procedure.
The following code will do exactly the same thing but I've changed some of the variable and method names around to make a point:
class Person
def call_me(nickname)
@name_i_would_like_to_be_known_by = nickname
end
def introduce
return "Hello, I'm " + @name_i_would_like_to_be_known_by
end
end
woman = Person.new
woman.call_me("Yasmin")
woman.introduce
Since we're free to name variables and methods as we wish, the most sensible implementation of Person would use nice, terse names:
class Person
def name=(name)
@name = name
end
def introduce
return "Hello, I'm " + @name
end
end
woman = Person.new
woman.name=("Yasmin")
woman.introduce
Using Ruby's syntactic sugar, we can replace the penultimate line with
woman.name = "Yasmin".
Methods that set object state – like name= above – are called setters. We've seen that setter methods can be called anything you like (call_me is a setter, give_me_a_name is a setter, and name= is a setter). The important thing is that they set object state using an instance variable.
Mutating object state
We can change object state, too. Let's build the following program:
robot = Robot.new
robot.legs = 4
robot.add_leg
robot.add_leg
robot.walk
# returns "I'm walking on my 6 legs!"
We already know how to build out the first (legs=) and the last (walk) parts:
class Robot
def legs=(number_of_legs)
@number_of_legs = number_of_legs
end
def walk
return "I'm walking on my " + @number_of_legs.to_s + " legs!"
end
end
robot = Robot.new
robot.legs = 4
robot.walk
How does add_leg work, though? We're changing the @legs property of the Robot instance.
Remember +=?
class Robot
def legs=(number_of_legs)
@number_of_legs = number_of_legs
end
def add_leg
@number_of_legs += 1
end
def walk
return "I'm walking on my " + @number_of_legs.to_s + " legs!"
end
end
robot = Robot.new
robot.legs = 4
robot.add_leg
robot.add_leg
robot.walk
Using initialize to set up instances
Imagine if String instances worked like this:
my_string = String.new
string.text = "Some text"
How annoying that would be! And yet, thanks to our obsession with setter methods above, that's exactly what we're doing with our Dog, Person, and Robot classes. Wouldn't it be better to write:
woman = Person.new("Yasmin")
Just like we write my_string = String.new("Some text")?
We can! Thanks to Ruby initializers.
In Ruby, whenever we call new on a class, that class builds an object and then runs the method initialize on it. If we want to store information about an object as part of that object's state – as a property on the object – we need to interfere with this initialize method.
Ruby automatically defines the
initializemethod, even if you don't write it. That's whynewstill worked for ourDog,Person, andRobotclasses before.
Every argument to the new function is passed to the initialize function. Therefore, if we want to send the string "brown" or "white" to be stored on a dog as soon as we call new, we need to set an instance variable in the initializer.
class Dog
def initialize(colour)
@colour = colour
end
def observe
"You see a " + @colour + " dog."
end
end
fido = Dog.new("brown")
fido.observe
No setter method needed! Let's do that again, with our Person class:
class Person
def initialize(name)
@name = name
end
def introduce
return "Hello, I'm " + @name
end
end
woman = Person.new("Yasmin")
woman.introduce
And once more, with our Robot class:
class Robot
def initialize(number_of_legs)
@number_of_legs = number_of_legs
end
def add_leg
@number_of_legs += 1
end
def walk
return "I'm walking on my " + @number_of_legs.to_s + " legs!"
end
end
robot = Robot.new(4)
robot.add_leg
robot.add_leg
robot.walk
By using multiple parameters with initialize, we can provide multiple pieces of information to an object at once and assign them in order:
class Person
def initialize(name, age)
@name = name
@age = age
end
def introduce
"Hello, I'm " + @name + ", and I'm " + @age.to_s + " years old."
end
def get_older
@age += 1
end
end
peter = Person.new("Peter", 17)
peter.get_older
peter.introduce
Different kinds of variables
All variables are references to objects in Ruby because everything is an object, from 1, to "Hello world", to the Dog class.
So far, we've met a few different kinds of variables:
- Regular variables, like
my_string = "Hello World - Constants, like
ONE = 1 - Variables inside methods, which get their names from parameters, like
scoresin ouraveragemethod - Instance variables, like
@name,@colourand@legs.
What's the difference between these kinds of variables? The answer is, broadly speaking, which objects can see them.
What can see an instance variable?
Instance variables are the simplest: they can only be seen by the object that they're 'inside':
class Dog
def initialize(colour)
@colour = colour
end
# We can use @colour in any methods defined on Dog instances
# Such as this one
def observe
"You see a " + @colour + " dog."
end
end
# We can't see @colour from outside instances of the Dog class
# Here, we're trying to access @colour from the main object
# It can't see the instance variable @colour
@colour
If you try to reference an instance variable that an object can't see, you'll get
nil.
What can see a local variable?
Regular variables are normally referred to as local variables. Here, we're in imperative-land - telling the computer what to do, line-by-line.
If a line defining that local variable has already been executed, that local variable is available to anything that wants it.
# Define a local variable in the main program object
my_variable = 1
# We can access the local variable here because the line above was executed before this one
my_variable
This is the case even if the local variable was defined in a conditional branch that got executed:
if true
# This conditional branch will be executed
my_variable = 1
end
my_variable
One gotcha, strange things happen if you define variables in branches that don't get executed:
if false
my_variable = 1
end
my_variable
The program can still read the name, but the value is set to nil.
There's one really tricky thing about local variables, and it has to do with methods. Here it is:
def my_method
my_variable = 1
end
my_variable
Why can't the main object see the my_variable variable, even though it was defined in the lines above? The answer that most makes sense: my_method didn't get executed yet. We only declared it, but we didn't call the method.
So we can solve it like this, right?
# define the method
def my_method
my_variable = 1
end
# run the method, executing the procedure that defines my_variable
my_method
my_variable
Wrong. For some reason, even though the procedure my_variable = 1 (inside the method my_method) has now been executed the main object still can't see my_variable. Why is this?
Scope
Every time we write one of the following:
defclass
We 'open' something.
defopens a method, so we can define a procedure inside.classopens a new class, so we can define methods inside.
The keyword end then 'closes' that something you just opened.
def average
# we opened the average method, so we can define procedures in it
end
class Dog
# we opened the Dog class, so we can define methods in it
end
Variables that we define inside a def...end or class...end cannot be seen outside of them:
def average
accumulator = 0
end
class Dog
some_variable = 1
end
accumulator
some_variable
The area of a program in which a variable can be read is called the variable scope. def and class are known as scope gates. When the program runs a line containing def or class, it enters a new scope. Variables defined inside this scope cannot be read outside of the scope gate. Variables defined outside of the scope gate cannot be read inside it.
my_variable = 1
def my_method
# I'm in a new scope gate! I can't read my_variables
my_variable + my_variable
end
my_method
Confused by the word 'scope'? Think of the scope on top of a sniper-rifle: when you look down it, you can only see a part of the world: the part of the world you can shoot.
One more gotcha, even though files containing Ruby code are all loaded into the program straight away, local variables cannot be read from outside of the file they're declared in. So files act as scope gates, too.
Using scope to understand method parameters
Scope is especially helpful when it comes to understanding method parameters. For each parameter, methods will define a local variable within their scope. The name of that variable will be set to the name of the parameter:
age = 22
def age_reporter(number)
# Whatever we pass as an argument to age_reporter will be assigned to a local variable named 'number'
return "Your age is " + number.to_s
end
age_reporter(age)
The same goes for parameters provided to procedures inside each loops:
people = ["Steve", "Yasmin", "Alex"]
people.each do |name|
# Each element of people is assigned to a local variable called 'name'
# We can then read that local variable inside this loop
name
end
# But we can't read the local variable outside the loop
name
Since def is a scope gate, we can't read these parameters outside of their message body:
name = "Sam"
def say_hi_to(person)
return "Hi, " + person
end
# We can't read this
person
This article dives a little deeper into the idea of 'scope' in Ruby.
- Scope is what parts of the program an object can 'see' at any time.
Using Objects to be more declarative
By defining our own classes, we can make our programs super-readable, almost like English. Here's an example:
heathrow = Airport.new
plane = Plane.new
heathrow.land(plane)
heathrow.hangar_report
# => "There is 1 plane in the hangar"
Also, we can stop programmers from doing things that don't fit with our model of how the program should work:
heathrow = Airport.new
heathrow.take_off(plane)
# => "Error: There are no planes to take off"
In the examples above, the object referenced by heathrow is an instance of the Airport class. Somehow, it can store planes. At the heart of the heathrow object, in its state, lives an array – an object we introduced in Chapter 7 as a way to store other objects. Around this array, heathrow defines methods which are appropriately named for the program being run – some sort of airport simulation. Here, the 'airport simulation' is known as the program domain.
Why don't we just use an array, instead of instances of this Airport class? That way, the program could work like this:
heathrow = []
plane = "Boeing 737"
heathrow.push(plane)
"There is #{ heathrow.length } plane in the hangar"
The answer is, using arrays describes the program domain less effectively. As programmers, we strive to write code that accurately reflects the program domain.
Here's how Airport 'wraps' an array:
class Airport
def initialize
@hangar = []
end
def land(plane)
@hangar.push(plane)
end
def take_off(plane)
if @hangar.length < 1
return "Error: there are no planes to take off"
end
if @hangar.include?(plane)
@hangar.delete(plane)
return plane
else
return "Error: plane not in hangar"
end
end
def hangar_report
if @hangar.length == 1
"There is 1 plane in the hangar"
else
"There are #{ @hangar.length } planes in the hangar"
end
end
end
heathrow = Airport.new
heathrow.hangar_report
Because the array is referenced only by an instance variable, nothing outside of Airport instances can read it, so we're protected from programmers trying to pop a plane from heathrow, and so on. This essentially limits the interface of the array to only methods we want to allow.
Spend some time playing with the code example above. Write a
Planeclass if you can and land it. The code here is similar to the code introduced in the first week of Makers, so don't worry if it boggles you at the moment.
Complete the mastery quiz for chapter 10
Use your mastery quizzes repository to complete the quiz for chapter 10.