A PIE - A Refresher on OOP Basics in 7 Minutes

Photo by Mink Mingle on Unsplash

A PIE - A Refresher on OOP Basics in 7 Minutes

All you need to know about the 4 pillars

Ask any programmer about the 4 fundamental concepts of object oriented programming, and they'll list A PIE - Abstraction, Polymorphism, Inheritance, and Encapsulation - without a moment's hesitation. After all, these are the core principles of OOP. Yet sometimes, we tend to forget the basics. And if you can't recall anything other than the names of these OOP 4 pillars, I've got you covered. Here's a complete refresher on OOP basics, from why OOP came into being to the relationship between objects.

Why was OOP introduced?

Before OOP came into being, computers used primitive data types to store information. However, they could store only one kind of data and that, too, single and simple values like byte, char, int, and boolean. But as program complexity increased, programmers needed some way to group together related variables, which led to the introduction of structures.

Structures could store different kinds of data, but it was impossible to define functions within them - you could only add references to them in a structure. This is when OOP, based on objects and classes instead of procedures and functions, was introduced. Simply put, an object is an instance of a class, while a class can be best understood as a template for objects.

The Four Pillars

The core fundamentals of OOP are inheritance, polymorphism, encapsulation, and abstraction. Let's discuss them one by one.

Inheritance

Just like we inherit some physical and behavioral characteristics from our parents, classes can also inherit behaviors and properties from their super or parent classes. In addition to establishing a relationship between multiple classes, inheritance also promotes code reusability.

There are 5 types of inheritance:

  • Single inheritance: The child inherits properties from a single class

  • Multiple inheritance: The child inherits from two base classes

  • Multi-level inheritance: A child class inherits from another class, which is also a subclass of some other class.

  • Hybrid: A combination of multiple & multi-level inheritance

  • Hierarchical: Where one base class has multiple subclasses

Despite the benefits that inheritance provides, it increases the effort and time needed to execute a program, makes the child & parent class tightly coupled and if you want to modify the program, then you'll have to change both the parent and the child class.

Polymorphism

Polymorphism refers to the ability of the object to exist in more than one form. It allows methods having the same name to exist in the same class as well as in different classes. You can best understand polymorphism with a real-world example - A man can be a husband, a father, and an employee at one time.

Polymorphism is of two kinds: Static and dynamic.

Static polymorphism

In static polymorphism, information is collected to call a method at compile time and involves early binding (offers compile-time checking for types to prevent implicit casts). One way to demonstrate or implement static polymorphism is with method overloading.

Method overloading involves multiple methods with the same name but different arguments (different method signatures) that are defined in the same class. The methods are differentiated by the number, types, and order of arguments. An example can help you understand this better. Let's say we have a method drive in the Car class, and it can be differentiated by:

  • Different numbers of parameters - drive(int speed, string dest)

  • Different types of parameters - drive(int speed, int dist)

  • Different order of parameters - drive(string dest, int speed)

Now, the method that will be called will depend on the parameters that you pass to it. So for instance, if you call drive('School', 60), then the third method will be called.

Dynamic polymorphism

In dynamic polymorphism, information is collected to call a method at run time and involves late binding (checks the type after the object is created or when some action is performed on it). One way to implement dynamic polymorphism is with method overriding, which is when a method signature is present in both the subclass and the superclass. The two methods have the same name, but their implementation differs - the implementation of the subclass overrides that of the superclass, and is determined dynamically as the program is run.

Method overriding allows you to write methods in the superclass without including conditionals like ifs & else-ifs to account for which subclass is being used when you call the method.

Let's consider an example.

Say you have a parent class car and a subclass sportsCar both with a .drive() method that takes in the number of miles driven. The drive method for the parent class calculates the speed by multiplying the miles by 0.06 while the .drive() method in the subclass calculates it by multiplying the miles by 0.03. In your program, the one that'll run will depend on which one you call, i.e., car.drive(miles) or sportsCar(miles).

Encapsulation

Encapsulation involves binding the data and methods in a single unit and hiding the two within a class to prevent anything outside from interacting with it directly. This is done through access specifiers, which we'll get to in just a bit. The least access there is to an object, the fewer implementation details are leaked, leading to looser coupling, and vice versa.

A note on coupling: Coupling is essentially the degree of interdependence between a couple of software modules. Tightly coupled modules are closely connected so changes in one module also affect the other. Meanwhile, loosely coupled modules are independent and changes in one module have a minimal impact on the others.

It's important not to allow external classes to directly edit the attribute of an object, especially if you're working on a large, complex program with other developers. Each piece of code shouldn't have access to or rely on the inner workings of some other sections of code. This will help you keep control of your program and prevent it from getting too complicated or ending up in unwanted and strange states.

What are access specifiers?

Access specifiers are essentially keywords used to determine the accessibility of the methods and classes and allow you to implement encapsulation. There are basically three keywords that define the way members are accessed outside the scope of the class. These are:

  1. Public: Public members can be accessed from outside the class

  2. Private: Private members cannot be accessed from outside the class. It is possible to create multiple private members that have the same name in different locations so that they don't conflict with each other.

  3. Protected: Protected members cannot be accessed from outside the class unless it's a derived class (i.e., you can access them in inherited classes).

Abstraction

Abstraction refers to displaying only the important data or hiding the details (data abstraction) and hiding the implementation details (control abstraction). The most famous example of abstraction is that of a car - you can accelerate a car without knowing what goes on inside the engine to make the car go faster.

Abstraction is achieved using interfaces. You can think of an interface as a structure that allows the computer to enforce particular properties on a class. So for instance, imagine you have a class, truck, and bus truck with an accelerate() function. How each vehicle accelerates is left for the class to determine, but the fact that they have to have the accelerate() function is what the interface is responsible for.

More about interfaces

Present within the interface are functions that must be part of the objects that follow the interface. Remember, there shouldn't be any data variables inside the interface; only function prototypes are allowed. Letting classes interact with a pre-determined interface makes sure that different parts of the program don't become tightly coupled. If classes are too entangled, even a small change can cause a ripple effect, leading to more changes. But with an interface that classes can use to interact with one another ensures that each piece can be developed individually.

Relationship Between Objects

That's pretty much all you need to know about the four pillars. But it's also important to touch up on the relationship between objects. There are essentially three kinds of relationships:

  1. Association: This is a kind of relationship between objects where there's no owner and objects have their own lifetime, and can be created or destroyed independently. A simple example is of a doctor - he can be associated with many patients, and a patient can visit as many doctors as he likes.

  2. Aggregation: An aggregation is essentially a kind of association where an object has its own lifecycle, but there's also some ownership involved. It can denote a parent/child relationship, but it might not denote physical containment. In other words, the parent can exist even without a child. For example, an employee is part of a department and if the department is demolished, the employee will still continue to live on.

  3. Composition: This is a kind of association where the child is also destroyed when the parent is destroyed, or when a class owns other classes that cannot exist once the owner is destroyed. For example, a house can have many rooms, but the rooms cannot exist on their own if the house is destroyed.

And that's pretty much all the basics of OOP. If I missed something, let me know!