Understanding the Four Core Concepts of Object-Oriented Programming

A popular interview question concerns the four core concepts in object-oriented programming. These concepts are encapsulation, abstraction, inheritance, and polymorphism. Let's look at each of these concepts.

The Old Way: Procedural Programming

Before object-oriented programming, we had procedural programming that divided a program into a set of functions. In that model, we have data stored in a bunch of variables and functions that operate on the data. This style of programming is very simple and straightforward; often, it's what you learn as part of your first programming subject at a university.

However, as your programs grow, you will end up with a bunch of functions that are all over the place. You might find yourself copying and pasting lines of code over and over. When you make a change to one function, several other functions break. That's what we call "spaghetti code" because there is so much interdependence between all these functions it becomes problematic.

Object-oriented programming (OOP) came to solve this problem.

A Better Approach: Object-Oriented Programming

In object-oriented programming, we combine a group of related variables and functions into a unit. We call that unit an object. We refer to these variables as properties and the functions as methods.

Here's an example: think of a car. A car is an object with properties such as make, model, and color, and methods like start(), stop(), and move().

For a more practical programming example, think of the localStorage object in your browser. Every browser has a localStorage object that allows you to store data locally. This localStorage object has a property like length, which returns the number of objects in the storage, and methods like setItem() and removeItem().

So, in object-oriented programming, we group related variables and functions that operate on them into objects. This is the first core concept, known as encapsulation.

1. Encapsulation: Bundling Data and Methods

Let me show you an example of encapsulation in action. Here we have several variables: baseSalary, overtime, and rate. Below these, we have a function to calculate the wage for an employee. We refer to this kind of implementation as procedural—we have variables on one side and functions on the other, and they are decoupled.

Procedural Example: ```javascript let baseSalary = 30000; let overtime = 10; let rate = 20;

function getWage(baseSalary, overtime, rate) { return baseSalary + (overtime * rate); } ```

Now, let's take a look at the object-oriented way to solve this problem. We can have an employee object with three properties (baseSalary, overtime, and rate) and one method (getWage).

Object-Oriented Example: javascript let employee = { baseSalary: 30000, overtime: 10, rate: 20, getWage: function() { return this.baseSalary + (this.overtime * this.rate); } }; employee.getWage();

Why is this better? First of all, look at the getWage function in the object-oriented example. This function has no parameters. In contrast, in our procedural example, our getWage function has multiple parameters. The reason we don't have any parameters in the OOP implementation is because these parameters are modeled as properties of the object. All these properties and the getWage function are highly related, so they are part of one unit.

One of the symptoms of procedural code is functions with too many parameters. When you write code in an object-oriented way, your functions end up having fewer and fewer parameters. As "Uncle Bob" Martin says, "The best functions are those with no parameters." The fewer the number of parameters, the easier it is to use and maintain that function.

2. Abstraction: Hiding Complexity

Think of a DVD player as an object. This DVD player has a complex logic board on the inside and a few buttons on the outside that you interact with. You simply press the "play" button and you don't care what happens on the inside. All that complexity is hidden from you. This is abstraction.

In practice, we can use the same technique in our objects. We can hide some of the properties and methods from the outside, which gives us a couple of benefits:

  • Simpler Interface: It makes the interface of those objects simpler. Using and understanding an object with a few properties and methods is easier than an object with several properties and methods.
  • Reduced Impact of Change: It helps us reduce the impact of change. Let's imagine that tomorrow we change these inner or private methods. These changes will not leak to the outside because no external code touches these methods. We may delete a method or change its parameters, but none of these changes will impact the rest of the application's code.

With abstraction, we reduce complexity and isolate the impact of change.

3. Inheritance: Eliminating Redundancy

The third core concept in object-oriented programming is inheritance. Inheritance is a mechanism that allows you to eliminate redundant code.

For example, think of HTML elements like text boxes, drop-down lists, and checkboxes. All these elements have a few things in common. They should have properties like hidden and innerHTML, and methods like click() and focus().

Instead of redefining all these properties and methods for every type of HTML element, we can define them once in a generic object, call it HTMLElement, and have other objects inherit these properties and methods. Inheritance helps us eliminate redundant code.

4. Polymorphism: One Interface, Many Forms

Finally, we have polymorphism. "Poly" means many, and "morph" means form, so polymorphism means "many forms." In object-oriented programming, polymorphism is a technique that allows you to get rid of long if-else or switch-case statements.

Back to our HTML elements example: all these objects should have the ability to be rendered on a page, but the way each element is rendered is different from the others. If you want to render multiple HTML elements in a procedural way, your code would probably look something like this:

Procedural Rendering (with a switch-case): javascript switch (element.type) { case 'TextBox': renderTextBox(element); break; case 'DropDownList': renderDropDownList(element); break; case 'CheckBox': renderCheckBox(element); break; }

With object-orientation, we can implement a render method in each of these objects, and the render method will behave differently depending on the type of the object you're referencing. So, we can get rid of this nasty switch-case block and use one line of code like this:

Polymorphic Rendering: javascript element.render();

Key Benefits of Object-Oriented Programming

To summarize, here are the main benefits of OOP:

  • Encapsulation: We group related variables and functions together, reducing complexity and increasing reusability.
  • Abstraction: We hide the details and complexity and show only the essentials. This reduces complexity and isolates the impact of changes in the code.
  • Inheritance: We can eliminate redundant code.
  • Polymorphism: We can refactor ugly switch-case statements into cleaner, more maintainable code.