This chapter will give you an overall taste of Object Oriented Programming. All of these topics will be covered in more detail in the chapters to come.
Object Oriented Programming, often referred to as OOP, is a programming paradigm that was created to deal with the growing complexity of large software systems. Programmers found out very early on that as applications grew in complexity and size, they became very difficult to maintain. One small change at any point in the program would trigger a ripple effect of errors due to dependencies throughout the entire program.
Programmers needed a way to create containers for data that could be changed and manipulated without affecting the entire program. They needed a way to section off areas of code that performed certain procedures so that their programs could become the interaction of many small parts, as opposed to one massive blob of dependency.
Enter OOP. We'll first introduce some terminology, then dive into examples.
Encapsulation is hiding pieces of functionality and making it unavailable to the rest of the code base. It is a form of data protection, so that data cannot be manipulated or changed without obvious intention. It is what defines the boundaries in your application and allows your code to achieve new levels of complexity. Ruby, like many other OO languages, accomplishes this task by creating objects, and exposing interfaces (i.e., methods) to interact with those objects.
Another benefit of creating objects is that they allow the programmer to think on a new level of abstraction. Objects are represented as real-world nouns and can be given methods that describe the behavior the programmer is trying to represent.
Polymorphism is the ability for different types of data to respond to a common interface. For instance, if we have a method that invokes the
move method on its argument, we can pass the method any type of argument as long as the argument has a compatible
move method. The object might represent a human, a cat, a jellyfish, or, conceivably, even a car or train. That is, it lets objects of different types respond to the same method invocation.
"Poly" stands for "many" and "morph" stands for "forms". OOP gives us flexibility in using pre-written code for new purposes.
The concept of inheritance is used in Ruby where a class inherits the behaviors of another class, referred to as the superclass. This gives Ruby programmers the power to define basic classes with large reusability and smaller subclasses for more fine-grained, detailed behaviors.
Another way to apply polymorphic structure to Ruby programs is to use a
Module. Modules are similar to classes in that they contain shared behavior. However, you cannot create an object with a module. A module must be mixed in with a class using the
include method invocation. This is called a mixin. After mixing in a module, the behaviors declared in that module are available to the class and its objects.
We'll see examples of all the above mentioned terms in action in the following chapters.
Throughout the Ruby community you'll often hear the phrase, "In Ruby, everything is an object!". We've avoided this so far since objects are a more advanced topic and it's necessary to get a handle on basic Ruby syntax before you start thinking about objects.
It's not even strictly true; not everything in Ruby is an object. However, anything that can be said to have a value is an object: that includes numbers, strings, arrays, and even classes and modules. However, there are a few things that are not objects: methods, blocks, and variables are three that stand out.
Objects are created from classes. Think of classes as molds and objects as the things you produce out of those molds. Individual objects will contain different information from other objects, yet they are instances of the same class. Here's an example of two objects of the
irb :001 > "hello".class
irb :002 > "world".class
In the above example, we use the
class instance method to determine what the class is for each object. As you can see, everything we've been using, from strings to integers, are in fact objects, which are instantiated from a class. We'll dig deeper into this very soon.
Ruby defines the attributes and behaviors of its objects in classes. You can think of classes as basic outlines of what an object should be made of and what it should be able to do. To define a class, we use syntax similar to defining a method. We replace the
class and use the PascalCase naming convention to create the name. We then use the reserved word
end to finish the definition. Ruby file names should be in snake_case, and reflect the class name. Thus, in the below example, the file name is
good_dog.rb and the class name is
sparky = GoodDog.new
In the above example, we created an instance of our
GoodDog class and stored it in the variable
sparky. We now have an object. We say that
sparky is an object or instance of class
GoodDog. This entire workflow of creating a new object or instance from a class is called instantiation, so we can also say that we've instantiated an object called
sparky from the class
GoodDog. The terminology in OOP is something you'll eventually get used to, but the important fact here is that an object is returned by calling the class method
new. Take a look at Figure 3 to visualize what we're doing.
As you can see, defining and creating a new instance of a basic class is simple. But before we go any further showing you how to create more elaborate classes, let's talk about modules briefly.
As we mentioned earlier, modules are another way to achieve polymorphism in Ruby. A module is a collection of behaviors that is usable in other classes via mixins. A module is "mixed in" to a class using the
include method invocation. Let's say we wanted our
GoodDog class to have a
speak method but we have other classes that we want to use a speak method with too. Here's how we'd do it.
sparky = GoodDog.new
sparky.speak("Arf!") # => Arf!
bob = HumanBeing.new
bob.speak("Hello!") # => Hello!
Note that in the above example, both the
GoodDog object, which we're calling
sparky, as well as the
HumanBeing object, which we're calling
bob, have access to the
speak instance method. This is possible through "mixing in" the module
Speak. It's as if we copy-pasted the
speak method into the
When you call a method, how does Ruby know where to look for that method? Ruby has a distinct lookup path that it follows each time a method is called. Let's use our program from above to see what the method lookup path is for our
GoodDog class. We can use the
ancestors method on any class to find out the method lookup chain.
puts "---GoodDog ancestors---"
puts "---HumanBeing ancestors---"
The output looks like this:
Speak module is placed right in between our custom classes (i.e.,
HumanBeing) and the
Object class that comes with Ruby. In Inheritance you'll see how the method lookup chain works when working with both mixins and class inheritance.
This means that since the
speak method is not defined in the
GoodDog class, the next place it looks is the
Speak module. This continues in an ordered, linear fashion, until the method is either found, or there are no more places to look.
That was a very quick overview of OOP in Ruby. We'll spend the next couple of chapters diving into the details.
How do we create an object in Ruby? Give an example of the creation of an object.
We create an object by defining a class and instantiating it by using the
.new method to create an instance, also known as an object.
my_obj = MyClass.new
What is a module? What is its purpose? How do we use them with our classes? Create a module for the class you created in exercise 1 and include it properly.
A module allows us to group reusable code into one place. We use modules in our classes by using the
include method invocation, followed by the module name. Modules are also used as a namespace (we will cover this in a later section).
my_obj = MyClass.new