I'm more into functional programming, but based on my tasks, I use OOP principles and such. It's really interesting and fun. It was my first year of uni when I was introduced to this concept called OOP. It was something wow and new for me. But at the same time, what the heck was that? I wasn't satisfied with what I was taught. So, after learning more about it and practicing, I finally got an idea about this programming paradigm. I also wanted to share it with you by explaining it in simple words as much as possible. And YOU! At least once in your life, during an interview, you will be asked annoying questions like "What are OOP principles?" and such. So, let's move on and dive into it.
Okay, but what it's OOP?
TL;DR: OOP is a programming paradigm based on the concept of "objects," which can contain data (attributes) and code (methods). I guess this one sentence was enough to describe it and answer questions like "What the heck is OOP?", but since our blog is about OOP for dummies, we will go deeper and bring some analogies from the real-life world to explain it as easily as possible.
For the coding part, I first wanted to show with pseudocode, but decided to code in TS. Also, for analogies, we will take analogies of Vehicles. π
Object-Oriented Programming (OOP) is a programming paradigm that uses "objects" to design applications and computer programs. These objects are instances of classes, which are templates that define the data and behavior associated with the objects. OOP is based on several core principles, aiming to provide a clear modular structure for programs which makes it good for defining abstract data types, reusability, scalability, and maintenance of code.
Do we really need OOP?
So it depends! It depends on your task and what you're working on. In general, here are some good reasons why you should use it if it's needed:
- Modularity: OOP breaks down a software program into discrete objects that encapsulate both data and behaviors. This modularity makes it easier to manage complexity in large applications.
- Reusability: Objects and classes can be reused across different parts of a program or in different programs. This reduces redundancy and development time.
- Scalability and Maintainability: OOP's structure allows for easy maintenance and updates. Since objects can be modified independently, changes can be made without affecting the entire system.
- Abstraction: OOP allows for abstracting complex real-world problems into manageable code entities, making it easier to conceptualize and solve problems.
The Four Pillars of OOP
The four main principles (or pillars) of Object-Oriented Programming (OOP) are encapsulation, abstraction, inheritance, and polymorphism. These principles provide a framework for designing and building software that is modular, maintainable, and reusable. Each of these principles addresses different aspects of software design and helps solve common problems encountered in software development.
Inheritance π°
Inheritance in programming is like inheriting traits from your parents. Just like you might have your mom's smile or your dad's knack for telling bad jokes, in programming, classes can inherit features from other classes. This means you don't have to start from scratch every timeβyou can build on what's already there. It's like a free upgrade for your code!
In simpler terms, inheritance lets one class take on the properties and methods of another. Think of it as creating a blueprint (the parent class) and then making variations of it (the child classes). These child classes get all the cool stuff from the parent class but can also have their own unique features.
Now let's talk with analogies from real-life. Imagine you have a basic vehicle class. This vehicle class has some common features like make, model, and a method to start the vehicle. Now, you want to create specific types of vehicles like cars, trucks or motorcycles. Instead of rewriting the wheel (pun intended), you just inherit from the vehicle class and add specific features to each type.
// Parent class
class Vehicle {
make: string;
model: string;
constructor(make: string, model: string) {
this.make = make;
this.model = model;
}
start(): void {
console.log(`${this.make} ${this.model} is starting.`);
}
}
// Child class - Car
class Car extends Vehicle {
numberOfDoors: number;
constructor(make: string, model: string, numberOfDoors: number) {
super(make, model); // Call the parent class constructor
this.numberOfDoors = numberOfDoors;
}
honk(): void {
console.log("Honk! Honk!");
}
}
// Child class - Truck
class Truck extends Vehicle {
payloadCapacity: number;
constructor(make: string, model: string, payloadCapacity: number) {
super(make, model); // Call the parent class constructor
this.payloadCapacity = payloadCapacity;
}
tow(): void {
console.log(`${this.make} ${this.model} is towing a load of ${this.payloadCapacity} tons.`);
}
}
// Usage
const myCar = new Car("Toyota", "Corolla", 4);
myCar.start(); // Output: Toyota Corolla is starting.
myCar.honk(); // Output: Honk! Honk!
const myTruck = new Truck("Ford", "F-150", 3);
myTruck.start(); // Output: Ford F-150 is starting.
myTruck.tow(); // Output: Ford F-150 is towing a load of 3 tons.
TL;DR: Inheritance allows classes to inherit properties and methods from other classes, making code reusable and organized.
Encapsulation π
Encapsulation is like having a secret stash of candy that no one else can touch. You decide when and how much to share. In OOP, encapsulation is about bundling data and methods that operate on that data within a single unit, like a class. It's like putting your code in a protective bubble wrap so that other parts of your program can't accidentally mess with it.
Think of a car π. You don't need to know how the engine works or how to fix it to drive the car. You just turn the key, press the pedals, and steer the wheel. That's encapsulation! The complex inner workings are hidden, and you only interact with the simple controls.
In programming, encapsulation helps us write cleaner, more secure code. We can control access to data using things like access modifiers (public, private, protected).
Here's an example in TypeScript:
class Car {
private fuelLevel: number;
constructor(private make: string, private model: string) {
this.fuelLevel = 0;
}
public getFuelLevel(): number {
return this.fuelLevel;
}
public setFuelLevel(amount: number): void {
if (amount > 0) {
this.fuelLevel += amount;
console.log(`Added ${amount} units of fuel. Current fuel level: ${this.fuelLevel}`);
}
}
public drive(): void {
if (this.fuelLevel > 0) {
console.log(`${this.make} ${this.model} is driving...`);
this.fuelLevel--;
} else {
console.log(`Out of fuel! Please add fuel to your ${this.make} ${this.model}.`);
}
}
}
// Usage
const myCar = new Car("Honda", "Civic");
console.log(myCar.fuelLevel); // Error: Property 'fuelLevel' is private and only accessible within class 'Car'.
myCar.setFuel(10); // Added 10 units of fuel. Current fuel level: 10
myCar.drive(); // Honda Civic is driving...
console.log(myCar.getFuelLevel()); // 9
In this example, the fuelLevel property is private, meaning it can only be accessed and modified within the Car class. We provide public methods like getFuelLevel() and setFuelLevel() to allow controlled access to this private data. This way, we can ensure that the fuel level is never set to an invalid value.
TL;DR: Encapsulation is about wrapping data and methods that work on that data within a single unit, and controlling access to that unit. It helps create maintainable, secure code.
Polymorphism π
Polymorphism sounds like a big, scary word, but it's actually a pretty simple concept. In fact, you probably use polymorphism all the time without even realizing it! It's like being able to use the same remote control for your TV, DVD player, and streaming device. One interface, multiple uses. πΊ
In OOP, polymorphism lets us treat objects of different classes that share a common parent class as if they were the same type. It's like a chameleon that can change its appearance to blend in with its surroundings.
Let's say you're organizing a race ποΈ. You have different types of vehicles - cars, trucks, and motorcycles. They all have different properties and behaviors, but they all share the ability to race. In your code, you could have a Vehicle parent class with a race() method, and each child class (Car, Truck, Motorcycle) could implement its own version of race(). Polymorphism allows you to treat all these vehicles as a single type (Vehicle) and call the race() method on any of them, without worrying about their specific type. The right version of race() will be called automatically based on the actual object type.
class Vehicle {
constructor(protected speed: number) {}
race(): void {
console.log(`Racing at ${this.speed} mph!`);
}
}
class Car extends Vehicle {
constructor(speed: number) {
super(speed);
}
race(): void {
console.log(`The car is racing at ${this.speed} mph!`);
}
}
class Truck extends Vehicle {
constructor(speed: number) {
super(speed);
}
race(): void {
console.log(`The truck is lumbering along at ${this.speed} mph!`);
}
}
class Motorcycle extends Vehicle {
constructor(speed: number) {
super(speed);
}
race(): void {
console.log(`The motorcycle is zooming at ${this.speed} mph!`);
}
}
// Usage
const vehicles: Vehicle[] = [
new Car(120),
new Truck(80),
new Motorcycle(150)
];
vehicles.forEach(vehicle => vehicle.race());
/*
Output:
The car is racing at 120 mph!
The truck is lumbering along at 80 mph!
The motorcycle is zooming at 150 mph!
*/
In this example, we have a Vehicle parent class and three child classes: Car, Truck, and Motorcycle. Each child class has its own implementation of the race() method. We create an array of Vehicle objects, but we're actually storing objects of the child classes. When we call race() on each vehicle, the appropriate version of the method is called based on the actual object type. That's polymorphism in action!
Polymorphism helps us write more flexible, maintainable code. We can add new types of vehicles without changing the code that uses them, as long as they follow the same interface defined by the parent class.
TL;DR: Polymorphism allows objects of different classes to be treated as objects of a common parent class. It enables you to write flexible, extensible code.
Abstraction π¨
Imagine you're driving a car. You don't need to know how the engine works or how the transmission shifts gears. All you need to know is how to use the gas pedal, brake pedal, and steering wheel. That's abstraction! It's hiding the complex details and showing you only what you need to know.
In OOP, abstraction is all about representing essential features without including the background details. It's like a restaurant menu - it tells you what dishes are available, but it doesn't tell you how the chef prepares them.
Abstraction helps us manage complexity by breaking it down into smaller, more manageable parts. We create abstract classes or interfaces that define a common structure and behavior for a group of related objects. Then, we can create concrete classes that inherit from these abstractions and provide their own specific implementations.
Let's say we're building a vehicle simulator. We can create an abstract Vehicle class that defines the common properties and methods for all vehicles, like make, model, and start(). Then, we can create concrete classes like Car, Truck, and Motorcycle that inherit from Vehicle and provide their own specific properties and methods.
Here's how it might look in TypeScript:
abstract class Vehicle {
constructor(protected make: string, protected model: string) {}
abstract start(): void;
getMakeAndModel(): string {
return `${this.make} ${this.model}`;
}
}
class Car extends Vehicle {
constructor(make: string, model: string) {
super(make, model);
}
start(): void {
console.log(`The ${this.getMakeAndModel()} car is starting!`);
}
}
class Truck extends Vehicle {
constructor(make: string, model: string) {
super(make, model);
}
start(): void {
console.log(`The ${this.getMakeAndModel()} truck is starting!`);
}
}
// Usage
const car = new Car("Toyota", "Camry");
const truck = new Truck("Ford", "F-150");
car.start(); // The Toyota Camry car is starting!
truck.start(); // The Ford F-150 truck is starting!
In this example, we have an abstract Vehicle class that defines the common make and model properties, as well as an abstract start() method that all vehicles must implement. It also has a concrete getMakeAndModel() method that returns the make and model of the vehicle.
The Car and Truck classes inherit from Vehicle and provide their own implementations of the start() method. They can also use the getMakeAndModel() method from the parent class.
By using abstraction, we can write code that is more modular, easier to understand, and easier to maintain. We can focus on the high-level concepts and leave the low-level details to the specific implementations.
TL;DR: Abstraction is a way to manage complexity by breaking things down into smaller, more manageable pieces. In OOP, we use abstract classes and interfaces(not gonna go into detail about their differences in this blog) to define common structures and behaviors for related objects.
Conclusion
Phew, that was quite a journey through the world of Object-Oriented Programming! We've explored the four pillars of OOP - Encapsulation, Abstraction, Inheritance, and Polymorphism - and hopefully, I've been able to shed some light on these concepts in a fun and easy-to-understand way.
OOP is a powerful programming paradigm that has many real-world applications. It's used in a wide range of software development, from mobile apps and video games to enterprise software and scientific simulations. By understanding and applying the principles of OOP, you can write code that is more modular, reusable, and maintainable - all essential skills for any aspiring software developer.
We could have gone into much more detail about each of the four pillars, but I wanted to keep things as simple and beginner-friendly as possible. Consider this your introduction to OOP - you've got the basics down, and now you're ready to explore further and build amazing things!
In summary, OOP is an essential concept in programming that every developer should understand. By mastering the four pillars of OOP, you'll be able to write better, more organized code that is easier to maintain and extend. It may take some practice to fully grasp these concepts, but the effort is well worth it in the long run.
Happy coding! π