# Object-Oriented Programming (OOP) Explained: A Complete Guide

Object-Oriented Programming System ([OOPs](https://www.geeksforgeeks.org/python-oops-concepts/)) is a powerful programming paradigm that uses objects and classes to design and [develop software](https://bytescrum.com/). This approach allows for better organization, modularity, and reuse of code. In this blog, we will cover the foundational and advanced concepts of OOPs, including classes, objects, constructors, destructors, encapsulation, inheritance, polymorphism, abstraction, and advanced OOPs principles.

## Basic Concepts of OOPs

### 1\. Classes and Objects

#### **Classes**

A class is a blueprint or template for creating objects. It defines the attributes (data) and methods (functions) that the objects created from the class will have.

**Example:**

```python
class Car:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year
    
    def display_info(self):
        print(f"Car: {self.year} {self.make} {self.model}")
```

**Objects**

Objects are instances of classes. When a class is defined, no memory is allocated until an object of that class is created.

**Example:**

```python
my_car = Car("Toyota", "Corolla", 2020)
my_car.display_info()  # Output: Car: 2020 Toyota Corolla
```

### 2\. Constructors and Destructors

#### **Constructors**

A constructor is a special method called when an object is instantiated. It is used to initialize the object's state.

**Example:**

```python
class Car:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year
```

#### **Destructors**

A destructor is a special method called when an object is destroyed. It is used to clean up resources. In Python, the `__del__` method acts as a destructor.

**Example:**

```python
class Car:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year
    
    def __del__(self):
        print(f"Car {self.make} {self.model} is being destroyed")
```

### 3\. Encapsulation

Encapsulation is the mechanism of wrapping the data (variables) and code (methods) together as a single unit. It restricts direct access to some of an object's components, which is a means of preventing accidental interference and misuse of the data.

**Example:**

```python
class Account:
    def __init__(self, owner, balance):
        self.owner = owner
        self.__balance = balance  # Private attribute
    
    def deposit(self, amount):
        self.__balance += amount
    
    def withdraw(self, amount):
        if amount <= self.__balance:
            self.__balance -= amount
        else:
            print("Insufficient funds")
    
    def get_balance(self):
        return self.__balance

acct = Account("John Doe", 1000)
acct.deposit(500)
print(acct.get_balance())  # Output: 1500
```

### 4\. Inheritance

Inheritance is a mechanism where a new class inherits the attributes and methods of an existing class. The class that is inherited from is called the parent or base class, and the class that inherits is called the child or derived class.

**Example:**

```python
class Animal:
    def __init__(self, name):
        self.name = name
    
    def speak(self):
        pass

class Dog(Animal):
    def speak(self):
        return "Woof!"

class Cat(Animal):
    def speak(self):
        return "Meow!"

dog = Dog("Buddy")
cat = Cat("Whiskers")
print(dog.speak())  # Output: Woof!
print(cat.speak())  # Output: Meow!
```

### 5\. Polymorphism

Polymorphism allows objects of different classes to be treated as objects of a common superclass. It is the ability to redefine methods for derived classes.

**Example:**

```python
class Bird:
    def fly(self):
        print("Flying")

class Sparrow(Bird):
    def fly(self):
        print("Sparrow is flying")

class Ostrich(Bird):
    def fly(self):
        print("Ostriches can't fly")

def make_fly(bird):
    bird.fly()

sparrow = Sparrow()
ostrich = Ostrich()

make_fly(sparrow)  # Output: Sparrow is flying
make_fly(ostrich)  # Output: Ostriches can't fly
```

### 6\. Abstraction

Abstraction is the concept of hiding the complex implementation details and showing only the essential features of the object. It helps in reducing programming complexity and effort.

**Example:**

```python
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass
    
    @abstractmethod
    def perimeter(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height
    
    def area(self):
        return self.width * self.height
    
    def perimeter(self):
        return 2 * (self.width + self.height)

rect = Rectangle(4, 7)
print(f"Area: {rect.area()}")        # Output: Area: 28
print(f"Perimeter: {rect.perimeter()}")  # Output: Perimeter: 22
```

## Advanced OOPs Concepts

### 1\. Multiple Inheritance

Multiple inheritance allows a class to inherit from more than one base class. This can be useful but can also introduce complexity and ambiguity, particularly with the diamond problem.

**Example:**

```python
class Animal:
    def eat(self):
        print("Eating")

class Bird(Animal):
    def fly(self):
        print("Flying")

class Fish(Animal):
    def swim(self):
        print("Swimming")

class FlyingFish(Bird, Fish):
    def fly_swim(self):
        self.fly()
        self.swim()

ff = FlyingFish()
ff.eat()  # Output: Eating
ff.fly_swim()  # Output: Flying \n Swimming
```

### 2\. Mixins

Mixins are a form of multiple inheritance where the classes being inherited from are not meant to stand alone but provide additional functionality to the derived class.

**Example:**

```python
class Loggable:
    def log(self, msg):
        print(f"Log: {msg}")

class Saveable:
    def save(self):
        print("Data saved")

class Account(Loggable, Saveable):
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance

acct = Account("John Doe", 1000)
acct.log("Account created")  # Output: Log: Account created
acct.save()  # Output: Data saved
```

### 3\. Method Overriding and Super Calls

Method overriding allows a child class to provide a specific implementation of a method that is already defined in its parent class. The `super()` function is used to call the method of the parent class.

**Example:**

```python
class Parent:
    def greet(self):
        print("Hello from Parent")

class Child(Parent):
    def greet(self):
        super().greet()
        print("Hello from Child")

child = Child()
child.greet()
# Output:
# Hello from Parent
# Hello from Child
```

### 4\. SOLID Principles

The SOLID principles are a set of design principles intended to make software designs more understandable, flexible, and maintainable.

#### **Single Responsibility Principle (SRP)**

A class should have only one reason to change, meaning it should have only one job or responsibility.

**Example:**

```python
class Order:
    def __init__(self, items):
        self.items = items
    
    def calculate_total(self):
        return sum(item.price for item in self.items)

class OrderPrinter:
    def print_order(self, order):
        for item in order.items:
            print(f"{item.name}: {item.price}")

# Order class is responsible for order management
# OrderPrinter class is responsible for printing the order
```

#### **Open/Closed Principle (OCP)**

Software entities should be open for extension but closed for modification.

**Example:**

```python
class Discount:
    def apply_discount(self, total):
        pass

class TenPercentDiscount(Discount):
    def apply_discount(self, total):
        return total * 0.9

class Order:
    def __init__(self, items, discount):
        self.items = items
        self.discount = discount
    
    def calculate_total(self):
        total = sum(item.price for item in self.items)
        return self.discount.apply_discount(total)
```

#### **Liskov Substitution Principle (LSP)**

Objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program.

**Example:**

```python
class Bird:
    def fly(self):
        pass

class Sparrow(Bird):
    def fly(self):
        print("Sparrow is flying")

class Ostrich(Bird):
    def fly(self):
        raise Exception("Ostriches can't fly")

def make_fly(bird: Bird):
    bird.fly()

# Here, make_fly should work with any Bird subclass without error.
```

#### **Interface Segregation Principle (ISP)**

Clients should not be forced to depend on interfaces they do not use.

**Example:**

```python
class Printer:
    def print(self, document):
        pass

class Scanner:
    def scan(self, document):
        pass

class MultiFunctionDevice(Printer, Scanner):
    def print(self, document):
        print(f"Printing: {document}")
    
    def scan(self, document):
        print(f"Scanning: {document}")
```

#### **Dependency Inversion Principle (DIP)**

High-level modules should not depend on low-level modules. Both should depend on abstractions.

**Example:**

```python
class Database:
    def get_data(self):
        pass

class MySQLDatabase(Database):
    def get_data(self):
        return "MySQL Data"

class BusinessLogic:
    def __init__(self, database: Database):
        self.database = database
    
    def process_data(self):
        data = self.database.get_data()
        print(f"Processing {data}")

db = MySQLDatabase()
logic = BusinessLogic(db)
logic.process_data()
# Output: Processing MySQL Data
```

### 5\. Design Patterns

Design patterns are typical solutions to common problems in software design. They are like pre-made blueprints that you can customize to solve a recurring design problem in your code. There are three main types of design patterns: creational, structural, and behavioral.

#### **Singleton Pattern**

Ensures a class has only one instance and provides a global point of access to it.

**Example:**

```python
class Singleton:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

singleton1 = Singleton()
singleton2 = Singleton()

print(singleton1 is singleton2)  # Output: True
```

#### **Factory Pattern**

Defines an interface for creating an object but lets subclasses alter the type of objects that will be created.

**Example:**

```python
class Dog:
    def speak(self):
        return "Woof!"

class Cat:
    def speak(self):
        return "Meow!"

class AnimalFactory:
    @staticmethod
    def create_animal(animal_type):
        if animal_type == "dog":
            return Dog()
        elif animal_type == "cat":
            return Cat()

dog = AnimalFactory.create_animal("dog")
print(dog.speak())  # Output: Woof!
```

#### **Observer Pattern**

Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

**Example:**

```python
class Subject:
    def __init__(self):
        self._observers = []

    def attach(self, observer):
        self._observers.append(observer)

    def notify(self, message):
        for observer in self._observers:
            observer.update(message)

class Observer:
    def update(self, message):
        pass

class ConcreteObserver(Observer):
    def update(self, message):
        print(f"Received message: {message}")

subject = Subject()
observer1 = ConcreteObserver()
observer2 = ConcreteObserver()

subject.attach(observer1)
subject.attach(observer2)

subject.notify("Hello Observers!")
# Output:
# Received message: Hello Observers!
# Received message: Hello Observers!
```

---

<details data-node-type="hn-details-summary"><summary>Conclusion</summary><div data-type="detailsContent">Object-Oriented Programming System is a robust paradigm that provides a clear modular structure for programs. It is particularly useful for managing large, complex software projects. Understanding and applying the principles and patterns of OOPs can lead to more maintainable, scalable, and reusable code. By mastering OOPs concepts such as classes, objects, inheritance, polymorphism, encapsulation, and abstraction, along with advanced techniques like SOLID principles and design patterns, you can create well-structured and efficient software systems.</div></details>

I hope it is not boring 😀. Do comment and share!
