Essential Concepts for Mastering OOP (Object-Oriented Programming)

Object-oriented programming (OOP) is a programming paradigm centred around the concept of objects.

class and object

What is a Class?

  • A class is a blueprint or template for creating objects.
  • It defines the attributes(data members) and behaviours(methods) that objects will have.
  • Classes are a fundamental concept in object-oriented programming (OOP).
  • It’s a real-world entity

For example, if you want to model a Car, you would create a Car class that might have properties like colour, make, and model, and methods (functions) like start_engine() or drive().

# Defining a class
class Car:
    def __init__(self, make, model, color):
        self.make = make  # Attribute
        self.model = model  # Attribute
        self.color = color  # Attribute

    def start_engine(self):  # Method
        print(f"The {self.color} {self.make} {self.model}'s engine has started.")

    def drive(self):  # Method
        print(f"The {self.color} {self.make} {self.model} is now driving.")

What is an Object?

  • Object is an instance of a class.
  • All data members and member functions of the class can be accessed with the help of objects.
  • When a class is defined, no memory is allocated, but memory is allocated when it is instantiated (i.e. an object is created).
my_car = Car(make="Toyota", model="Camry", color="Red")

# Accessing attributes and methods
print(my_car.make)  # Output: Toyota
my_car.start_engine()  # Output: The Red Toyota Camry's engine has started.
my_car.drive()  # Output: The Red Toyota Camry is now driving.
OOPs classes and objects

OOP Concept

Object-oriented programming (OOP) is about classes and objects. OOP enables us to model real-world entities, making code more modular, reusable, and easier to maintain. Four fundamental principles—abstraction, encapsulation, inheritance, and polymorphism—form the backbone of OOP. Understanding these principles is crucial for writing effective and efficient object-oriented code.

Encapsulation

Concept of Encapsulation
  • Wrapping data (attributes) and functions(methods) together in a single unit is called encapsulation.
  • It restricts access to certain components of an object to protect the integrity of the data and prevent unauthorized or accidental interference.
  • Private Members: In many OOP languages, you can make data members private, so they cannot be accessed from outside the class. For example, in Python, you can prefix an attribute with an underscore (_) to indicate it is private by convention only.
class BankAccount:
    def __init__(self, balance):
        self.__balance = balance

    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

account = BankAccount(1000)
account.deposit(500)
print(account.get_balance())  # Output: 1500

In the above code, we have wrapped all attributes and methods related to the Bank account as a single unit.

Inheritance

inhertiance
  • It allows a new class to inherit properties and behaviour from an existing class.
  • It helps in reusing code and establishing a natural hierarchy between classes.
class Vehicle:
    def __init__(self, make, model):
        self.make = make
        self.model = model

    def start(self):
        return f"{self.make} {self.model} is starting."

class Car(Vehicle):
    def __init__(self, make, model, doors):
        super().__init__(make, model)
        self.doors = doors

    def open_doors(self):
        return f"Opening {self.doors} doors."

my_car = Car("Honda", "Civic", 4)
print(my_car.start())       # Output: Honda Civic is starting.
print(my_car.open_doors())  # Output: Opening 4 doors.

The Car doesn’t have method start(), But the Car object is using start() due to inheritance

Polymorphism

polymorphism
  • Polymorphism means “many forms” and allows methods to do different things based on the object it is acting upon, even if they share the same name.
  • The ability of a message to be displayed in more than one form.
  • It can be achieved through method overloading, method overriding, and operator overloading

Example: A person can have different characteristics at the same time. Like a man at the same time is a father, a husband, and an employee. So the same person possesses different behaviors in different situations. This is called polymorphism. 

Method Overloading

  • Method overloading allows a class to have multiple methods with the same name but with different signatures(number, type, or order).
  • Compile-time Polymorphism
  • Python does not natively support method overloading in the same way as some other languages (like Java or C++). Read further
add(int item1, int item2)
add(int item1, int item2,int item3)
add(string item1, string item2)

Method Overriding

  • Method overriding allows a child class to provide a specific implementation of a method that is already defined in its parent class.
  • Runtime polymorphism
class Animal:
    def speak(self):
        return "Some generic sound"

class Dog(Animal):
    def speak(self):
        return "Bark"

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

def make_animal_speak(animal):
    print(animal.speak())

my_dog = Dog()
my_cat = Cat()

make_animal_speak(my_dog)  # Output: Bark
make_animal_speak(my_cat)  # Output: Meow

In this example, the sound method is defined in both the Animal class and its subclasses Dog and Cat. When the sound method is called on an object of the Dog or Cat class, the overridden method in the respective subclass is executed instead of the one in the Animal class.

Abstraction

abstraction
  • This principle involves hiding complex implementation details and showing only the essential features of an object.
  • This allows the user to interact with an object without needing to understand its internal workings.
  • In many OOP languages, abstract classes or interfaces are used to implement abstraction.
  • An abstract class cannot be instantiated and often includes one or more abstract methods that must be implemented by derived classes.
from abc import ABC, abstractmethod

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

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.1416 * self.radius * self.radius

my_circle = Circle(5)
print(my_circle.area())  # Output: 78.54

Advantages of OOP

  • Modularity: Code is organized into distinct classes, making it easier to manage and maintain.
  • Reusability: Inheritance and polymorphism promote code reuse.
  • Flexibility: Encapsulation and abstraction allow for changes in code with minimal impact on other parts of the program.
  • Scalability: OOP provides a structured way to develop complex software that can be easily scaled.

Association, Aggregation, and Composition

Association, Aggregation, Composition

Association

  • Association represents a general relationship between two or more objects where they interact with each other. It is the most basic form of a relationship.
  • Bidirectional or Unidirectional: An association can be one-way (unidirectional) or two-way (bidirectional).
  • Multiplicity: Specifies how many instances of one class are associated with one instance of another class (e.g., one-to-one, one-to-many).
  • Example: A Teacher can be associated with multiple Students, and a Student can be associated with multiple Teachers.

Example: A Driver and a Car have an association. A Driver drives a Car, and a Car can be driven by a Driver.

class Driver:
    def __init__(self, name):
        self.name = name

    def drives(self, car):
        print(f"{self.name} drives a {car.model}")

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

# Example usage:
driver = Driver("Alice")
car = Car("Toyota Corolla")

driver.drives(car)

Output: Alice drives a Toyota Corolla

In this case, Driver and Car are associated, but they can exist independently.

Aggregation

  • Aggregation is a specialized form of association, also known as a “has-a” relationship.
  • It represents a whole-part relationship where the part can exist independently of the whole.
  • Weak relationship: The lifecycle of the part is independent of the whole. If the whole object is destroyed, the part can still exist.
  • Visual Representation: In UML diagrams, aggregation is represented by a hollow diamond.
  • Example: A Library aggregates Books. Even if the Library is closed or demolished, the Books can still exist independently.

Example: A Library aggregates Books. Books belong to the library, but they can exist without the library.

class Book:
    def __init__(self, title):
        self.title = title

class Library:
    def __init__(self, name):
        self.name = name
        self.books = []

    def add_book(self, book):
        self.books.append(book)

    def show_books(self):
        for book in self.books:
            print(book.title)

# Example usage:
library = Library("City Library")
book1 = Book("1984 by George Orwell")
book2 = Book("To Kill a Mockingbird by Harper Lee")

library.add_book(book1)
library.add_book(book2)

library.show_books()

Output

1984 by George Orwell
To Kill a Mockingbird by Harper Lee

In this example, the Library contains Books, but the Books can exist even if the Library is destroyed.

Composition

  • Composition is a more restrictive form of aggregation.
  • It represents a whole-part relationship where the part cannot exist independently of the whole.
  • Strong relationship: The lifecycle of the part is strictly tied to the lifecycle of the whole. If the whole object is destroyed, the part is also destroyed.
  • Visual Representation: In UML diagrams, composition is represented by a filled diamond.
  • Example: A House and its Rooms. If the House is destroyed, its Rooms do not exist independently.

Example: A House is composed of Rooms. If the house is demolished, the rooms cease to exist.

class Room:
    def __init__(self, name):
        self.name = name

class House:
    def __init__(self, address):
        self.address = address
        self.rooms = []

    def add_room(self, room_name):
        room = Room(room_name)
        self.rooms.append(room)

    def show_rooms(self):
        for room in self.rooms:
            print(f"Room: {room.name}")

# Example usage:
house = House("123 Maple Street")
house.add_room("Living Room")
house.add_room("Bedroom")

house.show_rooms()

Output:

Room: Living Room
Room: Bedroom

In this example, the House is composed of Rooms. If the House object is destroyed, the Rooms no longer exist.

AssociationAggregationComposition
General RelationshipWhole-Part RelationshipStrong Whole-Part Relationship
No strong dependencyObjects are still relatively independentStrong dependency between objects
Objects can exist independentlyThe lifecycle of the part is not tied to the wholeThe lifecycle of the part is strictly tied to the whole.
Represents a “uses-a” or “knows-a” relationshipRepresents a “has-a” relationship with some ownershipRepresents a “contains-a” or “is-part-of” relationship

Hierarchy Analogy:

  • An association can be viewed as the most general concept, where objects have some kind of relationship.
  • Aggregation is a more specific type of Association where there is a whole-part relationship, but the parts are independent.
  • Composition is the most specific and strongest type of relationship, where the parts are entirely dependent on the whole.
Association
   |
   |-- Aggregation
   |       |
   |       |-- Composition
  • Association is the broadest concept, encompassing any kind of relationship between objects.
  • Aggregation is a specific type of Association where one object is a part of another but with less dependency.
  • Composition is a specific type of Aggregation with the highest level of dependency between the whole and its parts.

Inheritance vs Association

Inheritance

  • Inheritance is a mechanism where a new class (derived or child class) inherits the properties and behaviours (attributes and methods) of an existing class (base or parent class).
  • It represents an “is-a” relationship.
  • Purpose: Inheritance promotes code reusability by allowing a child class to reuse methods and attributes of the parent class. The child class can also override or extend the functionality of the parent class.
  • Example:
    • The dog is an Animal
    • A student is a Person

Association

  • The association represents a relationship where two or more classes are connected but remain independent of each other.
  • It represents a “has-a” relationship and is used to show how objects interact with each other.
  • Purpose: Association describes how objects work together, but each object has its own lifecycle and can exist independently of the other.
  • Example
    • The book has a Page
    • The car has an engine

Inheritance

class Animal:
    def speak(self):
         ..........

class Dog(Animal):
    def speak(self):
         ..........

Association

class Teacher:
    def some_function(self, student):
         ..........

class Student:
    def learn(self, teacher):
        Teacher().some_function()

Important Concepts

    A superclass reference variable can indeed refer to a subclass object

    Java

    Java/C++: A superclass reference variable can hold a reference to any subclass object, and the method that gets called depends on the actual object type at runtime.

    class Animal {
        void sound() {
            System.out.println("Animal makes a sound");
        }
    }
    
    class Dog extends Animal {
        void sound() {
            System.out.println("Dog barks");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Animal myAnimal = new Dog(); // Superclass reference to a subclass object
            myAnimal.sound(); // Output will be "Dog barks"
        }
    }

    Here, myAnimal is a reference variable of the Animal superclass, but it refers to an instance of the Dog subclass. When you call the sound() method on myAnimal, the Dog class’s version of sound() is executed, demonstrating polymorphism.

    Python

    Since variables are dynamically typed, any variable can hold any object. The concept of a superclass reference holding a subclass object is not explicitly enforced, but the effect is similar due to Python’s dynamic typing and polymorphism.

    class Animal:
        def sound(self):
            print("Animal makes a sound")
    
    class Dog(Animal):
        def sound(self):
            print("Dog barks")
    
    class Cat(Animal):
        def sound(self):
            print("Cat meows")
    
    my_animal = Dog()  # my_animal now references a Dog object
    my_animal.sound()  # Output: Dog barks
    
    my_animal = Cat()  # my_animal now references a Cat object
    my_animal.sound()  # Output: Cat meows
    

    In Python, my_animal can refer to any object, including an object of a subclass, and Python will determine at runtime which sound() method to call:

    Conclusion

    Object-Oriented Programming (OOP) is a powerful and flexible paradigm that allows developers to create modular, reusable, and scalable software. By organizing code around objects—instances of classes—OOP makes it easier to model real-world problems and manage complex systems. The four fundamental principles of OOP—encapsulation, inheritance, polymorphism, and abstraction—provide a robust framework for designing and implementing software.

    Polymorphism, in particular, plays a crucial role in OOP by allowing objects to be treated as instances of their parent class, enabling method overloading and overriding. These techniques enhance the flexibility and maintainability of code, allowing for the seamless extension and modification of software without disrupting existing functionality.

    By mastering OOP concepts, developers can write more efficient, organized, and maintainable code, making it easier to collaborate on large projects and adapt to future requirements. Whether you are building small applications or large-scale systems, OOP principles serve as a foundational approach that can be applied across various programming languages and development environments.

    Resource

    Further Reading

    Low-level Designing

    Leave a Comment