OOP & Design Patterns

01

Object

In OOP, an Object is a real-world entity that has properties (attributes) and behaviors (methods). It is an instance of a Class, which acts as a blueprint.

Real-Life Example:

Imagine you are building an Inventory Management System for a warehouse. One of the objects in this system could be a Product.

public class Product {
    private String productName;
    private int productID;
    private double price;
    private int stockQuantity;

    public Product(String productName, int productID, double price, int stockQuantity) {
        this.productName = productName;
        this.productID = productID;
        this.price = price;
        this.stockQuantity = stockQuantity;
    }

    public void displayProductDetails() {
        System.out.println("Product Name: " + productName);
        System.out.println("Product ID: " + productID);
        System.out.println("Price: " + price);
        System.out.println("Stock Quantity: " + stockQuantity);
    }

    // Getters and Setters
    public String getProductName() { return productName; }
    public void setProductName(String productName) { this.productName = productName; }

    // Other Getters and Setters
}
  • Product is an object that has properties like productName, productID, price, and stockQuantity.
  • The behavior of the Product object is shown in the displayProductDetails() method, which prints the details of the product.
Real-Life Usage:

In an Inventory Management System, each Product object could represent an actual product in the warehouse, with its own attributes (name, price, etc.) and behaviors (e.g., update stock, print product details).

02

Class

A Class in OOP is like a blueprint for creating objects. It defines properties (attributes) and behaviors (methods) that the objects created from the class will have. In simple terms, a class is a template for creating objects.

Real-Life Example: Product Class

Let’s continue with our Inventory Management System. We already have the Product object, but now we’ll define the Product Class, which acts as the blueprint for creating multiple product objects.

public class Product {
    // Properties (Attributes) of the Product class
    private String productName;
    private int productID;
    private double price;
    private int stockQuantity;

    // Constructor to initialize a Product object
    public Product(String productName, int productID, double price, int stockQuantity) {
        this.productName = productName;
        this.productID = productID;
        this.price = price;
        this.stockQuantity = stockQuantity;
    }

    // Methods (Behaviors) of the Product class
    public void displayProductDetails() {
        System.out.println("Product Name: " + productName);
        System.out.println("Product ID: " + productID);
        System.out.println("Price: " + price);
        System.out.println("Stock Quantity: " + stockQuantity);
    }

    // Getters and Setters
    public String getProductName() { return productName; }
    public void setProductName(String productName) { this.productName = productName; }

    // Other Getters and Setters for other properties
}
Breakdown:
  • Class Definition: The Product class defines the blueprint for product objects. It has attributes like productName, productID, price, and stockQuantity.
  • Constructor: The constructor (Product(String productName, int productID, double price, int stockQuantity)) is used to create a new product object with specific values.
  • Methods: The displayProductDetails() method is a behaviour of the product that prints the details of the product to the console.
Real-Life Usage:

In a warehouse system, you can have many products, and each product is created from the Product class. The Product class provides a standardised structure for storing the data, and each product object created from it can have its unique attributes like name, price, and stock quantity.

03

Inheritance

Inheritance allows one class (called the child class or subclass) to inherit properties and behaviors (methods) from another class (called the parent class or superclass). This concept promotes code reuse and establishes a hierarchical relationship between classes.

Real-Life Example: Product and Electronics

In an inventory management system, we might have a generic Product class, and then a more specialized class, such as Electronics, that inherits from the Product class. This way, we can reuse the common attributes and methods from the Product class while adding specific attributes or behaviors for the Electronics class.

// Parent class (Product)
public class Product {
    private String productName;
    private int productID;
    private double price;
    private int stockQuantity;

    public Product(String productName, int productID, double price, int stockQuantity) {
        this.productName = productName;
        this.productID = productID;
        this.price = price;
        this.stockQuantity = stockQuantity;
    }

    public void displayProductDetails() {
        System.out.println("Product Name: " + productName);
        System.out.println("Product ID: " + productID);
        System.out.println("Price: " + price);
        System.out.println("Stock Quantity: " + stockQuantity);
    }

    // Getters and Setters
}

// Child class (Electronics)
public class Electronics extends Product {
    private String brand;
    private int warrantyPeriod; // in months

    public Electronics(String productName, int productID, double price, int stockQuantity, String brand, int warrantyPeriod) {
        super(productName, productID, price, stockQuantity); // Call the parent class constructor
        this.brand = brand;
        this.warrantyPeriod = warrantyPeriod;
    }

    public void displayElectronicsDetails() {
        displayProductDetails(); // Inherited method from Product
        System.out.println("Brand: " + brand);
        System.out.println("Warranty Period: " + warrantyPeriod + " months");
    }

    // Getters and Setters for brand and warranty
}
Breakdown:
  • Product Class (Parent Class): This is the general class that defines common properties for all products.
  • Electronics Class (Child Class): This class inherits from the Product class, and it adds specific attributes such as brand and warrantyPeriod. It also calls the displayProductDetails() method from the Product class and adds its own method to display additional details.
Real-Life Usage:

Product is the base class, and we can have other subclasses like Clothing, Furniture, or Electronics, each inheriting common features from Product but with additional specialized attributes or methods. it can have its unique attributes like name, price, and stock quantity.

04

Polymorphism

Polymorphism allows objects to be treated as instances of their parent class, but they can take on many forms (hence the name “many shapes”). It enables a single method to behave differently based on the object that it is acting upon. There are two types of polymorphism in OOP:

  1. Compile-time polymorphism (method overloading)
  2. Runtime polymorphism (method overriding)
Real-Life Example: Payment System

Let’s consider a Payment system where we can have different payment methods like CreditCard and PayPal. Both classes can inherit from a common PaymentMethod class, and each will override the processPayment() method to behave according to the specific payment type.

// Parent Class (PaymentMethod)
public class PaymentMethod {
    public void processPayment() {
        System.out.println("Processing payment...");
    }
}

// Child Class (CreditCard)
public class CreditCard extends PaymentMethod {
    @Override
    public void processPayment() {
        System.out.println("Processing credit card payment...");
    }
}

// Child Class (PayPal)
public class PayPal extends PaymentMethod {
    @Override
    public void processPayment() {
        System.out.println("Processing PayPal payment...");
    }
}
Breakdown:
  • Parent Class (PaymentMethod): The processPayment() method in the parent class is generic and works for all payment types.
  • Child Classes (CreditCard and PayPal): These child classes override the processPayment() method to provide specific behavior for each payment method.
Real-Life Usage:

In an online store, when a user selects a payment method (CreditCard, PayPal, etc.), the system will dynamically determine the correct way to process the payment based on the object type (runtime polymorphism). This allows the same method to be used across different payment methods without knowing the exact class type beforehand.

05

Encapsulation

Encapsulation is the concept of bundling the data (attributes) and methods that operate on the data into a single unit, known as a class. It also involves restricting direct access to some of the object’s components, which helps in data hiding. The goal of encapsulation is to protect the object’s internal state by only allowing access through well-defined methods (getters and setters).

Real-Life Example: Bank Account

Let’s consider a BankAccount class where we want to encapsulate the account balance. We don’t want external users to directly change the balance but instead use methods like deposit() and withdraw() to modify the balance. We can achieve this by making the balance private and providing controlled access through public methods.

// BankAccount class demonstrating Encapsulation
public class BankAccount {
    // Private attribute: The balance is hidden from external access
    private double balance;

    // Constructor to initialize the balance
    public BankAccount(double initialBalance) {
        this.balance = initialBalance;
    }

    // Public method to deposit money
    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            System.out.println("Deposited: " + amount);
        } else {
            System.out.println("Invalid deposit amount.");
        }
    }

    // Public method to withdraw money
    public void withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            System.out.println("Withdrew: " + amount);
        } else {
            System.out.println("Invalid withdrawal amount.");
        }
    }

    // Getter method to retrieve the balance
    public double getBalance() {
        return balance;
    }
}
Breakdown:
  • Private Attribute: The balance is marked private, so it can’t be accessed directly from outside the class.
  • Public Methods: deposit(), withdraw(), and getBalance() are public methods that control how the balance can be changed or accessed. These methods ensure that no invalid operations occur on the balance (e.g., negative deposits or withdrawals that exceed the available balance).
  • Encapsulation: The internal state of the object (balance) is protected, and only authorized actions (depositing and withdrawing) can modify it.
Real-Life Usage:

In a banking system, this ensures that customers can’t directly manipulate their account balance, but instead, they interact with the account through controlled methods.

06

Abstraction

Abstraction is the concept of hiding the complex implementation details of a system and exposing only the necessary parts to the user. It allows a programmer to focus on high-level functionality without needing to understand the complex code behind it. In OOP, abstraction is often achieved through abstract classes and interfaces.

Real-Life Example: Vehicle System

Let’s consider a Vehicle system where we have various types of vehicles like Car and Bike. The common functionality like start() and stop() can be abstracted out in a Vehicle abstract class, while each specific vehicle class can implement these functionalities in its own way.

// Abstract Class (Vehicle)
public abstract class Vehicle {
    // Abstract methods
    public abstract void start();
    public abstract void stop();
}

// Concrete Class (Car)
public class Car extends Vehicle {
    @Override
    public void start() {
        System.out.println("Starting the car...");
    }

    @Override
    public void stop() {
        System.out.println("Stopping the car...");
    }
}

// Concrete Class (Bike)
public class Bike extends Vehicle {
    @Override
    public void start() {
        System.out.println("Starting the bike...");
    }

    @Override
    public void stop() {
        System.out.println("Stopping the bike...");
    }
}
Breakdown:
  • Abstract Class (Vehicle): The Vehicle class is abstract, meaning it can’t be instantiated directly. It defines the start() and stop() methods, but doesn’t provide implementations for them.
  • Concrete Classes (Car and Bike): Both the Car and Bike classes extend the Vehicle class and provide their own implementations for the start() and stop() methods.
Real-Life Usage:

In a Vehicle Management System, the user can interact with a Vehicle object and call start() and stop() methods, without needing to know whether it’s a car or a bike. The implementation details are abstracted away, and only the relevant interface is provided.

Design Patterns in OOP

Design patterns are reusable solutions to common software design problems. They offer time-tested strategies for solving issues in software architecture. We will cover some key design patterns in OOP and explain them with real-life examples.

01

Singleton Pattern

The Singleton Pattern ensures that a class has only one instance and provides a global point of access to that instance.

Real-Life Example: Database Connection

In a large system, you might want to ensure that there is only one instance of the database connection. The DatabaseConnection class will use the Singleton pattern to ensure only one connection instance is created, no matter how many times it’s accessed.

public class DatabaseConnection {
    private static DatabaseConnection instance;

    // Private constructor to prevent instantiation
    private DatabaseConnection() {
        // Connect to the database
    }

    // Public method to get the instance
    public static DatabaseConnection getInstance() {
        if (instance == null) {
            instance = new DatabaseConnection();
        }
        return instance;
    }

    // Method to execute queries
    public void executeQuery(String query) {
        // Execute the query
    }
}
Breakdown:
  • The DatabaseConnection class has a private static variable instance to hold the single instance.
  • The constructor is private to prevent direct instantiation of the class.
  • The getInstance() method ensures that only one instance is created.

Real-Life Usage:

In a web application, you only want one database connection to reduce overhead and ensure consistency across multiple requests.

02

Factory Pattern

The Factory Pattern provides a way to create objects without specifying the exact class of object that will be created. Instead, the factory method delegates the responsibility of instantiating the object to subclasses or to a specialised factory class.

Real-Life Example: Notification System

Let’s imagine you are building a Notification System where you need to send different types of notifications like Email, SMS, and Push Notification. Instead of directly instantiating the notification classes, the NotificationFactory class can decide which notification to create based on user preferences.

// Product Interface
public interface Notification {
    void sendNotification();
}

// Concrete Product (EmailNotification)
public class EmailNotification implements Notification {
    @Override
    public void sendNotification() {
        System.out.println("Sending email notification...");
    }
}

// Concrete Product (SMSNotification)
public class SMSNotification implements Notification {
    @Override
    public void sendNotification() {
        System.out.println("Sending SMS notification...");
    }
}

// Concrete Product (PushNotification)
public class PushNotification implements Notification {
    @Override
    public void sendNotification() {
        System.out.println("Sending push notification...");
    }
}

// Factory Class
public class NotificationFactory {
    public static Notification createNotification(String type) {
        if (type.equals("Email")) {
            return new EmailNotification();
        } else if (type.equals("SMS")) {
            return new SMSNotification();
        } else if (type.equals("Push")) {
            return new PushNotification();
        }
        return null;
    }
}
Breakdown:
  • Notification Interface: This defines a method sendNotification() that must be implemented by all types of notifications.
  • Concrete Products (EmailNotification, SMSNotification, PushNotification): These are specific types of notifications that implement the Notification interface.
  • Factory Class (NotificationFactory): The createNotification() method in the factory decides which notification object to create based on the type provided.
Real-Life Usage:

In a Notification System, the factory pattern allows you to create the appropriate notification object without knowing which class will be used. This decouples the code that uses notifications from the specific types of notifications.

03

Observer Pattern

The Observer Pattern defines a one-to-many dependency between objects, where a change in one object (the subject) automatically updates all dependent objects (the observers). It is particularly useful in scenarios where multiple components need to be notified of changes in a single entity, such as in event-driven systems or UI updates.

Real-Life Example: Weather Monitoring System

Let’s consider a Weather Monitoring System, where multiple devices like Smartphones, Laptops, and Tablets need to be updated when the weather data changes (temperature, humidity, etc.). The WeatherStation acts as the subject, and the devices (smartphones, tablets, etc.) are observers that get updated whenever the weather data changes.

import java.util.ArrayList;
import java.util.List;

// Subject Interface
public interface WeatherStation {
    void addObserver(Device device);
    void removeObserver(Device device);
    void notifyObservers();
}

// Concrete Subject
public class ConcreteWeatherStation implements WeatherStation {
    private List<Device> devices = new ArrayList<>();
    private String weatherData;

    @Override
    public void addObserver(Device device) {
        devices.add(device);
    }

    @Override
    public void removeObserver(Device device) {
        devices.remove(device);
    }

    @Override
    public void notifyObservers() {
        for (Device device : devices) {
            device.update(weatherData);
        }
    }

    // Method to change weather data
    public void setWeatherData(String weatherData) {
        this.weatherData = weatherData;
        notifyObservers(); // Notify all observers about the update
    }
}

// Observer Interface
public interface Device {
    void update(String weatherData);
}

// Concrete Observer (Smartphone)
public class Smartphone implements Device {
    @Override
    public void update(String weatherData) {
        System.out.println("Smartphone received weather update: " + weatherData);
    }
}

// Concrete Observer (Tablet)
public class Tablet implements Device {
    @Override
    public void update(String weatherData) {
        System.out.println("Tablet received weather update: " + weatherData);
    }
}
Breakdown:
  • Subject (WeatherStation): The WeatherStation is the subject, maintaining a list of observers (devices) and notifying them of changes.
  • Observer (Device): The Device interface is implemented by concrete observers like Smartphone and Tablet, which receive updates when the weather changes.
  • Concrete Subject (ConcreteWeatherStation): This class handles the actual weather data and notifies the observers when the data changes.
Real-Life Usage:

In a weather monitoring system, whenever the weather changes (e.g., a temperature update), the system notifies all registered devices (smartphones, tablets, etc.) in real-time.

04

Strategy Pattern

The Strategy Pattern is a behavioral design pattern that defines a family of algorithms, encapsulates each one, and makes them interchangeable. This allows a class to change its behavior at runtime, based on the strategy it selects.

Real-Life Example: Tax Calculation System

In a Tax Calculation System, different countries or regions have different tax calculation strategies. Instead of hardcoding tax calculation logic, the TaxCalculator class can use the Strategy Pattern to dynamically choose the appropriate strategy based on the user’s region.

// Strategy Interface
public interface TaxStrategy {
    double calculateTax(double income);
}

// Concrete Strategy (US Tax)
public class USTaxStrategy implements TaxStrategy {
    @Override
    public double calculateTax(double income) {
        return income * 0.25; // Example: 25% tax
    }
}

// Concrete Strategy (EU Tax)
public class EUTaxStrategy implements TaxStrategy {
    @Override
    public double calculateTax(double income) {
        return income * 0.20; // Example: 20% tax
    }
}

// Context Class
public class TaxCalculator {
    private TaxStrategy taxStrategy;

    // Constructor to set the strategy
    public TaxCalculator(TaxStrategy taxStrategy) {
        this.taxStrategy = taxStrategy;
    }

    // Method to calculate tax based on the selected strategy
    public double calculate(double income) {
        return taxStrategy.calculateTax(income);
    }
}
Breakdown:
  • Strategy Interface: The TaxStrategy interface defines the method calculateTax(), which will be implemented by concrete strategies.
  • Concrete Strategies (USTaxStrategy, EUTaxStrategy): These classes implement the TaxStrategy interface, each providing a different tax calculation.
  • Context (TaxCalculator): This class uses a specific strategy for tax calculation. The strategy is passed to the constructor, and the calculate() method uses the chosen strategy.
Real-Life Usage:

In a multi-country e-commerce platform, the TaxCalculator class can use different tax strategies for US, EU, and other countries based on the user’s region.m notifies all registered devices (smartphones, tablets, etc.) in real-time.

05

Decorator Pattern

The Decorator Pattern allows you to add new functionality to an object without altering its structure. It provides a flexible alternative to subclassing for extending functionality.

Real-Life Example: Coffee Shop Order

In a Coffee Shop, you can order a basic coffee and then decorate it with extras like milk, sugar, or whipped cream. Instead of creating new classes for each combination, you can use the decorator pattern to add these extras dynamically.

public interface Coffee {
    double cost();
}

public class SimpleCoffee implements Coffee {
    @Override
    public double cost() {
        return 5.0; // Base price of a simple coffee
    }
}

public class MilkDecorator implements Coffee {
    private Coffee coffee;

    public MilkDecorator(Coffee coffee) {
        this.coffee = coffee;
    }

    @Override
    public double cost() {
        return coffee.cost() + 1.0; // Add cost of milk
    }
}

public class SugarDecorator implements Coffee {
    private Coffee coffee;

    public SugarDecorator(Coffee coffee) {
        this.coffee = coffee;
    }

    @Override
    public double cost() {
        return coffee.cost() + 0.5; // Add cost of sugar
    }
}
Breakdown:

Decorators (MilkDecorator, SugarDecorator): These classes add additional functionality to the base coffee object by “decorating” it with new features like milk or sugar.

Coffee Interface: This defines a cost() method.

SimpleCoffee: The basic coffee class that implements the Coffee interface.

06

Command Pattern

The Command Pattern turns requests or simple operations into objects. This allows for parameterizing clients with different requests, queuing requests, and logging the requests.

Real-Life Example: Remote Control for Devices

In a Remote Control system, different commands like Power On, Volume Up, and Change Channel can be turned into command objects that the remote control can execute.

public interface Command {
    void execute();
}

public class LightOnCommand implements Command {
    private Light light;

    public LightOnCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        light.turnOn();
    }
}

public class Light {
    public void turnOn() {
        System.out.println("The light is on");
    }

    public void turnOff() {
        System.out.println("The light is off");
    }
}
Breakdown:

Receiver (Light): The object that performs the action (turning on/off the light).

Command Interface: Defines the execute() method for all commands.

Concrete Command (LightOnCommand): A concrete command that turns the light on when executed.