Object Oriented Ruby and Inheritance (continued)
In today’s tutorial, we will be again going through OO concepts - some of which the we already introduced in the previous lesson - and build a small text adventure game to practise what we learn.
Don’t forget to commit to git regularly and also try to type out the examples as much as possible instead of copy & pasting!
If you are going through the tutorial in your own time and need any help then join the slack channel, but first read our Code of Conduct as we will not tolerate any inappropriate behavior.
How does inheritance fit in OO?
To make things a bit clearer, you must understand that every class in Ruby is a subclass of Object, even though we don’t explicitly say so. This is why methods like the to_s
, that we mentioned in the previous tutorial, or inspect
exist in all classes, because they are inherited from parent Object
class.
public_methods
In Ruby you can do some cool things, like check what methods are available on an object using public_methods
. Public methods are the methods of an object that can be called from the outside world.
class Dog
def bark
# do something
end
def walk
# go for a walk
end
private
def scratch
# scratch self
end
end
In the example above, bark
and walk
are public methods; however scratch
is private and can only be called from the Dog class directly.
This is handy if you don’t have the documentation available and want to view a class’s API.
accessors, readers and writers
We’ve already learned how to expose update different instance variables of an object using setter methods, and how to retrieve values by exposing the variables with getter methods.
class Flower
def initialize(color)
@color = color
end
def color
@color
end
def update_color(new_color)
@color = new_color
end
end
Ruby has some special methods for simplying this functionality.
attr_reader :color
will give you a getter method, attr_writer :color
will give you a setter and attr_accessor :color
will give you both a getter and a setter.
Modify the Flower class so that it no longer has a color
and an update_color
method, but instead using the methods we just learned.
class methods
So far we’ve only discussed instance methods, even though we have already used class methods like File.open()
and Random.rand()
.
So what’s the difference?
Class methods, are methods that can be called on a class rather an an instance of it. For example, Random.rand()
, which generates a random number doesn’t need an instance of Random to be generate, the same applies to File.open()
; there is no file to apply the open operation on yet.
Let’s define a class method with_red_color
, in the Flower
class. To define a class method we must use self.
in front of the method name.
class Flower
def self.with_red_color
Flower.new("red")
end
...
end
By running Flower.with_red_color
we get back an instance of the Flower class with the color set to red.
Altenative syntax
Class methods can also be defined by appending self to class
class Flower
class << self
def with_red_color
Flower.new("red")
end
end
end
This behaves exactly like self.with_red_color
. However, the first way is encouraged as it’s easier to spot class methods when you have a lot of code. Why is this mentioned then? Being able to read and understand code that other people have written is important, and using class << self
is not that uncommon.
Constants
Constants are similar to variables with the difference that the value remains unchanged while the program runs.
class Flower
ROSE = "red"
ANEMONE = "purple"
CHRYSANTHEMUM = "yellow"
...
end
We can now use the constants to create new types of known flowers.
Flower.new(Flower::ROSE)
Exercise: A text based game
To practise what we’ve learned so far, we will be writing a text based adventure game!
For the adventure we will create a Player
, a Location
and a Map
class.
Let’s start by defining Player
. A player has an array of items
and a location
.
class Player
def initialize
# initialize attributes
end
end
Task 1: Viewing location and picking up items
For now, let’s use the following hash as the location.
location = { description: "You are in the living-room. A wizard is snoring loudly on the couch.",
items: ["whiskey", "bucket"] }
So using this example, let’s implement the following methods on Player
-
look_around
prints out the description of the location. -
pick_up(item)
removes and item from the location, and adds it toPlayers
’sitems
array -
extend
look_around
to print out all the items of a location- For each item print
"You see a #{item} on the floor"
- For each item print
Let’s try this out by passing in this location hash to a new instance of Player
.
player = Player.new(location)
player.look_around
player.pick_up("whiskey")
player.look_around
Task 2: Moving between locations
So far, our Player
only knows about one location, but in the game we can move between multiple locations through different paths.
To do that, let’s create a Map
. The map get initialized with a list of locations
and assigns the current_location
to the first location on the list.
In Map
, add a method describe
that prints out the description
of the current_location
.
For implementing move_to(direction)
, first let’s use a new version of the location hash.
locations = [{ name: "living_room",
description: "You are in the living-room. A wizard is snoring loudly on the couch.",
items: ["whiskey", "bucket"],
edges: [{ direction: "upstairs",
item: "ladder",
location: "attic" }] }, {
name: "attic",
description: "You are in the attic. There is a giant welding torch in the corner.",
edges: [{ direction: "downstairs",
item: "ladder",
location: "living_room" }] }]
The move_to
method receives the direction that we want to move towards. It must find the location connected to the current location with that path. To do that, we need to iterate over the locations
and then the edges
of each location, and return the edge’s location if its direction matches the direction we are trying to go towards.
@locations.each do |location|
location[:edges].each do |edge|
location_name = # do something if # check condition
end
end
Not that we know the location name, let’s find the new location from the list and assign it to @current_location
.
To make it easier to explain what the code does, can first move the code that finds the location’s and then update the current location
Let’s try it out!
map = Map.new(locations)
map.describe
map.move_to("upstairs")
map.describe
Let’s also extend the describe
method of the map to print out the location’s available paths. You can use puts "There is a #{path[:item]} going #{path[:direction]} from here."
. This way we know what path we can move towards.
Task 3: Using both Map
and Player
To use the Map
with the Player
, let’s change the Player
to accept an instance of Map
when it’s initialized.
On the Player
class create
- a
location
method that points to@map.current_location
- a
walk
method that call’sMap
’smove_to(direction)
.
Task 4 - Loading data
To get more data in the game, you’ll need to load them from a YAML file, using the ruby yaml library.
We have loaded data from a file before in the second Ruby tutorial; the only difference with loading data from a YAML file is that after reading the contents of a file using File.read()
we need to process the content through yaml.
require 'yaml'
data = File.read(filename)
adventure_map = YAML.load(data)
As yaml
is not part of Ruby’s core libraries (but is part of the standard libraries), you need to require
it before making use of it.
You can download the file with all the data setup for our adventure from Github.
Bonus: Exception Handling
Exceptions are errors thrown by the program when something goes wrong. We can manage Exceptions using try catch blocks.
begin
# broken code
rescue
# handling exception
end
We can use Exception handling to manage errors when we attempt to move_to a non existing locations. To do that, we need to wrap finding the location name, and assigning it to the @current_location
in a begin
block, and in the rescue
block following it we can output something like put "You can't go that #{direction"}
Try this out by attempting to move left
on the map.
...
map.move_to("left")
Homework: Extending the game
Extend the game so that
- a
Player
can drop an item. The item should be removed from thePlayer
’s item list and added to thecurrent_location
’s items. - add an
inventory
method to aPlayer
. Callinginventory
prints out a list with all the items that thePlayer
is holding - extract a
Location
object. Modify your code so that it works with the location object, instead of a hash.
If you need any help don’t hesitate to ask in our chatroom.
This ends our Object Oriented Ruby and Inheritance (part 2) tutorial. Is there something you don’t understand? Try and go through the provided resources with your coach. If you have any feedback, or can think of ways to improve this tutorial send us an email and let us know.
Further reading
If you want to challenge yourself further and dive into the world of Rails, a Ruby framework for building websites, here is a tutorial book to get you started.