Classes and Objects
Ruby is Object Oriented. Many languages claim to be object oriented, but most fall short in some area or another. When Ruby says it’s object oriented, it really, really means it.
We have used lots of Ruby objects so far, when we have done things like:
"Abracadabra".reverse.downcase
and
Time.now
Lets look now at how we can define our own objects.
Classes
Ruby has a class based object model. This means we define classes, then use them to stamp out as many objects as we like. Of course, this being Ruby, classes are objects and have methods in their own right, but we’ll get to this soon enough.
We define a class like so:
class Pet
end
Here we have defined a very simple class, called Pet. By convention, classes in Ruby always start with a capital letter. Now lets create a pet:
flopsy = Pet.new
This is great, we have created a new instance of the Pet class. Flopsy is an instance of Pet.
Note that all objects get a new method for free. This they inherit from the Object superclass. More on inheritance in a bit.
Finding the class of an object
We can go in reverse, to find the class of an instance like this
flopsy.class
=> Pet
Ruby doesn’t shy away from introspection, it comes baked in, as we shall see later.
Methods - Giving Flopsy Abilities
This is all very nice, but flopsy is not very interesting, she can’t walk the tightrope or play chess, or really do anything much. To make Flopsy more interesting, we need a method:
class Pet
def play_chess
puts "Now playing chess"
end
end
Here see now. We have added a play chess method to flopsy. We can now write:
flopsy.play_chess
…and she will, after a fashion. She is only a housepet after all.
Naming Conventions
There are a few things to bear in mind when naming methods in Ruby if you want to appear cool and down with the kids.
First, use snake case for all function names, like this.
each_with_index
Second, if your method returns a boolean, and is a question, frame it as such. Use a question mark, like so:
['toast','jam','honey'].include? 'ham'
person.has_name?
password.valid?
Third, if your method modifies the original object in place, rather than returning a new object, and is therefore destructive, indicate this with an exclamation mark, like so:
['toast',['jam','honey']].flatten!
=> ['toast','jam','honey']
Instance variables - Giving flopsy some superpowers
Flopsy is still a little dull. It would be great to be able to store some data about her, maybe give her some custom attributes.
In Ruby we save an instance variable using the @ syntax. Instance variables are @ variables. All instance variables are private, so to get at them, we need to write methods called getters and setters to access them. Lets have a look now:
class Pet
def super_powers=(powers)
@super_powers = powers
end
def super_powers
@super_powers
end
end
Here we have given flopsy two methods, a getter and a setter. The first is a setter. The super_powers= method receives a parameter and stores it in an instance variable called @super_powers.
The second is a getter. It simply returns the @super_powers instance variable that was previously set.
We can now set flopsy’s super power like this:
flopsy.super_powers = "Flight"
and retrieve it like this:
flopsy.super_powers
=> "Flight"
Note we don’t have to declare the @super_powers variable anywhere. We can just set it, and that’s fine.
This is great because…
Getters and setters give us a clean way to provide an interface onto our object. It insulates us from implementation details. We are free to store the data in any way we wish, as a variable, as a file, in a database, in an encrypted hash, or as a combination of other variables.
This is how active record works when using Rails. Values can be got from the database as though we were accessing object attributes.
Just like Flopsy, the boundary between attributes and methods is far more fuzzy than in most languages. This is partly because of Ruby’s optional parentheses, which make it look as though we are accessing attributes, when in fact we are always accessing methods.
We can have read only attributes by only creating a getter, and write only attributes by only creating a setter. An example of a write only attribute would be a password, which might get set, and then encrypted with a one way hash, never to be read again.
The attr method
Since class variables are so common, ruby defines shortcuts for creating them and their associated getters and setters. The attr method creates an attribute and its associated getter. If the second parameter is true a setter is created too. The attr_reader and attr_writer methods create getters and setters independently.
Initialising flopsy using the initialize method.
The initialize method is called by the new method and it is here that we put any code we need to initialize the object.
When flopsy’s sidekick mopsy was first created, she didn’t have any powers at all, observe:
mopsy = Pet.new
mopsy.super_powers
=> nil
Poor mopsy. We can remedy this situation by giving mopsy a basic superpower when she is initialised. Lets do this now.
class Pet
def initialize(args = {})
@super_powers = args[:power] || "Ability to eat toast really really quickly"
end
end
Now when we recreate mopsy, she comes already tooled up
mopsy = Pet.new
mopsy.super_powers
=> "Ability to eat toast really really quickly"
We can also do this:
mopsy = Pet.new :power => "none worth mentioning"
mopsy.super_powers
=> "none worth mentioning"
If you need to see a list of all mopsy’s attributes you can do so using inspect like so:
mopsy.inspect
=> "#<Pet:0x102f87fe8 @super_powers=\"Ability to eat toast really really quickly\">"
Upshot
- Functions in Ruby are created using the def keyword (short for define).
- Functions that exist in an object are typically called methods.
- Functions and methods are the same, except one belongs to an object.
- Objects are created from classes using the .new method
Exercise - Create your own fluffster
Everything is an object, so it’s important that we get a whole lot of practice in with making them. In this series of exercises we will create a class, implement some methods, overload some operators and create some virtual attributes.
Define your class
- Define a class either for a lethal warship or a fluffy animal, depending on your mood today.
- Add methods so you can set a name for your gunship/fluffster.
- Add methods to get and set an age in milliseconds.
- Write a method that retrieves the age in days. You should use the same age attribute as in 3, just modify it before returning it.
- Write a method that sets the age in days. Again, use the same age attribute, just monkey with it a bit before setting it.
- Write a getter that returns the age in weeks. (age in days / 7)
- Write getters and setters that return a string representing a standard action, e.g. “wriggle cutely”, or “blow things up with extravagant firepower”
Exercise - Receiving a hash
Extend the warship / flufster class you created earlier so it can receive a hash of initial values.
Write an initializer method that accepts a hash and sets default values appropriately. You should be able to call it like this:
Fluffster.new age: 2, name: "Floppy"
Exercise - Read only virtual attributes
- Add x and y position attributes, with getters, but no setters. Initialise the position attributes to be zero.
- Add move north, south, east and west methods. When these methods are called they should modify the x and y positions.
- Add a distance_from_home method that computes the current distance from home.
- Add an at_home? method that returns true if the person is at home, i.e. if their x and y coordinates are zero.
Virtual attributes are particularly useful for things like passwords where you don’t actually want to store the password anywhere, or allow retrieval.