Object-Oriented Programming in Ruby. Basics and definitions (2/2)

Faouzi
Faouzi
Jun 18, 2019 · 11 min read
Image for post
Image for post
Classes and Modules in Ruby

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:

How to implement Inheritance in Ruby

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.

A more elaborated inheritance example. Sub-classing produces a hierarchical tree with more specialized classes.

There are many things happening here:

From line 1 to 17 we define the class Vehicle that contains aattr_accessor method, one constructor initialize and two instance methods speed_up and slow_down. Remember from our previous article that the use ofinitialize 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 energy.

From line 25 to 29 we define the class FlyingCar that inherits from the Car class and contains one instance method fly.

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 Vehicle class on its instance gives the expected behavior (line 35–39). Interestingly trying to call a method from the Car class on an instance of the Vehicle class raised an NoMethodError which makes sense because Car is a subclass of Vehicle.
  • Car being a subclass of Vehicle , its instance responds to method calls from its own class but also from the Vehicle class (line 42–47). As saw in our first example.
  • Now comes the interesting part. FlyingCar is a subclass of Car . This means that FlyingCar inherits from Car but also from Vehicle. Indeed our instance flyingcar answers to method calls defined in Vehicle and Car classes. 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 ancestors on the FlyingCar class. The return value of this method called lists in an Array the corresponding hierarchical tree. In Ruby the ancestors class 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 Human and 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-a relationships 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 Comparable and Enumerable provide common functionalities to a wide variety of classes and so, provide several common interfaces to their respective instances.

Polymorphism.

Let’s do the same exercise as previously. Here a proposed definition of polymorphism:

the quality or state of existing in or assuming different forms.

Hmmm…kind of enigmatic especially if we consider programming. What could take or assume different forms in our programs?

Inheritance Polymorphism.

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 Car and 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:

Method Overriding

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 Car and 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).

Using super.

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.

Using super for method overriding

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.

Interface Polymorphism.

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:

Before Duck-Typing…

From line 18 to 40 we defined four classes, Scientist, Programmer, MedicalDoctor and 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 do_science. The Fireman class has a method called do_save_people ect…

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.

After Duck Typing

From line 9 to 31 we have the same classed as before except that they all contains an instance method named work.

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.

Conclusion

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 super).
  • 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)

  1. An Introduction to Object-Oriented Programming (3rd Edition). Timothy A Budd. Addisson Wesley Longman. ISBN 0–201–76031–2. 2002. Free chapter samples here.
  2. Object Oriented Programming with Ruby. Launch School. Available online (https://launchschool.com/books/oo_ruby).
  3. Object Oriented Perl. Damian Conway. Manning. ISBN 9781884777790. 1999.
  4. 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.

Launch School

Publications of the Launch School Community

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store