Goglides Dev 🌱

Cover image for Understanding Object-Oriented Programming (OOPs) Concepts With Examples
Roshan Thapa
Roshan Thapa

Posted on

Understanding Object-Oriented Programming (OOPs) Concepts With Examples

What is Object Oriented Programming?
Object oriented programming (OOPs) is a programming paradigm where programs are designed around data or objects rather than functions and logic.

OOP can be seen as an evolution or extension of procedural programming. Many early programming languages, such as C, followed a procedural paradigm. With the advent of languages like C++ and Java, OOP gained popularity for its ability to model complex systems more intuitively and promote code reuse through inheritance and encapsulation.

The basic concepts of OOPs are:

  • Objects
  • Classes
  • Inheritance
  • Abstraction
  • Encapsulation
  • Polymorphism OOPs empowers software developers to craft systematically structured and reusable code blueprints, commonly referred to as classes, that can be utilized to build instances of objects.

Some popular programming languages that use the concepts of OOPs include Java, C++, Python, and JavaScript.

Objects in OOPs: Instances of Real-World Entities
The main building blocks of OOPs are objects. An object is an instance of a class that contains data and methods to manipulate that data.

For example:

class Person:

  def __init__(self, name, age):
    self.name = name
    self.age = age

  def greet(self):
    print("Hello, my name is " + self.name)

person1 = Person("John", 36)
person1.greet()

# Output: Hello, my name is John
Enter fullscreen mode Exit fullscreen mode

Here, person1 is an object of the Person class. It contains the data name and age, and methods like greet() to interact with the object.

Class Definition: Building Blocks of OOP
A class is like a blueprint for creating objects. It defines the attributes and behaviors of an object. Objects are created from classes through instantiation.

For example:


class Vehicle:

  def __init__(self, make, color):
    self.make = make
    self.color = color

  def drive(self):
     print("Driving", self.make)

car = Vehicle("Toyota", "grey")
car.drive()

# Output: Driving Toyota
Enter fullscreen mode Exit fullscreen mode

Here Vehicle is the class, and car is the object instantiated from that class.

Inheritance: Parent and Child Classes
Inheritance allows new child classes to be derived from existing parent classes. The child class inherits the attributes and behaviors from the parent.

For example:

class Animal:

  def eat(self):
    print("Eating...")


class Dog(Animal):

  def bark(self):
    print("Woof woof!")

dog = Dog()
dog.eat() # Inherited method
dog.bark() # Own method
Enter fullscreen mode Exit fullscreen mode

Here, Dog inherits from Animal and can access the eat() method. It also defines its own bark() method.

Abstraction: Abstract Classes and Methods
Abstraction refers to exposing only the required attributes and hiding the unnecessary implementation details from the user.

For example, a phone’s dial() method abstracts away the underlying network protocol required to make a call. The user just needs to call dial().

Another example:

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass
Enter fullscreen mode Exit fullscreen mode

Abstract classes cannot be instantiated and often serve as a blueprint for other classes. Abstract methods, like area above, must be implemented by any child class.

Abstraction helps reduce complexity and allows objects to share a uniform interface for usage. Abstract classes and interfaces help achieve abstraction.

Did you know you can try out hands on coding exercises on ReviewNPrep CodeHub.

Encapsulation: Protecting Implementation Details
Encapsulation refers to binding the data and functions together as a single unit called class. It prevents external code from accidentally modifying internal state data of an object.

For example, making fields private or protected in a class prevents external code from modifying them directly. Setter/getter methods should be used to access private fields.

class BankAccount:

  def __init__(self, balance):
    self.__balance = balance

  def get_balance(self):
    return self.__balance

account = BankAccount(1000)
print(account.__balance) # Error
print(account.get_balance()) # 1000
Enter fullscreen mode Exit fullscreen mode

Here, __balance is private, so it can’t be directly accessed. The get_balance() method is used to access it.

  1. Public Methods Public methods are accessible from outside the class and can be invoked by other classes or modules. They serve as the interface through which external code can interact with the object. Public methods are designed to provide meaningful functionalities and may involve operations on the object’s attributes.
class BankAccount:
    def __init__(self, balance):
        self.balance = balance

    def deposit(self, amount):
        """Public method to deposit money into the account."""
        self.balance += amount
        print(f"Deposit successful. New balance: {self.balance}")

    def withdraw(self, amount):
        """Public method to withdraw money from the account."""
        if amount <= self.balance:
            self.balance -= amount
            print(f"Withdrawal successful. New balance: {self.balance}")
        else:
            print("Insufficient funds.")
Enter fullscreen mode Exit fullscreen mode

In the example above, deposit and withdraw are public methods that allow external code to interact with the BankAccount object.

  1. Private Methods Private methods, on the other hand, are intended for internal use within the class and are not accessible from outside. They are prefixed with a double underscore (__) to indicate their private nature. Private methods are often used for utility functions or to perform operations that should not be directly exposed to external code.
class BankAccount:
    def __init__(self, balance):
        self.balance = balance

    def __validate_amount(self, amount):
        """Private method to validate the amount."""
        return amount > 0

    def deposit(self, amount):
        """Public method to deposit money into the account."""
        if self.__validate_amount(amount):
            self.balance += amount
            print(f"Deposit successful. New balance: {self.balance}")
        else:
            print("Invalid deposit amount.")

    def withdraw(self, amount):
        """Public method to withdraw money from the account."""
        if self.__validate_amount(amount) and amount <= self.balance:
            self.balance -= amount
            print(f"Withdrawal successful. New balance: {self.balance}")
        else:
            print("Invalid withdrawal amount or insufficient funds.")
Enter fullscreen mode Exit fullscreen mode

In this modified example, __validate_amount is a private method used to validate the deposit and withdrawal amounts. It ensures that the amount is positive before performing the transaction.

By using public and private methods strategically, encapsulation helps in maintaining a clear distinction between the external interface of a class and its internal implementation details, contributing to code robustness and security.

Polymorphism: Method Overloading and Method Overriding
Polymorphism allows objects to take on multiple forms. For example, the same function name can be used to perform different operations for different object types.

class Dog:

  def speak(self):
    print("Woof woof!")


class Cat:

  def speak(self):
    print("Meow")

def animal_speak(animal):
  animal.speak()

dog = Dog()
cat = Cat()

animal_speak(dog) # Woof woof!
animal_speak(cat) # Meow
Enter fullscreen mode Exit fullscreen mode

The same speak() method behaves differently depending on the object type. This allows for common interfaces and flexible code.

Compile-time Polymorphism (Static Polymorphism): This is achieved through method overloading, where multiple methods in the same class have the same name but different parameters.
Runtime Polymorphism (Dynamic Polymorphism): This is achieved through method overriding, where a method in a base class is redefined in a derived class.
For example:

#Compile-time or Static Polymorphism
class Calculator:
    def add(self, a, b):
        return a + b

    def add(self, a, b, c):
        return a + b + c

#Runtime Polymorphism (Dynamic Polymorphism)
class Animal:
    def make_sound(self):
        pass

class Dog(Animal):
    def make_sound(self):
        print("Woof!")
Enter fullscreen mode Exit fullscreen mode

Benefits of OOPs
*Modularity *– Objects encapsulate functionality into independent and interchangeable modules. This allows for easier troubleshooting and updates.
*Readability *– OOP principles promotes code readability by encapsulating related functionalities within classes, making code maintenance simple.

class Car:
    def __init__(self, brand, model):
        self.brand = brand
        self.model = model

    def display_info(self):
        print(f"{self.brand} {self.model}")
Enter fullscreen mode Exit fullscreen mode

Reusability – Code can be reused through inheritance, reducing duplication.

class DatabaseConnection:
    def __init__(self, connection_string):
        self.connection_string = connection_string

    def connect(self):
        pass

class UserDatabaseConnection(DatabaseConnection):
    def get_user(self, user_id):
        pass
Enter fullscreen mode Exit fullscreen mode

*Flexibility *– New objects can be created from existing classes through inheritance.
*Data abstraction *– Individual Objects expose high-level interfaces hiding low-level implementation details.
Putting Everything Together

# File: animal.py

from abc import ABC, abstractmethod

class Animal(ABC):
    def __init__(self, name, sound):
        self.name = name
        self.sound = sound

    @abstractmethod
    def make_sound(self):
        pass


# File: mammal.py

from animal import Animal

class Mammal(Animal):
    def give_birth(self):
        print(f"{self.name} is giving birth!")

    def make_sound(self):
        print(f"{self.name} says {self.sound}")


# File: bird.py

from animal import Animal

class Bird(Animal):
    def fly(self):
        print(f"{self.name} is flying!")

    def make_sound(self):
        print(f"{self.name} sings: {self.sound}")


# File: zoo.py

from mammal import Mammal
from bird import Bird

class Zoo:
    def __init__(self):
        self.animals = []

    def add_animal(self, animal):
        if isinstance(animal, Animal):
            self.animals.append(animal)
        else:
            print("Invalid animal!")

    def make_all_sounds(self):
        for animal in self.animals:
            animal.make_sound()


# File: main.py

from zoo import Zoo
from mammal import Mammal
from bird import Bird

# Creating instances of different animal classes
lion = Mammal("Lion", "Roar")
eagle = Bird("Eagle", "Caw")

# Creating a zoo and adding animals
zoo = Zoo()
zoo.add_animal(lion)
zoo.add_animal(eagle)

# Making all animals in the zoo produce their sounds
zoo.make_all_sounds()

# Demonstrating method overriding
lion.give_birth()

# Demonstrating encapsulation
# Uncommenting the line below will result in an error
# print(lion.sound)
Enter fullscreen mode Exit fullscreen mode

In this example:

The Animal class serves as an abstract base class with an abstract method make_sound.
The Mammal and Bird classes inherit from the Animal class, showcasing the concept of inheritance.
The Zoo class demonstrates the use of a collection of objects and encapsulates the logic for adding animals and making them produce sounds.
The main.py file ties everything together by creating instances of different animal classes, adding them to the zoo, and invoking methods to showcase polymorphism and encapsulation.
This zoo application exemplifies the power of OOP in modeling real-world scenarios. Complex programs are broken into multiple smaller units, promoting code reusability, and providing a clear and organized structure for complex systems.

Conclusion
Object-Oriented Programming introduces a structured approach to software development, allowing developers to model real-world scenarios more effectively. By employing concepts like inheritance, abstraction, and encapsulation, OOP enhances code organization, readability, and reusability.

Understanding these fundamental principles is crucial for building robust, scalable, and maintainable software systems. Incorporating OOP concepts into your development toolkit empowers you to tackle complex problems with a systematic and intuitive approach.

Top comments (2)

Collapse
 
rickrickiin profile image
Rickrickiin

Such programming is very convenient.