Object-Oriented Programming in Ruby. Basics and definitions (1/2)
I recently started to study object-oriented programming (OOP) in Ruby which is the second module provided by Launch School in their curriculum. The module starts by giving us a broad overview of the central concepts related to OOP in Ruby. They wrote a particularly well structured book that is freely available here. Nevertheless having a good grasp and understanding of these concepts is pretty difficult. I start digging a little bit into the bibliography and started reading some interesting book chapters about OOP concepts. The objective of these articles is to translate my understanding of some of these concepts into beginner friendly definitions. Nevertheless the idea here is not to just give a catalogue of the classical definitions in OOP (classes, objects, attributes) that you can find anywhere, but instead I will follow a more integrative approach to make the OOP abstraction less abstract and tackle concepts as encapsulation, inheritance and polymorphism and illustrate these with some examples.
Pillars of OOP
Just going out from months of learning Ruby programming in a procedural way, facing the new concepts of OOP is really disturbing. You need to reset your way of thinking and stop seeing your program as a successive and ordered set of procedures. Instead OOP allows to structure your program as a collection of units, called objects, each object being responsible for specific tasks (Timothy Budd 2002). In OOP it is by the interaction of these objects that your program runs (Timothy Budd 2002). Briefly, an object is an aggregation, bundle or encapsulation of states ( also named, attributes or more generically data) and behaviours, a set of methods that provide an interface to the functionality of the object (Damian Conway 1999, Hal Fulton and Andre Arko 2015). Object’s behaviours are dictated by the class. Object’s behaviours are exhibited in response to method calls. Every object is an instance of some class that will respond to method calls in a similar way (Timothy Budd 2002). Nevertheless while using the same methods (method with similar names), the interpretation of the methods resulting in specific outputs or return values might be different. In other words similar states processed through similar behaviours (methods) might give different results: this process is called polymorphism (Timothy Budd 2002). Polymorphism stems from another interesting aspect of OOP: inheritance. In Ruby and other programming languages, classes can be linked to each other following a hierarchical structure. Attributes and behaviours initialized and defined in higher-level classes (usually called parent-class, ancestor or super-class) can be accessed and used by lower-level classes (usually named child-class, descendant or sub-class) . Sub-classes are said to inherit their behaviour from the super-class (Timothy Budd 2002) and…
Haha ! We just started and we can see here that we already brought a lot of new words and concepts. This little introduction while kind of confusing paves the way with some critical concepts in OOP which are:
- Objects and Class
- Interface and Methods
it is crucial to understand those in order to build fundamentals knowledge about OOP. A difficulty while starting to study OOP is the semantic and notably the fact that people will use different words for similar concepts. I will try to stay consistent and will mainly follow the semantic proposed by the Launch School. In this article I will first focus on class and object which will allow me to talk about interface and encapsulation. In a next article we will tackle the notion of inheritance and polymorphism.
Classes and Objects
Defining the class
The object-oriented method is based on the notion of class. It describes an abstract data type that is a set of objects defined by their states and behaviours. In other words, a class is a formal definition of the states of objects and the methods that may be called to access object states and exhibit object behaviours (Bertrand Meyer 1994, Damian Conway 1999). In OOP classes are usually defined as blueprint or factories that are designed to produce objects and provide means of manipulating them (Launch school, Timothy Budd 2002)
In Ruby defining a class is really easy:
Defining the object
An object is a run-time instance of some class (Bertrand Meyer 1994). It has a unique identity, a capacity to store data, and an ability to respond to specific requests (Damian Conway 1999). In OOP objects are thus usually described as entity that act as containers for data but also control the access to the data. Data, that objects contained and provide access to, define the state of the objects. Creating an object in Ruby is also very simple. Once you defined a class you can invoke the class method
new. When creating an object it is said to be instantiated (remember, an object is an INSTANCE of a class).
Giving states to our object
We said that objects are container of data. But how can we actually inject data to our object? Many languages have the notion of constructors. A constructor might be seen as a routine that allow to create an object and provide piece of data to it (Damian Conway 1999). Ruby initialize data thank to the
initialize method and by using instance variables. In Ruby instance variables are defined using the
@ character. Instance variables retain the value assigned to it even after method in which it was initialized is terminated. This property makes them suitable to maintain and track object’s states (Launch School, David Black 2014).
States are unique. They belong to one object only. In other words objects are collections of unique instance variables (Damian Conway 1999). However objects are more than containers. They have an extra property called encapsulation. Encapsulation harbours many meanings. In our case encapsulation refers to data hiding or isolation. This suggests here that our program or the user cannot just simply access the data or states of an object. Let’s quickly think about it. How would you access here the
person1 ? What if we pass
person1 as an argument to some methods that we know usually output values from variables:
Using the same class as before, we initialized instance variables and create an object that harbours specific states, a name and an age. Using
p did not allow us to access or read any data. What these methods did was to output the identity of the object, with the
p method displaying the states of the corresponding object inside a very complicated id.
Let’s take a real world example to understand this. You want a coffee and for that you go to a coffee machine. You choose the type of coffee you want, insert some coins and push the button to get the coffee. The coffee will have the state you choose because you put the right amount of money and pushed the right button. This button is the interface you use to ask the machine to give you the good object with the right state. In our Ruby program we need interfaces to access the states of objects, we need method definitions. Let’s add some methods to our class in order to read our data:
We added two methods,
name. Both allow us to access and read the state of our object. These methods defined in our class are called instance methods and can be called on any object that belongs to the
Person class. These methods are designed to return the value referenced by our instance variables. To print them out we can call these methods on our object and pass the all expression as an arguments to the Ruby
puts method as shown in line 17 and 18.
In Ruby, instance methods are by default universally available. However, depending on their implementation they can be used to limit the ways in which an object’s state may be accessed or changed. Indeed an object’s state can only be accessed or modified in the ways permitted by that object’s methods (Damian Conway 1999). Let’s see an example of how method can limit the access to state. Very silly example:
The way we defined our methods prevents anyone to access the name of a person if this name does not start by the letter ‘A’ or to access the age of a person if is below 18 years old. Methods can also be used to modify data and alter object’s states. What if we made a mistake with the age of
Pierre who is actually 72 and not 27 years. Easy, let’s build a new interface / method that will allow us to correct his age:
We added here a method called
age= with one parameter. This method will basically passed one argument and will assign its value to our instance variable
@age. Notice the particular syntax we used here to define the method. We use the
= sign as a way to tell ruby that this method will modify object states (Launch school).
age= are our interface to interact with object’s states / data. These methods are called getter and setter methods respectively. Thank to these methods we can now read and re-write the data encapsulated in our objects. To re-enforce a previous statement, do not forget that each state is unique here and modifying the state of one object cannot affect the state of another objects. This is due to the feature of our instance variables that have a very specific scope that is the object they are bound to (Launch School, David Black 2014).
But imagine now that your object has 20 states ! You would need to write 20 pairs of setter and getter methods. Thankfully Ruby provides an easy way to write such setter and getter methods without the need of writing each method definition. You remember that we said that in OOP many words can be used to describe similar things. The word attribute is an equivalent of state. Based on this terminology, getter method can be considered as state-reader or attribute-reader and setter methods can be seen as state-writer or attribute-writer. In Ruby, attribute is a term for a specific configuration of methods and instance variables that provide useful shortcut to write both attribute-reader / -writer methods easily (David Black 2014). These methods are critical to create object attributes which store object’s state (data) in instance variables.
In our above example you can see that instead of classically defining setter or getter methods we used the class methods
attr_accessors that permit to both read and write object data.
Encapsulating data in Class
Until now, we’ve only considered states that are accessed through objects. However, object states and methods are sometimes not sufficient to provide an appropriate mechanism for controlling data associated with all objects of a particular class. Remember classes are also objects and as such they can contain states and encapsulate data. Let’s be concrete here. Remember our coffee machine. Let’s propose a simple implementation of the
Let’s now suppose that we want to keep track of the total number of drinks that the machine produced. Solving this particular question will put us in a situation where we need to follow the state of the coffee machine. Indeed our drinks do not record the count of drinks produced by the coffee machine. This information is not a property of a particular drink object but instead, it is a collective property of the coffee machine. Consequently, we should initialize a variable that is encapsulated at the class level. Ruby allows programmers to do so by using class variables. They are defined by
@@ . Let’s do this here:
The way the class is designed here allows to keep track of the number of drink sold by the machine. We initialized a class variable
@@number_drink and assign to it an integer
0. We then keep track of the number of sold drink by taking advantage of the
initialize method that will increment
@@number_drinkby 1 each time a object is created (each time a drink is sold in our context). Of course, because of encapsulation we need appropriate interfaces to access this kind of class-wide data: for that a class can also provide class methods through which its class state may be safely accessed and or modify (Damian Conway 1999). A class method is not called on a specific object, that is because a class state does not belong to any specific object. Consequently to call a class method we must specify both the class name and the method name (Damian Conway 1999). This is what we did in our example by defining the method
CoffeeMachine.number_sold_drink. These is also another way to define such method in Ruby which relies on the use of the keyword
self rather that writing the class’ name. I will not talk and dig the notion of
self here and invite you to have a look to other resources (OOP Launch School, David Black 2014).
Encapsulating what is already encapsulated using access control
We saw and discussed what was encapsulation. We now know that each object harbors a unique state and that interfaces / methods are needed to specifically access and modify these states. We also saw that classes can also encapsulate states and that class methods need to be defined in order to access such class-wide data. But if you remember we said previously that by default methods defined in a class are universally accessible. This can be dangerous somehow, especially if our program deals with sensitive data.
Let’s imagine you design a program for a bank. This program will generate basic data for each new client: names, ages, address, account number, password for online account, International Bank Account Number… you obviously do not want to make some of these details easily accessible. Although they will be tied to specific object (here the client), you want to avoid your program to unexpectedly access or change some of these data. In Ruby you can literally put your method definition in a private or protected ‘area’. In both cases this means that you will not be able to use these methods outside of the class and call these methods on your objects. Let’s see one example in our bank context.
In this example we define our setter and getter methods using the
attr_accessor method provided by Ruby. We initialize different instance variables that will contains the state of our object. As you can see
@iban, while being initialized is not passed as an argument in our
attr_accessor method defined in line 2. Instead we defined a new
attr_accessor method in line 17 after putting
private in line 15. Everything that comes after the
private statement will be considered as…private. These methods cannot be called on any object both inside and outside the class limiting their access to data through our program (Launch School, David Black 2014).
Protected methods is the kind / soft version of the private methods. You cannot invoke them on objects outside the class but you can do it inside the corresponding class in method definition. Let’s take an example. You design a method that can alert you when the amount of money of one of your client goes below a certain limit. You do not want this data to be accessible through the whole program but you do want to assess the amount and automatically compare it to some limit fixed by the bank. But because the safety and the protection of the data matters more, you first decide to go first for a private approach:
As you can see in line 24, we put our setter and getter methods in private. Then in line 30 and 31 we call the getter method
money on object. As expected and demonstrated before this raises a
NoMethodError message. Now in line 10 to 12 we define a method
below_limit? that compares the amount of money of the client to the limit set by the bank and return a Boolean. The limit is passed as an argument to this method. In the body of the method definition we can see that we are calling the getter method
self which in our case will the instantiated object (OOP Launch School). This raises a
NoMethodError message illustrating the fact that private methods indeed cannot be called on objects outside but also inside the class. As you will see below this situation fits perfectly with the use of protected methods.
This time in line 22, we used the
protected statement. We can see that calling the protected method
money directly on instantiated objects outside of the class does raise a
NoMethodError message. However the
alert method invocation does not raise any error and instead print the expecting results and allow our object to behave as expected. This demonstrates the ability to call protected methods on objects inside their class.
We covered here many concepts that we could summarize in straightforward definitions:
- In Ruby everything is an object. Objects are created by class that define the states and behaviours of their objects.
- Ruby and other OOP languages provide encapsulation. This means object attributes or data and methods of an object are associated specifically with that object. Additionally, this implies that the scope of those states and methods is by default the object itself (Hal Fulton and Andre Arko 2015).
- As a consequence of the previous statement, class-wide attributes assigned to class variables are accessible thank to class methods whereas object’s attributes assigned to instance variables are accessible thank to the use of instance methods.
- Finally the interface allows us to define the level of interaction with object states and behaviours. By default in Ruby, all interfaces or methods are public and universally available. Depending on the level of interaction we expect, we can use different approach by using the keywords private or protected.
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 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.
- Object-Oriented Software Construction (2nd Edition). Bertrand Meyer. Prentice Hall. ISBN-13: 978–0136291558. 1997.
- Object Oriented Programming with Ruby. Launch School. Available online (https://launchschool.com/books/oo_ruby).
- Well grounded Rubyist (2nd Edition). David A Black. Manning. ISBN 9781617291692. 2014.