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:

Numbers interacting inside a program, by adding together

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 String class: '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].average feels far more declarative than average([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".shoutify should 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 Dog class.

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 observe with 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 nil in 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 name colour.
  • In line 2, we set up a new name, @colour, and assign it to the colour parameter of the colour= 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 Person class with some methods.
  • assign a new instance of Person to the variable woman.
  • Set the property @my_name on the Person instance referenced by woman to a new string, "Yasmin".
  • Call the introduce method on the Person instance referenced by woman, which returns a string, "Hello, I'm Yasmin".

Look closer at the method give_me_a_name. We can call the parameter name whatever 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 initialize method, even if you don't write it. That's why new still worked for our Dog, Person, and Robot classes 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 scores in our average method
  • Instance variables, like @name, @colour and @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:

  • def
  • class

We 'open' something.

  • def opens a method, so we can define a procedure inside.
  • class opens 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 Plane class 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.

results matching ""

    No results matching ""