Object-Oriented Programming: Encapsulation, Polymorphism, Inheritance
Object-Oriented programming is a programming-paradigm revolving around the definition of objects that send messages to each other. These objects also contain it’s own public and internal interfaces, which I will highlight later in this article.
To successfully implement an object-oriented design, one must carefully and intuitively design around 4 basic pillars, these are known as the 4 Pillars of Object-Oriented Design:
Abstraction and encapsulation are very similar object-oriented concepts but differ in a few key areas. Abstraction allows engineers to think on a higher level, getting a ‘birds-eye view’ of a program without being bogged down by the underlying mechanics. This ‘higher-level’ perspective is referred to as the design-level. As opposed to encapsulation, abstraction tends to generalize things, hiding unwanted data and revealing necessary data. Encapsulation, on the other hand, focuses on the internal mechanisms-of-action for a program, and hiding those mechanisms (states/behaviors), using access modifiers.
An example of abstraction use is in programming languages. Launch School’s initial track was to use Ruby because it was tailored for an English-speaking human, rather than the computer. Writing in Ruby is more similar to writing in English, and a lot of the underlying mechanisms are hidden. This is accomplished because of the high levels of abstraction in Ruby. Lower-level languages do not contain much abstraction, so require the programmer to account for more of the underlying implementation-level attributes to make the code run.
Design-Level: High-level, 'birds eye' view of a program. The realm of abstraction, responsible for getting rid of unnecessary data that doesn't relate to the overall design of a program.Implementation-Level: Mechanics-layer. The data, states, and behavior that contribute to the functionality of a program. The realm of encapsulation that shields unnecessary data to the public interface.
Depending on the programming language, there are many different types of inheritance. Ruby, however, only allows for single inheritance between classes and uses what is called mixins when sub-classes to.
Single Inheritance: Sub-class/child class inherits from one super class.
Single Inheritance Example in Ruby:
In the above code, the
Dog class inherits the behaviors of the Animal class, and any object instantiated from
Dog can invoke the methods in
Animal . In this case, objects instantiated from
Dog the class have the constructor method from
Inheritance in Ruby helps to extract common behavior from multiple classes and moves them into a single super-class. Inheritance also considered abstraction, because we are removing repetitive code and placing it into a generalized unit that makes the program more clear about what it is doing, and the purposes behind certain behavior.
Another frame to view encapsulation is ‘bundling’. Encapsulation takes different classes, methods, and variables and bundles them up into a single unit for reference.
This bundling can help to lessen the cognitive load on the programmer, adding another layer of abstraction. Encapsulation can also hide states and behaviors from direct access, so programmers won’t accidentally modify states and behaviors.
Encapsulation is found in many niches of Ruby, some examples are in:
Variables, instance variables, in particular, can be encapsulated using the keywords
private , and
protected . The
private keyword hides data from the method’s public interface, making it unable to be directly accessed from outside of the class. Below is an example of encapsulation by the
private keyword in action:
In the above code, a single unit to contain the methods is created: the
Dog class. Once the class is defined, the unit’s public interface needs to be defined. The constructor method
initializeon line 2 and the instance method
to_s on line 6–8 are part of the
Dog class’s public interface.
All the states and method’s available for direct-access outside of the Dog class. It is the 'logical point which outside classes, objects, and methods interact.'
to_s classes are both accessible from outside the class. However, the getter method
name defined for line 12 is hidden from outside the class per the
private keyword. This is the final stage of encapsulation, restricting access from unnecessary public data within the unit.
Calling the getter method
NoMethodError is triggered, since there isn’t a method available from that scope. Additionally, the instance variable
name is hidden from outside the class, so the only way to access the dog’s name is through the
Encapsulation using the
protected keyword is similar to
private except the
public keyword allows for inter-class interaction between the states and behaviors defined below it. This is useful when you need two classes to share information, but you do not want that information on either of the class’ public interfaces.
Encapsulation occurs in many of the key players of Ruby. For example,
- Classes: Encapsulate methods.
- Methods: Encapsulate logic.
- Objects: Encapsulate state and behaviors.
Polymorphism, in short, is the ability for data to be represented in more than one way. Rubyists commonly implement polymorphism through two processes:
- Through inheritance.
- Through duck-typing.
Polymorphism through Inheritance:
So far, we have discussed the concept of single-inheritance in Ruby. However, there is one more form of inheritance that ruby uses: interface inheritance. Interface inheritance uses
mixins to include modules in the class, with each class inheriting from the interface provided by the module.
Polymorphism through Duck-Typing:
Duck typing is based on the saying ‘If it walks like a duck and acts like a duck, then it must be a duck.’ The focus is on what an object can do, rather than what it is. According to duck typing, one should treat objects according to the defined methods/behavior, rather than the inherited classes and mixed-in modules.
Example of Duck-Typing:
In the code above, all 4 classes:
Bird , and
Fish will have an attempt made to invoke the
make_noise instance method. However, on the final element of the array, the object of the
Fish class, there is not behavior for making a noise (Fish don’t make noises). Therefore, an error is raised and the code crashes.
Rather than creating super-classes for each one of the classes, we explicitly define the methods within each class without the use of inheritance. This type-system is primarily for programmers who are more concerned with convenience rather than type security. This is one form of polymorphic behavior due to the
make_noise method invocation generating different outcomes, or nothing at all, depending on the object’s class.
Knowing and practicing the 4 pillars of object-oriented design are critical in developing a high proficiency in software design. Mastering these concepts allows for software systems to become more flexible, adaptable, and legible. These principles are not just applicable to Ruby, but span across many different programming languages, as most languages support the object-oriented paradigm.
Many thanks to Karl Lingiah for the support and guidance in learning these pillars and for the content supplied in this article, particularly in the pillars of encapsulation and abstraction.
More thanks to the friendly and helpful staff at Launch School for helping me begin my first steps in the long road from beginner to master.