The Basics of OOP Ruby
An overview of key concepts
Ruby is an object-oriented programming language (OOP) that uses classes
as blueprints for objects
. Objects are the basic building-blocks of Ruby code (everything in Ruby is an object), and have two main properties: states and behaviours. Ruby classes are the blueprints that establish what attributes
(also known as states) and behaviours (known in Ruby as methods
) that an object should have.
Let’s back up and clarify what exactly OOP is:
OOP is a type of computer programming language that arose as a solution and response to bigger and more complex code. Although a complete history is beyond the scope of this article, the main takeaway is that OOP principles have been around since the 1950’s-1960’s, but really began to dominate the programming world in the 1990’s and continues to do so today (Wikipedia). Abstraction, polymorphism, inheritance and encapsulation form four of the main pillars of OOP. The acronym “A PIE” is a helpful mnemonic to remember them. Let’s explore how they apply to Ruby:
Abstraction
Ruby exhibits Abstraction by allowing us to form mental models of problems using familiar ‘real-world’ concepts, which allows us to abstract the problem to a more familiar domain. It puts the emphasis of the programming language on human needs over machine needs. We concern ourselves with a higher-level sense of the problem without worrying about implementation details like binary code or whether the code will run on a specific operating system. This allows us to restrict our focus to objects
with properties (states) and behaviours (methods
).
Encapsulation
Speaking about restricting our focus, Ruby does something similar in the form of encapsulation. Just like how we don’t need to worry about the meaning of life while we’re engaged in our work as programmers, Ruby restricts its focus to only what is relevant to the task at hand. It’s perfectly valid to think about the meaning of life, but it’s probably not relevant most of the time that you’re programming. Encapsulation is the deliberate erection of boundaries in code that prevents erroneous accessing and modifying of states and behaviours that don’t make sense for what our intention is. If for example, we create a CityPark
class to form a blueprint of properties and behaviours that we expect out of a CityPark
object, we expect that there may be a relation in terms of behaviours with a Forest
class, but we don’t want their particular attributes to overlap.
We would expect both a Park
object and a Forest
object to contain tree attributes, but the specifics should be unique to the particular object. In other words, that information should be encapsulated within that specific instance of the class
. If we try to retrieve the information regarding the number of trees in a specific park, we don’t want to return the value of the number of trees in some forest. The state of each object is said to be unique, because it’s bound to a specific object. We can therefore create multiple CityPark
objects and expect to have the ability to retrieve information about each specific park individually.
Notice how on line 25
we chain the name
method to the high_park
local variable which is where the specific instance of the CityPark
class is stored. This works because the name
method is actually an instance method
of the CityPark
class (CityPark#name
). The name
method is a ‘getter’ method which retrieves the data stored in an instance variable. Instance variables are where we store the specific unique attributes (or states) for instances of the class.
Public, Private, Protected
Encapsulation is at work in many other ways in Ruby, and classifying methods as either public
, private
, orprotected
is another example. public
methods are the interface that we use to interact with an instance of the class (they can be invoked outside of the class — as we do on lines 25–29
). private
methods have a much more restricted scope, and are only invokable from within a class, and without an explicit receiver. Private methods are for implementation details that don’t need to be accessed outside of the class, or to deliberately hide information. Think about how you might want to restrict access to sensitive information like a bank account balance, while allowing another method within the class to check if that balance has enough money to withdraw. protected
methods are a sort of compromise and behave like public
methods when accessed inside a class definition, but behave like private
methods when accessed outside.
Polymorphism
Also notice how we used what looks like the same name
method on objects of two different classes — and Ruby didn’t get confused!? This is a demonstration of polymorphism, which is being able to have a single interface perform different functionality depending on the context in which it’s invoked. Because of Ruby’s method lookup path, (and encapsulation) the Ruby interpreter first looks for the method
that’s being invoked in the class
of the calling object (here on line 25
it would be the CityPark
class). Because a name
method is found within the CityPark
class, it stops looking and invokes this method. Note that in our example the two name
methods are fully independent: CityPark#name
and Forest#name
. If we wished, we could modify one of those methods for different functionality without affecting the other.
This flexibility allows us to re-write methods of the same name within custom classes to make their implementation details specific to the needs we want, while exposing a familiar public interface with which we interact with.
But hold up. So far in our example both the CityPark
and Forest
class share the exactly the same behaviours and attributes (objects of each class have name
and num_trees
attributes, and an instance method to retrieve that information). Doesn’t it seem inefficient to have written repetitive code? Anyone say DRY? As in: “Don’t Repeat Yourself”, not: “that insult was harsh and accurate”.
Inheritance
Remember that other pillar of OOP? Inheritance is the ability of related classes to share behaviours through a hierarchical structure of single inheritance. Subclasses inherit the methods from their parent classes (which includes the methods that it inherits through its parent class and so on so forth up the chain). It’s called single inheritance because a given class can only ever directly subclass from one parent class. In our example, we can re-imagine our class relationships by creating a superclass that we’ll call GreenSpace
. By subclassing the CityPark
and Forest
classes from this shared ancestor GreenSpace
, both of those subclasses will inherit the behaviours and attribute domains of the GreenSpace
class (the <
symbol on lines 12 and 14
denote a subclass relationship). So many characters saved, what relief! Seriously though, efficiency is wonderful.
Now, when we create a new CityPark
object on line 16
, there is no initialize
constructor method within the CityPark
class, so the Ruby interpreter will go through the method lookup path in search of an initialize
method (as a constructor method, the initialize
method is called automatically upon instantiation of a new object of the class). The exact method lookup path for a particular calling object can be found by invoking the ancestors
class method on the class of the calling object (it will return an array
, which is the names of the classes and modules that will be searched (in order) for the method being invoked). Ruby will stop looking and invoke the first method that it finds by the name provided.
Class methods are methods that are invoked on class itself, rather than on an object of the class. They do not have scope to the individual attributes of objects of the class, they’re focused on functionality that is more general to the class (such as finding out the method lookup path for any object within the class, which doesn’t concern itself with any specific instance).
Modules
Did you notice earlier that I mentioned modules, without explaining what those are? Modules are Ruby’s solution to multiple inheritance. Modules can be containers of methods that are out of place elsewhere in your code (applicable in some places but not others), or it can contain multiple whole classes to group classes that are similar but not hierarchically related. Any number of modules can be mixed into a class (remember that a class can only directly inherit from one other class) by using the include
reserved word followed by the module name. This is called a mixin. Note that objects cannot be instantiated from modules; that is restricted to classes.
To demonstrate when to use a module vs. inheritance it can be useful to think about the relationship. If it’s an “is-a” relationship such as “A City Park is a Green Space”, then it’s likely more sensible to inherit behaviours through hierarchy from a GreenSpace
parent class. If it’s more of a “can-do” relationship such as “You can go swimming in a CityPark”, then that behaviour might better suit being mixed in through a module*. The convention with module naming is to append the “-able” suffix to the module name, such as with Swimmable
.
If we add some more classes to our example, a RecreationCentre
class that subclasses from CityPark
, and a Lake
class that subclasses from Forest
, we see how modules are useful. Despite all of the classes sharing the methods from the GreenSpace
class higher up in the hierarchy, we don’t want to put the swim
method there because that would include the behaviour in places it doesn’t belong such as our CityPark
class. Instead of creating a mess by including the swim
behaviour in a shared parent class (or being gross and repeating ourselves), we include the Swimmable
module to add the swim
method functionality only where it’s needed: within the Lake
and RecreationCentre
classes.
Setter and Getter Methods
Let’s investigate our new code further. Notice the attr_reader
on line 10
. This is the line of code that creates that getter method that we mentioned earlier. It’s part of the attr family, which creates attribute setter and getter methods for the arguments passed in as symbols (here :name
, and :num_trees
). attr_reader
is the short-form way to create a reader method, attr_writer
will create a writer method, and attr_accessor
will create both. It’s convention to pass the instance variable as a symbol rather than to use some other more descriptive name like get_name or show_num_trees. This makes the invocation of the method clean and straightforward. It’s also possible to define setter and getter methods using the def
reserved word like with traditional instance methods if you want to add functionality (like formatting a name in a setter method).
Super
Did you notice the word super
on line 25
? The reserved word super
passes any arguments supplied up the method lookup path to the first method of the same name that Ruby finds, which it then invokes. In our example, super
on line 25
passes the name
and num_trees
arguments up to the initialize
method in the GreenSpace
class. Ruby then continues to execute the originally invoked initialize
method on line 24
and assigns the philanthropist
argument to the @philanthropist
instance variable, where it is stored as a unique state of the instance of the RecreationCentre
class (see the return value of line 43
). Super can also be invoked without arguments to pass all supplied arguments up the lookup path.
Self
Why don’t we talk about some more Ruby OOP basics, such as the reserved keyword self
. Self is interesting, because its meaning is dependant on context. Self refers to the calling object, which for us can mean a specific instance of the class, or the class itself, depending on where it’s used.
In our latest example, on line 37
inside the RecreationCentre
class we define a class method. Remember those? We know we’re defining a class method because the context is inside a class, but outside of an instance method definition. The self
here refers to the class itself, which makes it clear why we use it to define a class method.
On lines 33–35
we defined a new instance method called whats_this
which only invokes self
. Because the context here is within an instance method, self
now refers to the specific instance of the class that the method is invoked on, which we do on line 57
. Does any of that return value look familiar to you?
Inside some encoding is RecreationCentre
which a class we’ve defined, then an octal number followed by @name=”Scadding Court”
, which is an attribute we’ve assigned, followed by @num_trees=25
, and then @philanthropist=”Jim Balsillie”
. Interesting! This is all the information about the object we invoked the CityPark#whats_this
instance method on. This is our calling object.
Did you notice another change in the code on line 10
? The attr_reader
has been changed to an attr_accessor
, which now creates not just the getter, but both the setter and getter methods. We demonstrate this change in functionality on lines 54–56
. Because these setter and getter methods are public (by default), we can invoke them outside of the class definition.
Final Thoughts
I want to point out that class relationships aren’t always obvious when designing a program, and designing good relationships between classes is a skill that takes a lot of practice. There’s a whole field dedicated to OOP design patterns, so don’t expect to master them overnight. There are tools like Class Responsibility Cards (CRC) that can help during the design phase, and also it’s often helpful to do exploratory code spikes to test out relationship ideas before proceeding with the code. In the example we’ve used here, it doesn’t make a lot of sense to have a RecreationCentre
class subclass from a CityPark
class.
When you think about it, how much do recreation centres and parks have in common? Recreation centres should be their own standalone class whose objects act as collaborator objects to the CityPark
class. City parks can have recreation centres, but those centres aren’t actually green spaces so much as things that are located in green spaces.
There’s a lot more to OOP Ruby than what I’ve shown, but I hope this at least helps clarify some concepts for you, and if not, then leave a comment and we’ll get you sorted out.
If you think you know a concept, try explaining it to a wall. It’s called the ‘Rubber duck’ technique and is especially useful for self-directed learning. Reaching out to another student is even better.
*With thanks to Max Hawkins for editing advice as well a good explanation of when to use modules vs. class inheritance.
- *Also, much thanks to the Launch School staff for helping me learn these concepts.