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.")
Python

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.
Python

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
Python

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.
Python

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)
Python

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
Python

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 understanding its internal workings.
  • In many OOP languages, abstract classes or interfaces 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
Python

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)
Python

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()
Python

Output

1984 by George Orwell
To Kill a Mockingbird by Harper Lee
Python

In this example, the library contains books, but books can exist even if they are 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()
Python

Output:

Room: Living Room
Room: Bedroom
Python

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
Python

  • 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(<strong>Animal</strong>):
    def speak(self):
         ..........
Python

Association

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

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

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"
    }
}
Python

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
Python

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:

Alternative to OOP

Here are a few common programming paradigms:

  • Procedural Programming: This is one of the earliest and simplest styles. In procedural programming, code is written in a sequence of instructions or procedures. The focus is on functions or subroutines that operate on data rather than on data itself. It’s often structured with modular design but without objects or classes. Popular languages for procedural programming include C and BASIC.
  • Functional Programming (FP): Functional programming is based on mathematical functions. It emphasizes immutability, where data is not modified after creation, and pure functions, where the output is determined solely by the input. FP avoids shared state and side effects, making it suitable for parallel processing. Common functional programming languages include Haskell, Lisp, and newer implementations in JavaScript and Python.
  • Declarative Programming: In declarative programming, you describe what you want to achieve rather than how to achieve it. SQL, for example, is declarative; you specify what data you need but not how to retrieve it. Similarly, HTML is declarative because it defines how a webpage should look without detailing the rendering process.
  • Logic Programming: This paradigm is based on formal logic. Instead of telling the computer what to do, you specify rules and relationships, and the computer deduces the solution. Prolog is a well-known logic programming language, commonly used in AI and computational linguistics.
  • Event-Driven Programming: This is often used in GUIs, where program execution is driven by user actions or events (like clicks, keystrokes, etc.). It’s common in languages like JavaScript, where interactions trigger specific code routines.
  • Aspect-Oriented Programming (AOP): AOP complements OOP by allowing the separation of cross-cutting concerns, such as logging, security, and error handling. It enables you to add behaviour to code without modifying the actual code, typically through the use of aspects, join points, and advice.
  • Reactive Programming: Reactive programming is a paradigm focused on asynchronous data streams and the propagation of change. It’s highly suitable for applications that need to handle real-time data or user interaction, like live feeds or dynamic UI updates. This style is particularly prominent in modern JavaScript frameworks (such as RxJS in Angular) and reactive systems in languages like Java (Project Reactor) and Kotlin.

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

Leave a Comment