In a previous article, we defined some basics concepts (Objects and Classes) about Object-Oriented Programming (OOP) in Ruby. Tackling these concepts immediately raises a number of questions:
- How should our programs manipulate classes and the corresponding objects (the instances of these classes)?
- What are the possible relations between classes? between objects?
- How can we capitalize on the commonalities that may exist between various classes?
We will see in this article that answers to these questions rely on the use of specific approaches characteristic of OOP as inheritance and polymorphism.
Inheritance and its implementation.
Looking at the definition for ‘inheritance’ that we get from the dictionary, already gives us some keys notions:
- Inheritance means receiving something from someone.
- What we receive can alter the way we look and behave (notion of cultural or genetic inheritance).
In programming talking about inheritance means talking about the relationship existing between the classes we designed and how one can inherit from another. In ruby it is very simple to implement inheritance between classes:
In line 1–5 we define
class A that contains the definition for one instance method
legacy . In line 7–8 we define a
class B . As you may have noticed we used a new construct here:
< A . Using the operator
< followed by the name of any class permit to implement inheritance between classes in Ruby. One specificity of Ruby is that class can only inherit from one another class. Indeed Ruby does not allow multiple inheritance.
In our example we say that
class A is the super-class and that
class B is a subclass of
class A . What does this imply in our program? In line 10 and 13 we created instances of each class and in lines just below we called the
legacy method on these instances. Interestingly you can see that while
legacy is not explicitly defined in
class B , its instance answers to the method call. This illustrates that in the context of inheritance instances from the subclass can access both behaviors (methods) but also data (attributes, not illustrated here) associated with the super-class (Timothy Budd 2002).
Our example was pretty trivial but illustrates well the basic mechanism implemented by inheritance. However when coding with intention there are some subtleties to be aware of. Inheritance can be more complex and needs to be used to map specific relationship between your classes. Let’s see a basic example here from which we will elaborate a more precise definition of inheritance.
There are many things happening here:
From line 1 to 17 we define the
class Vehicle that contains a
attr_accessor method, one constructor
initialize and two instance methods
slow_down. Remember from our previous article that the use of
initialize here is a common way to automatically encapsulate data when creating objects and that
attr_accessor is a built-in method in Ruby that provide access (Read and Write) to this data.
From line 19 to 23 we define the class
Car that inherits from the
Vehicle class and contains one instance method
From line 25 to 29 we define the class
FlyingCar that inherits from the
Car class and contains one instance method
From line 31 to 33 we create instance of each class with appropriate sates.
From line 35 to 55 we have a set of method calls for each object. Let’s see these in details:
- We can see that calling all the instance methods from the
Vehicleclass on its instance gives the expected behavior (line 35–39). Interestingly trying to call a method from the
Carclass on an instance of the
Vehicleclass raised an
NoMethodErrorwhich makes sense because
Caris a subclass of
Carbeing a subclass of
Vehicle, its instance responds to method calls from its own class but also from the
Vehicleclass (line 42–47). As saw in our first example.
- Now comes the interesting part.
FlyingCaris a subclass of
Car. This means that
Carbut also from
Vehicle. Indeed our instance
flyingcaranswers to method calls defined in
Carclasses. This illustrated very well how inheritance in Ruby can progressively create a hierarchical tree with extended and more specialized sub-classes.
- In Ruby we can easily visualize this hierarchical tree. In line 57 we call the class method
FlyingCarclass. The return value of this method called lists in an Array the corresponding hierarchical tree. In Ruby the
ancestorsclass method is commonly used to check the method lookup path (Launch school). By definition, to resolve a method call on an object, Ruby will look for the expected method going through the class hierarchy.
Based on our example we can now say the following: behaviors and data associated with sub-classes can be considered as an extension of the super-classes. This means that in addition to the inherited behaviors and attributes, the subclass can also define new methods and include new data. The subclass appears then as a more specialized class than the super-class (Timothy Budd 2002). This has important consequences while designing programs. Indeed while it might seem easy to understand the concept behind inheritance, the real challenge lies into its appropriate implementation when designing your program. A common rule is to use inheritance to map
is-a relationships. In our example, a car is a vehicle, a flying car is a car and is a vehicle.
Modules in Ruby: a tool to circumvent the lack of multiple inheritance.
We said it earlier. Ruby does not implement multiple inheritance. Instead we can only subclass one class from one super-class. This is a drastic choice but avoid to fall into ambiguous class design. Have a look to this code:
We are dealing with animals species so it is quite easy to map the
is-a relationship existing between them. But our interest here will focus on the
swim instance method defined in our
Fish. Both humans and fish have the ability to swim. Naively if multiple inheritance was allowed we could just make
Human inheriting from
Fish so that he gets the
swim instance method. Weird isn’t it? Human is not a fish obviously. But how can we extract common abilities / functionalities between classes that exhibit no direct inheritance relationship. Here comes the module.
But what is a module?
- Modules are the containers part of the class. In Ruby modules can contains, class definitions, method definitions, other modules and constants (this process is called name-spacing). Modules cannot be instantiated meaning they cannot create any objects.
- They are classically used to extract common methods / behaviors that map
has-arelationships between classes. It allows to provide common functionality to objects from different classes. In our case humans has the ability to swim, so do fishes. we could then extract this method into a module and get the following code:
From line 1 to 5 we defined a module called
Swimmable . Defining a module is as easy as defining a class or a method. We use the
module...end construct for that. Inside we can then put any methods, classes, constants or other modules we want. Then to provide data and interfaces defined inside modules to other class we need to use the keyword
include + the name of the module: this process called mix-in allows any instance of your class to have access to all the methods defined in your module.
Very insightful examples of how modules are used are actually available in Ruby Core Library. Modules as
Enumerable provide common functionalities to a wide variety of classes and so, provide several common interfaces to their respective instances.
Let’s do the same exercise as previously. Here a proposed definition of polymorphism:
Hmmm…kind of enigmatic especially if we consider programming. What could take or assume different forms in our programs?
To answer this question let’s think about it. Our classes are designed to produce objects (that encapsulate data and behaviors) and give us the mean to manipulate them. To manipulate them we need interfaces / methods. Objects can answer to specific method calls which result in specific behaviors. We just saw that with inheritance we could share common methods between classes. In our previous example, objects instantiated from
FlyingCar classes share the instance method
energy and respond to its call similarly meaning that they exhibit a common behavior. But let’s imagine now that our flying cars can use solar energy and oil to function. We would need here to modify the implementation of the method
energy. Let’s do it:
We design a similar program than our previous example except that in our
FLyingCar I added a method called
energy. Doing this, we just override the
energy method inherited from the
Car super-class. One could say “why doing that?” We could have created another method let’s say
energy_2 (very bad naming by the way) that would output the same but would not override the one inherited from the
Car class. From a design perspective it would be a bad approach. Because it would mean that we could still call these two methods on any instances of the
FLyingCar which could lead to some errors or confusion given the fact that our flying cars need both oil and solar energy. Method overriding is a powerful tool and allows to bring more functional specialization in sub-classes.
In our example now, we can use exactly the same
energy method / interface on any instances of both
FlyingCar classes. In other words, instances from these classes will answer to the same method call but will exhibit different behaviors: this is polymorphism in action. In Ruby polymorphism provides single interface to objects of different classes (inspired by the definition of Bjarne Stroustrup). This means that polymorphism is the ability of different objects, instantiated from different classes to respond in different ways to the SAME method calls. In the context of inheritance, method overriding is a way to implement inheritance polymorphism in Ruby (Damian Conway 1999).
Before diving into another type of polymorphism. We will talk a little bit about the built-in function provided by Ruby and called
super . It allows us to call methods up the inheritance hierarchy tree. When calling
super from inside a method, it will search in the method lookup path for a method with the same name and then invoke it.
super will then return the value of this method and can be used to implement inheritance polymorphism.
In line 31 we used
super which returns the value from the method
energy defined in the
Car class. The return string value is then concatenated with another string in the
energy method defined in the
FlyingCar class. Being a method we can pass any arguments to
super . This is notably a nice way to rewrite
initialize methods in sub-classes. To dig more into the function of
super I invite you reading this.
In their book Hal Fulton and Andre Arko report a definition of polymorphism proposed by Damian Conway and translate it to Ruby (Fulton and Arko 2015). They define a second type of polymorphism called interface polymorphism. This does not require any inheritance relationship between classes but essentially the definition of methods with similar names. This touches a point that is very important in Ruby: the importance of messages. Objects receive messages or “answer” to method calls. Ruby does not care about the type of the object as long as it can receives and interprets the message. A very clear example of this is Duck Typing:
In computer programming with object-oriented programming languages, duck typing is a style of dynamic typing in which an object’s current set of methods and properties determines the valid semantics, rather than its inheritance from a particular class or implementation of a specific interface.
In other words the programmer can use a single code with different objects from different classes, the importance being how the object behaves rather than what the object is. This supports the idea that Ruby objects are mainly defined by the messages they can respond to. Let’s have a look to an example:
From line 18 to 40 we defined four classes,
Fireman. Each of them contains a specific instance method that will output the type of work they do. For the class
Scientist has an instance method called
Fireman class has a method called
From line 1 to 16 we defined a class
MyWork that contains a class method
self.i_work which can take a list of arguments (here instances from the classes described before) and will output the type of work it corresponds to. For that we implement a
case statement. This means that the program we designed here, needs to determine what is the type of object before calling the appropriate method. Imagine now that you need to do that for hundreds of jobs… your case statement will be huge and difficult to read and your code will be poorly flexible and reusable.
The trick here is that, although they all do different things, programmers, doctors, scientists and firemen all work. Then, instead of defining a method with a name that specify what they do, we could just use a more generic naming. Let’s have a look to this approach.
From line 9 to 31 we have the same classed as before except that they all contains an instance method named
From line 1 to 7 we defined a class
MyWork that contains a class method
self.i_work which can take a list of arguments (here instances from the classes described before). In the method definition body, we will iterate through each element of the argument list and invoke the method
work on each. In these settings the code does not assess the type of object it is dealing with. Instead it provides a single interface here the method
work to several object from different classes. By doing this we have a more flexible and readable code. If we needed to add more jobs we could just create new classes and kept the class
MyWork as it is.
Modules and interface polymorphism
Another way of implementing interface polymorphism would be to combine modules with method overriding. Modules would provide common interfaces to several different classes’ instances when method overriding could be a way to re-write and modify methods to provide more specific functionalities only in some of your classes. While possible this is not the way modules are usually used in Ruby (Fulton and Arko 2015). Indeed modules are considered to be part of the class they are mix-in, this process being viewed as providing some level of multiple inheritance (Fulton and Arko 2015) as discussed previously.
This article aims at defining two critical concepts of OOP: Inheritance and Polymorphism. As a conclusion and for the laziest readers, here are some straightforward definitions for these concepts (adapted from Damian Conway 1999):
- Inheritance: A relationship between two classes in which the subclass assumes all the attributes and methods of the super-class.
- Polymorphism: Ability for different objects, instantiated from different classes to respond in different ways to the SAME message / interface / method calls
- Inheritance Polymorphism: A form of polymorphism that requires an invoking object to belong to a particular class hierarchy. Under inheritance polymorphism, a method can only be invoked on an object if the object belongs to the hierarchical tree, up to the original super-class (Implementation by method overriding or using
- Interface Polymorphism: A form of polymorphism that does not require the invoking object to belong to a particular class hierarchy. Under interface polymorphism, a method may be invoked on an object if the object’s class has a suitably named method (Implementation using Duck Typing, possibility to implement it using modules + method overriding)
Bibliography (in order of citation)
- An Introduction to Object-Oriented Programming (3rd Edition). Timothy A Budd. Addisson Wesley Longman. ISBN 0–201–76031–2. 2002. Free chapter samples here.
- Object Oriented Programming with Ruby. Launch School. Available online (https://launchschool.com/books/oo_ruby).
- Object Oriented Perl. Damian Conway. Manning. ISBN 9781884777790. 1999.
- The Ruby Way: Solutions and Techniques in Ruby Programming (3rd Edition). Hal Fulton and Andre Arko. Addison-Wesley Professional Ruby Series. ISBN-13: 978–0321714633. 2015.