Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save carefree-ladka/65a0f9dea8f3199b5eea69b4d637d3d1 to your computer and use it in GitHub Desktop.

Select an option

Save carefree-ladka/65a0f9dea8f3199b5eea69b4d637d3d1 to your computer and use it in GitHub Desktop.
Comprehensive Guide to Object-Oriented Programming Design in Java

Comprehensive Guide to Object-Oriented Programming Design in Java

Table of Contents

  1. Introduction to OOP
  2. The Four Pillars of OOP
  3. Classes and Objects
  4. Constructors
  5. Access Modifiers
  6. Method Overloading and Overriding
  7. Abstract Classes
  8. Interfaces
  9. Composition vs Inheritance
  10. SOLID Principles
  11. Design Patterns
  12. Best Practices

Introduction to OOP

Object-Oriented Programming (OOP) is a programming paradigm that organizes software design around data, or objects, rather than functions and logic. Java is a pure object-oriented language where everything revolves around classes and objects.

Key Concepts:

  • Object: An instance of a class containing state (data) and behavior (methods)
  • Class: A blueprint or template for creating objects
  • Message Passing: Objects communicate by calling methods on each other

The Four Pillars of OOP

Encapsulation

Encapsulation is the bundling of data and methods that operate on that data within a single unit (class), and restricting direct access to some components.

Example:

public class BankAccount {
    // Private fields - data hiding
    private String accountNumber;
    private double balance;
    private String ownerName;
    
    // Constructor
    public BankAccount(String accountNumber, String ownerName) {
        this.accountNumber = accountNumber;
        this.ownerName = ownerName;
        this.balance = 0.0;
    }
    
    // Public methods - controlled access
    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            System.out.println("Deposited: $" + amount);
        } else {
            System.out.println("Invalid deposit amount");
        }
    }
    
    public boolean withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            System.out.println("Withdrawn: $" + amount);
            return true;
        } else {
            System.out.println("Insufficient funds or invalid amount");
            return false;
        }
    }
    
    // Getter methods
    public double getBalance() {
        return balance;
    }
    
    public String getAccountNumber() {
        return accountNumber;
    }
    
    public String getOwnerName() {
        return ownerName;
    }
}

Benefits:

  • Data security and integrity
  • Flexibility to change implementation without affecting external code
  • Better maintainability

Inheritance

Inheritance allows a class to inherit properties and methods from another class, promoting code reusability.

Example:

// Parent class (Superclass)
public class Vehicle {
    protected String brand;
    protected String model;
    protected int year;
    
    public Vehicle(String brand, String model, int year) {
        this.brand = brand;
        this.model = model;
        this.year = year;
    }
    
    public void start() {
        System.out.println("Vehicle is starting...");
    }
    
    public void stop() {
        System.out.println("Vehicle is stopping...");
    }
    
    public void displayInfo() {
        System.out.println(year + " " + brand + " " + model);
    }
}

// Child class (Subclass)
public class Car extends Vehicle {
    private int numberOfDoors;
    private String fuelType;
    
    public Car(String brand, String model, int year, int numberOfDoors, String fuelType) {
        super(brand, model, year); // Call parent constructor
        this.numberOfDoors = numberOfDoors;
        this.fuelType = fuelType;
    }
    
    // Method overriding
    @Override
    public void start() {
        System.out.println("Car engine starting with key...");
    }
    
    // Additional method specific to Car
    public void honk() {
        System.out.println("Beep beep!");
    }
    
    @Override
    public void displayInfo() {
        super.displayInfo();
        System.out.println("Doors: " + numberOfDoors + ", Fuel: " + fuelType);
    }
}

// Another child class
public class Motorcycle extends Vehicle {
    private boolean hasSidecar;
    
    public Motorcycle(String brand, String model, int year, boolean hasSidecar) {
        super(brand, model, year);
        this.hasSidecar = hasSidecar;
    }
    
    @Override
    public void start() {
        System.out.println("Motorcycle kick-starting...");
    }
    
    public void wheelie() {
        System.out.println("Performing a wheelie!");
    }
}

Usage:

public class Main {
    public static void main(String[] args) {
        Car car = new Car("Toyota", "Camry", 2023, 4, "Hybrid");
        car.displayInfo();
        car.start();
        car.honk();
        
        Motorcycle bike = new Motorcycle("Harley-Davidson", "Sportster", 2022, false);
        bike.displayInfo();
        bike.start();
        bike.wheelie();
    }
}

Polymorphism

Polymorphism allows objects of different types to be treated as objects of a common parent type. It enables one interface to be used for a general class of actions.

Types:

  1. Compile-time (Static) Polymorphism: Method overloading
  2. Runtime (Dynamic) Polymorphism: Method overriding

Example:

// Parent class
public abstract class Animal {
    protected String name;
    
    public Animal(String name) {
        this.name = name;
    }
    
    // Abstract method - must be implemented by subclasses
    public abstract void makeSound();
    
    public void sleep() {
        System.out.println(name + " is sleeping...");
    }
}

// Subclasses
public class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }
    
    @Override
    public void makeSound() {
        System.out.println(name + " says: Woof! Woof!");
    }
    
    public void fetch() {
        System.out.println(name + " is fetching the ball!");
    }
}

public class Cat extends Animal {
    public Cat(String name) {
        super(name);
    }
    
    @Override
    public void makeSound() {
        System.out.println(name + " says: Meow! Meow!");
    }
    
    public void scratch() {
        System.out.println(name + " is scratching the furniture!");
    }
}

public class Cow extends Animal {
    public Cow(String name) {
        super(name);
    }
    
    @Override
    public void makeSound() {
        System.out.println(name + " says: Moo! Moo!");
    }
}

// Demonstrating polymorphism
public class AnimalShelter {
    public static void main(String[] args) {
        // Polymorphic array - parent type holding different child objects
        Animal[] animals = new Animal[3];
        animals[0] = new Dog("Buddy");
        animals[1] = new Cat("Whiskers");
        animals[2] = new Cow("Bessie");
        
        // Same method call produces different results
        for (Animal animal : animals) {
            animal.makeSound(); // Polymorphic behavior
            animal.sleep();
            System.out.println();
        }
        
        // Method that accepts parent type
        makeAnimalSound(new Dog("Max"));
        makeAnimalSound(new Cat("Luna"));
    }
    
    public static void makeAnimalSound(Animal animal) {
        animal.makeSound(); // Works with any Animal subclass
    }
}

Abstraction

Abstraction means hiding complex implementation details and showing only essential features of an object. It helps reduce programming complexity and effort.

Example:

// Abstract class
public abstract class Shape {
    protected String color;
    
    public Shape(String color) {
        this.color = color;
    }
    
    // Abstract methods - no implementation
    public abstract double calculateArea();
    public abstract double calculatePerimeter();
    
    // Concrete method
    public void displayColor() {
        System.out.println("Color: " + color);
    }
}

// Concrete implementations
public class Circle extends Shape {
    private double radius;
    
    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }
    
    @Override
    public double calculateArea() {
        return Math.PI * radius * radius;
    }
    
    @Override
    public double calculatePerimeter() {
        return 2 * Math.PI * radius;
    }
}

public class Rectangle extends Shape {
    private double width;
    private double height;
    
    public Rectangle(String color, double width, double height) {
        super(color);
        this.width = width;
        this.height = height;
    }
    
    @Override
    public double calculateArea() {
        return width * height;
    }
    
    @Override
    public double calculatePerimeter() {
        return 2 * (width + height);
    }
}

// Using abstraction
public class ShapeTest {
    public static void main(String[] args) {
        Shape circle = new Circle("Red", 5.0);
        Shape rectangle = new Rectangle("Blue", 4.0, 6.0);
        
        printShapeInfo(circle);
        printShapeInfo(rectangle);
    }
    
    public static void printShapeInfo(Shape shape) {
        shape.displayColor();
        System.out.println("Area: " + shape.calculateArea());
        System.out.println("Perimeter: " + shape.calculatePerimeter());
        System.out.println();
    }
}

Classes and Objects

A class is a blueprint for creating objects, and an object is an instance of a class.

Example:

public class Student {
    // Instance variables (attributes)
    private String name;
    private int rollNumber;
    private double gpa;
    
    // Static variable (class variable) - shared by all instances
    private static int totalStudents = 0;
    
    // Constructor
    public Student(String name, int rollNumber, double gpa) {
        this.name = name;
        this.rollNumber = rollNumber;
        this.gpa = gpa;
        totalStudents++;
    }
    
    // Instance method
    public void study(String subject) {
        System.out.println(name + " is studying " + subject);
    }
    
    // Getter methods
    public String getName() {
        return name;
    }
    
    public double getGpa() {
        return gpa;
    }
    
    // Setter method with validation
    public void setGpa(double gpa) {
        if (gpa >= 0.0 && gpa <= 4.0) {
            this.gpa = gpa;
        } else {
            System.out.println("Invalid GPA");
        }
    }
    
    // Static method - belongs to class, not instance
    public static int getTotalStudents() {
        return totalStudents;
    }
    
    // toString method for object representation
    @Override
    public String toString() {
        return "Student{name='" + name + "', rollNumber=" + rollNumber + ", gpa=" + gpa + "}";
    }
}

// Using the class
public class Main {
    public static void main(String[] args) {
        // Creating objects
        Student student1 = new Student("Alice", 101, 3.8);
        Student student2 = new Student("Bob", 102, 3.5);
        Student student3 = new Student("Charlie", 103, 3.9);
        
        // Accessing instance methods
        student1.study("Mathematics");
        student2.study("Physics");
        
        // Accessing static method
        System.out.println("Total students: " + Student.getTotalStudents());
        
        // Using toString
        System.out.println(student1);
    }
}

Constructors

Constructors are special methods used to initialize objects. They have the same name as the class and no return type.

Example:

public class Employee {
    private String name;
    private String employeeId;
    private double salary;
    private String department;
    
    // Default constructor
    public Employee() {
        this.name = "Unknown";
        this.employeeId = "000";
        this.salary = 0.0;
        this.department = "Unassigned";
    }
    
    // Parameterized constructor
    public Employee(String name, String employeeId) {
        this.name = name;
        this.employeeId = employeeId;
        this.salary = 30000.0; // Default salary
        this.department = "General";
    }
    
    // Constructor with all parameters
    public Employee(String name, String employeeId, double salary, String department) {
        this.name = name;
        this.employeeId = employeeId;
        this.salary = salary;
        this.department = department;
    }
    
    // Copy constructor
    public Employee(Employee other) {
        this.name = other.name;
        this.employeeId = other.employeeId;
        this.salary = other.salary;
        this.department = other.department;
    }
    
    public void displayInfo() {
        System.out.println("Name: " + name + ", ID: " + employeeId + 
                         ", Salary: $" + salary + ", Dept: " + department);
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        Employee emp1 = new Employee(); // Default constructor
        Employee emp2 = new Employee("John Doe", "E001"); // Partial parameters
        Employee emp3 = new Employee("Jane Smith", "E002", 75000, "Engineering");
        Employee emp4 = new Employee(emp3); // Copy constructor
        
        emp1.displayInfo();
        emp2.displayInfo();
        emp3.displayInfo();
        emp4.displayInfo();
    }
}

Access Modifiers

Access modifiers control the visibility of classes, methods, and variables.

Types:

  • private: Accessible only within the same class
  • default (no modifier): Accessible within the same package
  • protected: Accessible within the same package and subclasses
  • public: Accessible from anywhere

Example:

// File: Person.java
package com.example.people;

public class Person {
    private String ssn;           // Only accessible within Person class
    protected String name;         // Accessible in subclasses and same package
    String address;                // Default - accessible in same package only
    public int age;                // Accessible everywhere
    
    public Person(String ssn, String name, String address, int age) {
        this.ssn = ssn;
        this.name = name;
        this.address = address;
        this.age = age;
    }
    
    // Private method
    private void calculateTaxes() {
        System.out.println("Calculating taxes using SSN: " + ssn);
    }
    
    // Public method to access private method
    public void processTaxReturn() {
        calculateTaxes(); // Can call private method within same class
    }
    
    // Protected method
    protected void updateName(String newName) {
        this.name = newName;
    }
}

// File: Employee.java
package com.example.people;

public class Employee extends Person {
    private String employeeId;
    
    public Employee(String ssn, String name, String address, int age, String employeeId) {
        super(ssn, name, address, age);
        this.employeeId = employeeId;
    }
    
    public void displayInfo() {
        // System.out.println(ssn); // ERROR: Cannot access private member
        System.out.println("Name: " + name);      // OK: protected, accessed in subclass
        System.out.println("Address: " + address); // OK: default, same package
        System.out.println("Age: " + age);        // OK: public
    }
    
    public void changeName(String newName) {
        updateName(newName); // OK: protected method accessible in subclass
    }
}

Method Overloading and Overriding

Method Overloading (Compile-time Polymorphism)

Same method name with different parameters in the same class.

public class Calculator {
    // Overloaded methods - same name, different parameters
    
    public int add(int a, int b) {
        return a + b;
    }
    
    public double add(double a, double b) {
        return a + b;
    }
    
    public int add(int a, int b, int c) {
        return a + b + c;
    }
    
    public String add(String a, String b) {
        return a + b;
    }
    
    // Different number and type of parameters
    public void display(String message) {
        System.out.println("Message: " + message);
    }
    
    public void display(String message, int times) {
        for (int i = 0; i < times; i++) {
            System.out.println(message);
        }
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        Calculator calc = new Calculator();
        
        System.out.println(calc.add(5, 10));           // Calls add(int, int)
        System.out.println(calc.add(5.5, 10.3));       // Calls add(double, double)
        System.out.println(calc.add(1, 2, 3));         // Calls add(int, int, int)
        System.out.println(calc.add("Hello", "World")); // Calls add(String, String)
    }
}

Method Overriding (Runtime Polymorphism)

Subclass provides specific implementation of a method already defined in parent class.

public class PaymentProcessor {
    public void processPayment(double amount) {
        System.out.println("Processing payment of $" + amount);
    }
    
    public double calculateFee(double amount) {
        return amount * 0.02; // 2% fee
    }
}

public class CreditCardProcessor extends PaymentProcessor {
    @Override
    public void processPayment(double amount) {
        System.out.println("Processing credit card payment of $" + amount);
        System.out.println("Verifying card details...");
        System.out.println("Payment authorized!");
    }
    
    @Override
    public double calculateFee(double amount) {
        return amount * 0.03; // 3% fee for credit cards
    }
}

public class PayPalProcessor extends PaymentProcessor {
    @Override
    public void processPayment(double amount) {
        System.out.println("Processing PayPal payment of $" + amount);
        System.out.println("Redirecting to PayPal...");
        System.out.println("Payment completed!");
    }
    
    @Override
    public double calculateFee(double amount) {
        return amount * 0.029 + 0.30; // 2.9% + $0.30 for PayPal
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        PaymentProcessor processor1 = new CreditCardProcessor();
        PaymentProcessor processor2 = new PayPalProcessor();
        
        processor1.processPayment(100.0);
        System.out.println("Fee: $" + processor1.calculateFee(100.0));
        
        processor2.processPayment(100.0);
        System.out.println("Fee: $" + processor2.calculateFee(100.0));
    }
}

Abstract Classes

Abstract classes cannot be instantiated and may contain abstract methods (without implementation) that must be implemented by subclasses.

Example:

public abstract class DatabaseConnection {
    protected String connectionString;
    protected boolean isConnected;
    
    public DatabaseConnection(String connectionString) {
        this.connectionString = connectionString;
        this.isConnected = false;
    }
    
    // Abstract methods - must be implemented by subclasses
    public abstract void connect();
    public abstract void disconnect();
    public abstract void executeQuery(String query);
    
    // Concrete method - shared by all subclasses
    public void logConnection() {
        System.out.println("Connection to: " + connectionString);
        System.out.println("Status: " + (isConnected ? "Connected" : "Disconnected"));
    }
    
    // Concrete method with implementation
    public boolean isConnected() {
        return isConnected;
    }
}

public class MySQLConnection extends DatabaseConnection {
    private int port;
    
    public MySQLConnection(String connectionString, int port) {
        super(connectionString);
        this.port = port;
    }
    
    @Override
    public void connect() {
        System.out.println("Connecting to MySQL database on port " + port + "...");
        isConnected = true;
        System.out.println("MySQL connection established!");
    }
    
    @Override
    public void disconnect() {
        System.out.println("Closing MySQL connection...");
        isConnected = false;
        System.out.println("MySQL disconnected!");
    }
    
    @Override
    public void executeQuery(String query) {
        if (isConnected) {
            System.out.println("Executing MySQL query: " + query);
        } else {
            System.out.println("Not connected to database!");
        }
    }
}

public class MongoDBConnection extends DatabaseConnection {
    private String database;
    
    public MongoDBConnection(String connectionString, String database) {
        super(connectionString);
        this.database = database;
    }
    
    @Override
    public void connect() {
        System.out.println("Connecting to MongoDB database: " + database + "...");
        isConnected = true;
        System.out.println("MongoDB connection established!");
    }
    
    @Override
    public void disconnect() {
        System.out.println("Closing MongoDB connection...");
        isConnected = false;
        System.out.println("MongoDB disconnected!");
    }
    
    @Override
    public void executeQuery(String query) {
        if (isConnected) {
            System.out.println("Executing MongoDB query: " + query);
        } else {
            System.out.println("Not connected to database!");
        }
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        DatabaseConnection mysql = new MySQLConnection("localhost", 3306);
        DatabaseConnection mongo = new MongoDBConnection("localhost:27017", "mydb");
        
        mysql.connect();
        mysql.logConnection();
        mysql.executeQuery("SELECT * FROM users");
        mysql.disconnect();
        
        System.out.println("\n---\n");
        
        mongo.connect();
        mongo.logConnection();
        mongo.executeQuery("db.users.find()");
        mongo.disconnect();
    }
}

Interfaces

Interfaces define a contract that classes must follow. They contain only abstract methods (until Java 8, which introduced default and static methods).

Example:

// Interface for drawable objects
public interface Drawable {
    void draw(); // Abstract method (implicitly public abstract)
    void resize(double scale);
}

// Interface for moveable objects
public interface Moveable {
    void move(int x, int y);
    int getX();
    int getY();
}

// Class implementing single interface
public class Circle implements Drawable {
    private double radius;
    private String color;
    
    public Circle(double radius, String color) {
        this.radius = radius;
        this.color = color;
    }
    
    @Override
    public void draw() {
        System.out.println("Drawing a " + color + " circle with radius " + radius);
    }
    
    @Override
    public void resize(double scale) {
        radius *= scale;
        System.out.println("Circle resized. New radius: " + radius);
    }
}

// Class implementing multiple interfaces
public class Rectangle implements Drawable, Moveable {
    private double width;
    private double height;
    private int x;
    private int y;
    private String color;
    
    public Rectangle(double width, double height, int x, int y, String color) {
        this.width = width;
        this.height = height;
        this.x = x;
        this.y = y;
        this.color = color;
    }
    
    @Override
    public void draw() {
        System.out.println("Drawing a " + color + " rectangle at (" + x + "," + y + ")");
        System.out.println("Dimensions: " + width + " x " + height);
    }
    
    @Override
    public void resize(double scale) {
        width *= scale;
        height *= scale;
        System.out.println("Rectangle resized to: " + width + " x " + height);
    }
    
    @Override
    public void move(int newX, int newY) {
        this.x = newX;
        this.y = newY;
        System.out.println("Rectangle moved to: (" + x + "," + y + ")");
    }
    
    @Override
    public int getX() {
        return x;
    }
    
    @Override
    public int getY() {
        return y;
    }
}

// Interface with default method (Java 8+)
public interface Printable {
    void print();
    
    // Default method with implementation
    default void printWithBorder() {
        System.out.println("====================");
        print();
        System.out.println("====================");
    }
    
    // Static method
    static void printHeader(String title) {
        System.out.println("=== " + title + " ===");
    }
}

public class Document implements Printable {
    private String content;
    
    public Document(String content) {
        this.content = content;
    }
    
    @Override
    public void print() {
        System.out.println(content);
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        Circle circle = new Circle(5.0, "Red");
        circle.draw();
        circle.resize(1.5);
        
        System.out.println("\n---\n");
        
        Rectangle rect = new Rectangle(10.0, 5.0, 0, 0, "Blue");
        rect.draw();
        rect.move(10, 20);
        rect.resize(2.0);
        
        System.out.println("\n---\n");
        
        Document doc = new Document("This is a sample document.");
        doc.print();
        doc.printWithBorder(); // Using default method
        Printable.printHeader("My Document"); // Using static method
    }
}

Composition vs Inheritance

Composition ("has-a" relationship) is often preferred over inheritance ("is-a" relationship) for better flexibility.

Example:

// Using Composition (Preferred approach)

// Component classes
public class Engine {
    private String type;
    private int horsepower;
    
    public Engine(String type, int horsepower) {
        this.type = type;
        this.horsepower = horsepower;
    }
    
    public void start() {
        System.out.println(type + " engine starting (" + horsepower + " HP)");
    }
    
    public void stop() {
        System.out.println("Engine stopping");
    }
}

public class Transmission {
    private String type;
    private int gears;
    
    public Transmission(String type, int gears) {
        this.type = type;
        this.gears = gears;
    }
    
    public void shiftGear(int gear) {
        System.out.println("Shifting to gear " + gear);
    }
}

public class GPS {
    public void navigate(String destination) {
        System.out.println("Navigating to: " + destination);
    }
}

// Main class using composition
public class Car {
    private String model;
    private Engine engine;           // Car HAS-A Engine
    private Transmission transmission; // Car HAS-A Transmission
    private GPS gps;                  // Car HAS-A GPS (optional)
    
    // Constructor injection
    public Car(String model, Engine engine, Transmission transmission) {
        this.model = model;
        this.engine = engine;
        this.transmission = transmission;
    }
    
    // Optional feature
    public void installGPS(GPS gps) {
        this.gps = gps;
    }
    
    public void start() {
        System.out.println("Starting " + model);
        engine.start();
    }
    
    public void drive(String destination) {
        start();
        transmission.shiftGear(1);
        if (gps != null) {
            gps.navigate(destination);
        }
        System.out.println("Driving to " + destination);
    }
    
    public void stop() {
        engine.stop();
        System.out.println(model + " stopped");
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        Engine v6Engine = new Engine("V6", 280);
        Transmission automatic = new Transmission("Automatic", 8);
        
        Car myCar = new Car("Toyota Camry", v6Engine, automatic);
        myCar.installGPS(new GPS());
        
        myCar.drive("Downtown");
        myCar.stop();
        
        System.out.println("\n---\n");
        
        // Easy to swap components
        Engine v8Engine = new Engine("V8", 450);
        Car sportsCar = new Car("Mustang", v8Engine, automatic);
        sportsCar.drive("Race Track");
        sportsCar.stop();
    }
}

Advantages of Composition:

  • More flexible: can change behavior at runtime
  • Loose coupling between classes
  • Easier to test and maintain
  • Can combine behaviors from multiple sources

When to use Inheritance:

  • Clear "is-a" relationship exists
  • Need to use polymorphism
  • Want to leverage existing code through extension

SOLID Principles

SOLID is an acronym for five design principles that make software more maintainable and flexible.

1. Single Responsibility Principle (SRP)

A class should have only one reason to change.

// BAD: Multiple responsibilities
public class Employee {
    private String name;
    private double salary;
    
    public void calculateSalary() { /* ... */ }
    public void saveToDatabase() { /* ... */ }  // Database responsibility
    public void generateReport() { /* ... */ }   // Reporting responsibility
}

// GOOD: Separate responsibilities
public class Employee {
    private String name;
    private double salary;
    
    public String getName() { return name; }
    public double getSalary() { return salary; }
    public void setSalary(double salary) { this.salary = salary; }
}

public class EmployeeRepository {
    public void save(Employee employee) {
        // Database logic here
        System.out.println("Saving employee to database...");
    }
    
    public Employee findById(int id) {
        // Database retrieval logic
        return null;
    }
}

public class SalaryCalculator {
    public double calculateSalary(Employee employee) {
        // Salary calculation logic
        return employee.getSalary() * 1.1; // 10% bonus
    }
}

public class EmployeeReportGenerator {
    public void generateReport(Employee employee) {
        // Report generation logic
        System.out.println("Generating report for: " + employee.getName());
    }
}

2. Open/Closed Principle (OCP)

Classes should be open for extension but closed for modification.

// BAD: Need to modify class to add new discount types
public class DiscountCalculator {
    public double calculateDiscount(String customerType, double amount) {
        if (customerType.equals("Regular")) {
            return amount * 0.05;
        } else if (customerType.equals("Premium")) {
            return amount * 0.10;
        } else if (customerType.equals("VIP")) {
            return amount * 0.20;
        }
        return 0;
    }
}

// GOOD: Use abstraction to extend without modifying
public interface DiscountStrategy {
    double calculateDiscount(double amount);
}

public class RegularDiscount implements DiscountStrategy {
    @Override
    public double calculateDiscount(double amount) {
        return amount * 0.05;
    }
}

public class PremiumDiscount implements DiscountStrategy {
    @Override
    public double calculateDiscount(double amount) {
        return amount * 0.10;
    }
}

public class VIPDiscount implements DiscountStrategy {
    @Override
    public double calculateDiscount(double amount) {
        return amount * 0.20;
    }
}

public class ShoppingCart {
    private DiscountStrategy discountStrategy;
    
    public ShoppingCart(DiscountStrategy discountStrategy) {
        this.discountStrategy = discountStrategy;
    }
    
    public double checkout(double amount) {
        double discount = discountStrategy.calculateDiscount(amount);
        return amount - discount;
    }
}

// Adding new discount type doesn't require modifying existing code
public class SeasonalDiscount implements DiscountStrategy {
    @Override
    public double calculateDiscount(double amount) {
        return amount * 0.15;
    }
}

3. Liskov Substitution Principle (LSP)

Objects of a superclass should be replaceable with objects of subclasses without breaking the application.

// BAD: Violates LSP
public class Bird {
    public void fly() {
        System.out.println("Flying...");
    }
}

public class Penguin extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("Penguins can't fly!");
    }
}

// GOOD: Proper abstraction respecting LSP
public abstract class Bird {
    public abstract void move();
    public abstract void eat();
}

public class FlyingBird extends Bird {
    @Override
    public void move() {
        fly();
    }
    
    public void fly() {
        System.out.println("Flying through the air");
    }
    
    @Override
    public void eat() {
        System.out.println("Eating seeds");
    }
}

public class Sparrow extends FlyingBird {
    @Override
    public void fly() {
        System.out.println("Sparrow flying swiftly");
    }
}

public class Penguin extends Bird {
    @Override
    public void move() {
        swim();
    }
    
    public void swim() {
        System.out.println("Swimming in water");
    }
    
    @Override
    public void eat() {
        System.out.println("Eating fish");
    }
}

// Usage - can replace Bird with any subclass
public class BirdWatcher {
    public void observeBird(Bird bird) {
        bird.move(); // Works correctly for all bird types
        bird.eat();
    }
}

4. Interface Segregation Principle (ISP)

Clients should not be forced to depend on interfaces they don't use.

// BAD: Fat interface forces unnecessary implementation
public interface Worker {
    void work();
    void eat();
    void sleep();
    void attendMeeting();
}

public class Robot implements Worker {
    @Override
    public void work() {
        System.out.println("Robot working");
    }
    
    @Override
    public void eat() {
        // Robots don't eat - forced to implement
        throw new UnsupportedOperationException();
    }
    
    @Override
    public void sleep() {
        // Robots don't sleep - forced to implement
        throw new UnsupportedOperationException();
    }
    
    @Override
    public void attendMeeting() {
        throw new UnsupportedOperationException();
    }
}

// GOOD: Segregated interfaces
public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

public interface Sleepable {
    void sleep();
}

public interface MeetingAttendee {
    void attendMeeting();
}

public class HumanWorker implements Workable, Eatable, Sleepable, MeetingAttendee {
    @Override
    public void work() {
        System.out.println("Human working");
    }
    
    @Override
    public void eat() {
        System.out.println("Human eating lunch");
    }
    
    @Override
    public void sleep() {
        System.out.println("Human sleeping");
    }
    
    @Override
    public void attendMeeting() {
        System.out.println("Human attending meeting");
    }
}

public class Robot implements Workable {
    @Override
    public void work() {
        System.out.println("Robot working 24/7");
    }
}

5. Dependency Inversion Principle (DIP)

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

// BAD: High-level class depends on low-level class
public class EmailService {
    public void sendEmail(String message) {
        System.out.println("Sending email: " + message);
    }
}

public class Notification {
    private EmailService emailService = new EmailService(); // Tight coupling
    
    public void send(String message) {
        emailService.sendEmail(message);
    }
}

// GOOD: Both depend on abstraction
public interface MessageService {
    void sendMessage(String message);
}

public class EmailService implements MessageService {
    @Override
    public void sendMessage(String message) {
        System.out.println("Sending email: " + message);
    }
}

public class SMSService implements MessageService {
    @Override
    public void sendMessage(String message) {
        System.out.println("Sending SMS: " + message);
    }
}

public class PushNotificationService implements MessageService {
    @Override
    public void sendMessage(String message) {
        System.out.println("Sending push notification: " + message);
    }
}

public class Notification {
    private MessageService messageService;
    
    // Dependency injection through constructor
    public Notification(MessageService messageService) {
        this.messageService = messageService;
    }
    
    public void send(String message) {
        messageService.sendMessage(message);
    }
}

// Usage - easy to switch implementations
public class Main {
    public static void main(String[] args) {
        Notification emailNotification = new Notification(new EmailService());
        emailNotification.send("Hello via Email");
        
        Notification smsNotification = new Notification(new SMSService());
        smsNotification.send("Hello via SMS");
        
        Notification pushNotification = new Notification(new PushNotificationService());
        pushNotification.send("Hello via Push");
    }
}

Design Patterns

Design patterns are reusable solutions to common software design problems.

1. Singleton Pattern

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

public class Database {
    // Private static instance
    private static Database instance;
    private String connectionString;
    
    // Private constructor prevents instantiation
    private Database() {
        connectionString = "jdbc:mysql://localhost:3306/mydb";
        System.out.println("Database connection initialized");
    }
    
    // Public method to get instance
    public static Database getInstance() {
        if (instance == null) {
            instance = new Database();
        }
        return instance;
    }
    
    public void query(String sql) {
        System.out.println("Executing: " + sql);
    }
}

// Thread-safe Singleton (better approach)
public class ThreadSafeDatabase {
    private static volatile ThreadSafeDatabase instance;
    private String connectionString;
    
    private ThreadSafeDatabase() {
        connectionString = "jdbc:mysql://localhost:3306/mydb";
    }
    
    public static ThreadSafeDatabase getInstance() {
        if (instance == null) {
            synchronized (ThreadSafeDatabase.class) {
                if (instance == null) {
                    instance = new ThreadSafeDatabase();
                }
            }
        }
        return instance;
    }
    
    public void query(String sql) {
        System.out.println("Executing: " + sql);
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        Database db1 = Database.getInstance();
        Database db2 = Database.getInstance();
        
        System.out.println(db1 == db2); // true - same instance
        
        db1.query("SELECT * FROM users");
    }
}

2. Factory Pattern

Creates objects without specifying the exact class to create.

// Product interface
public interface Vehicle {
    void drive();
    void stop();
}

// Concrete products
public class Car implements Vehicle {
    @Override
    public void drive() {
        System.out.println("Driving a car on the road");
    }
    
    @Override
    public void stop() {
        System.out.println("Car stopped");
    }
}

public class Bike implements Vehicle {
    @Override
    public void drive() {
        System.out.println("Riding a bike");
    }
    
    @Override
    public void stop() {
        System.out.println("Bike stopped");
    }
}

public class Truck implements Vehicle {
    @Override
    public void drive() {
        System.out.println("Driving a truck, hauling cargo");
    }
    
    @Override
    public void stop() {
        System.out.println("Truck stopped");
    }
}

// Factory class
public class VehicleFactory {
    public static Vehicle createVehicle(String type) {
        switch (type.toLowerCase()) {
            case "car":
                return new Car();
            case "bike":
                return new Bike();
            case "truck":
                return new Truck();
            default:
                throw new IllegalArgumentException("Unknown vehicle type: " + type);
        }
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        Vehicle car = VehicleFactory.createVehicle("car");
        car.drive();
        car.stop();
        
        Vehicle bike = VehicleFactory.createVehicle("bike");
        bike.drive();
        bike.stop();
    }
}

3. Builder Pattern

Constructs complex objects step by step.

public class Computer {
    // Required parameters
    private String cpu;
    private String ram;
    
    // Optional parameters
    private String storage;
    private String gpu;
    private boolean hasWifi;
    private boolean hasBluetooth;
    
    private Computer(ComputerBuilder builder) {
        this.cpu = builder.cpu;
        this.ram = builder.ram;
        this.storage = builder.storage;
        this.gpu = builder.gpu;
        this.hasWifi = builder.hasWifi;
        this.hasBluetooth = builder.hasBluetooth;
    }
    
    @Override
    public String toString() {
        return "Computer{" +
                "cpu='" + cpu + '\'' +
                ", ram='" + ram + '\'' +
                ", storage='" + storage + '\'' +
                ", gpu='" + gpu + '\'' +
                ", hasWifi=" + hasWifi +
                ", hasBluetooth=" + hasBluetooth +
                '}';
    }
    
    // Static nested Builder class
    public static class ComputerBuilder {
        // Required parameters
        private String cpu;
        private String ram;
        
        // Optional parameters - initialized to default values
        private String storage = "256GB SSD";
        private String gpu = "Integrated";
        private boolean hasWifi = false;
        private boolean hasBluetooth = false;
        
        public ComputerBuilder(String cpu, String ram) {
            this.cpu = cpu;
            this.ram = ram;
        }
        
        public ComputerBuilder storage(String storage) {
            this.storage = storage;
            return this;
        }
        
        public ComputerBuilder gpu(String gpu) {
            this.gpu = gpu;
            return this;
        }
        
        public ComputerBuilder wifi(boolean hasWifi) {
            this.hasWifi = hasWifi;
            return this;
        }
        
        public ComputerBuilder bluetooth(boolean hasBluetooth) {
            this.hasBluetooth = hasBluetooth;
            return this;
        }
        
        public Computer build() {
            return new Computer(this);
        }
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        // Build computer with only required parameters
        Computer basicComputer = new Computer.ComputerBuilder("Intel i5", "8GB")
                .build();
        System.out.println(basicComputer);
        
        // Build computer with all features
        Computer gamingComputer = new Computer.ComputerBuilder("Intel i9", "32GB")
                .storage("1TB NVMe SSD")
                .gpu("NVIDIA RTX 4090")
                .wifi(true)
                .bluetooth(true)
                .build();
        System.out.println(gamingComputer);
        
        // Build computer with some optional features
        Computer officeComputer = new Computer.ComputerBuilder("AMD Ryzen 5", "16GB")
                .storage("512GB SSD")
                .wifi(true)
                .build();
        System.out.println(officeComputer);
    }
}

4. Observer Pattern

Defines a one-to-many dependency where when one object changes state, all dependents are notified.

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

// Subject interface
public interface Subject {
    void attach(Observer observer);
    void detach(Observer observer);
    void notifyObservers();
}

// Observer interface
public interface Observer {
    void update(String news);
}

// Concrete Subject
public class NewsAgency implements Subject {
    private List<Observer> observers = new ArrayList<>();
    private String latestNews;
    
    @Override
    public void attach(Observer observer) {
        observers.add(observer);
    }
    
    @Override
    public void detach(Observer observer) {
        observers.remove(observer);
    }
    
    @Override
    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(latestNews);
        }
    }
    
    public void setNews(String news) {
        this.latestNews = news;
        System.out.println("\n[NEWS AGENCY] Breaking news published!");
        notifyObservers();
    }
}

// Concrete Observers
public class NewsChannel implements Observer {
    private String channelName;
    
    public NewsChannel(String channelName) {
        this.channelName = channelName;
    }
    
    @Override
    public void update(String news) {
        System.out.println(channelName + " received news: " + news);
    }
}

public class NewsWebsite implements Observer {
    private String websiteName;
    
    public NewsWebsite(String websiteName) {
        this.websiteName = websiteName;
    }
    
    @Override
    public void update(String news) {
        System.out.println(websiteName + " published: " + news);
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        NewsAgency agency = new NewsAgency();
        
        Observer cnn = new NewsChannel("CNN");
        Observer bbc = new NewsChannel("BBC");
        Observer website = new NewsWebsite("TechNews.com");
        
        // Subscribe observers
        agency.attach(cnn);
        agency.attach(bbc);
        agency.attach(website);
        
        // Publish news - all observers notified
        agency.setNews("Major earthquake hits California");
        
        // Unsubscribe one observer
        agency.detach(bbc);
        
        // Publish another news
        agency.setNews("New vaccine approved by FDA");
    }
}

5. Strategy Pattern

Defines a family of algorithms, encapsulates each one, and makes them interchangeable.

// Strategy interface
public interface PaymentStrategy {
    void pay(double amount);
}

// Concrete strategies
public class CreditCardPayment implements PaymentStrategy {
    private String cardNumber;
    private String cvv;
    
    public CreditCardPayment(String cardNumber, String cvv) {
        this.cardNumber = cardNumber;
        this.cvv = cvv;
    }
    
    @Override
    public void pay(double amount) {
        System.out.println("Paid $" + amount + " using Credit Card ending in " + 
                         cardNumber.substring(cardNumber.length() - 4));
    }
}

public class PayPalPayment implements PaymentStrategy {
    private String email;
    
    public PayPalPayment(String email) {
        this.email = email;
    }
    
    @Override
    public void pay(double amount) {
        System.out.println("Paid $" + amount + " using PayPal account: " + email);
    }
}

public class CryptoPayment implements PaymentStrategy {
    private String walletAddress;
    
    public CryptoPayment(String walletAddress) {
        this.walletAddress = walletAddress;
    }
    
    @Override
    public void pay(double amount) {
        System.out.println("Paid $" + amount + " using Crypto wallet: " + 
                         walletAddress.substring(0, 8) + "...");
    }
}

// Context class
public class ShoppingCart {
    private PaymentStrategy paymentStrategy;
    private double totalAmount;
    
    public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }
    
    public void addItem(double price) {
        totalAmount += price;
    }
    
    public void checkout() {
        if (paymentStrategy == null) {
            System.out.println("Please select a payment method");
            return;
        }
        paymentStrategy.pay(totalAmount);
        totalAmount = 0;
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        ShoppingCart cart = new ShoppingCart();
        
        // Add items
        cart.addItem(50.00);
        cart.addItem(30.00);
        cart.addItem(20.00);
        
        // Pay with credit card
        cart.setPaymentStrategy(new CreditCardPayment("1234-5678-9012-3456", "123"));
        cart.checkout();
        
        // New shopping session
        cart.addItem(75.00);
        cart.addItem(25.00);
        
        // Pay with PayPal
        cart.setPaymentStrategy(new PayPalPayment("user@example.com"));
        cart.checkout();
        
        // Another session
        cart.addItem(100.00);
        
        // Pay with Crypto
        cart.setPaymentStrategy(new CryptoPayment("1A2B3C4D5E6F7G8H9I0J"));
        cart.checkout();
    }
}

6. Decorator Pattern

Adds new functionality to objects dynamically without altering their structure.

// Component interface
public interface Coffee {
    String getDescription();
    double getCost();
}

// Concrete component
public class SimpleCoffee implements Coffee {
    @Override
    public String getDescription() {
        return "Simple Coffee";
    }
    
    @Override
    public double getCost() {
        return 2.00;
    }
}

// Abstract decorator
public abstract class CoffeeDecorator implements Coffee {
    protected Coffee coffee;
    
    public CoffeeDecorator(Coffee coffee) {
        this.coffee = coffee;
    }
    
    @Override
    public String getDescription() {
        return coffee.getDescription();
    }
    
    @Override
    public double getCost() {
        return coffee.getCost();
    }
}

// Concrete decorators
public class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }
    
    @Override
    public String getDescription() {
        return coffee.getDescription() + ", Milk";
    }
    
    @Override
    public double getCost() {
        return coffee.getCost() + 0.50;
    }
}

public class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }
    
    @Override
    public String getDescription() {
        return coffee.getDescription() + ", Sugar";
    }
    
    @Override
    public double getCost() {
        return coffee.getCost() + 0.25;
    }
}

public class VanillaDecorator extends CoffeeDecorator {
    public VanillaDecorator(Coffee coffee) {
        super(coffee);
    }
    
    @Override
    public String getDescription() {
        return coffee.getDescription() + ", Vanilla";
    }
    
    @Override
    public double getCost() {
        return coffee.getCost() + 0.75;
    }
}

public class CaramelDecorator extends CoffeeDecorator {
    public CaramelDecorator(Coffee coffee) {
        super(coffee);
    }
    
    @Override
    public String getDescription() {
        return coffee.getDescription() + ", Caramel";
    }
    
    @Override
    public double getCost() {
        return coffee.getCost() + 1.00;
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        // Simple coffee
        Coffee coffee1 = new SimpleCoffee();
        System.out.println(coffee1.getDescription() + " - $" + coffee1.getCost());
        
        // Coffee with milk
        Coffee coffee2 = new MilkDecorator(new SimpleCoffee());
        System.out.println(coffee2.getDescription() + " - $" + coffee2.getCost());
        
        // Coffee with milk and sugar
        Coffee coffee3 = new SugarDecorator(new MilkDecorator(new SimpleCoffee()));
        System.out.println(coffee3.getDescription() + " - $" + coffee3.getCost());
        
        // Fancy coffee with all additions
        Coffee coffee4 = new CaramelDecorator(
                            new VanillaDecorator(
                                new SugarDecorator(
                                    new MilkDecorator(
                                        new SimpleCoffee()))));
        System.out.println(coffee4.getDescription() + " - $" + coffee4.getCost());
    }
}

Best Practices

1. Naming Conventions

Follow Java naming conventions for better code readability.

// Classes: PascalCase (noun)
public class CustomerAccount { }
public class PaymentProcessor { }

// Interfaces: PascalCase (adjective or noun)
public interface Drawable { }
public interface PaymentMethod { }

// Methods: camelCase (verb)
public void calculateTotalPrice() { }
public boolean isValid() { }
public String getUserName() { }

// Variables: camelCase (noun)
private String firstName;
private int accountBalance;
private boolean isActive;

// Constants: UPPER_SNAKE_CASE
public static final int MAX_RETRY_ATTEMPTS = 3;
public static final String DEFAULT_CURRENCY = "USD";

// Packages: lowercase
package com.company.project.module;

2. Proper Encapsulation

public class BankAccount {
    // Private fields
    private double balance;
    private String accountNumber;
    
    // Constructor
    public BankAccount(String accountNumber, double initialBalance) {
        this.accountNumber = accountNumber;
        this.balance = initialBalance;
    }
    
    // Getter with validation
    public double getBalance() {
        return balance;
    }
    
    // Setter with validation
    public void deposit(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Deposit amount must be positive");
        }
        balance += amount;
    }
    
    public void withdraw(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Withdrawal amount must be positive");
        }
        if (amount > balance) {
            throw new IllegalArgumentException("Insufficient funds");
        }
        balance -= amount;
    }
    
    // Read-only property
    public String getAccountNumber() {
        return accountNumber;
    }
}

3. Favor Composition Over Inheritance

// Instead of deep inheritance hierarchies
public class Animal { }
public class Mammal extends Animal { }
public class Dog extends Mammal { }

// Use composition
public class Dog {
    private AnimalBehavior behavior;
    private DietType diet;
    private Habitat habitat;
    
    public Dog() {
        this.behavior = new MammalBehavior();
        this.diet = new CarnivoreDiet();
        this.habitat = new LandHabitat();
    }
}

4. Use Meaningful Names

// BAD
public class Mgr {
    private int d;
    private List<String> lst;
    
    public void proc() { }
}

// GOOD
public class CustomerManager {
    private int daysUntilExpiration;
    private List<String> customerEmailList;
    
    public void processCustomerOrders() { }
}

5. Keep Methods Small and Focused

// BAD: Method doing too much
public void processOrder(Order order) {
    // Validate order
    // Calculate total
    // Apply discounts
    // Process payment
    // Update inventory
    // Send confirmation email
    // Generate invoice
}

// GOOD: Break into smaller methods
public void processOrder(Order order) {
    validateOrder(order);
    double total = calculateTotal(order);
    total = applyDiscounts(total, order.getCustomer());
    processPayment(total, order.getPaymentMethod());
    updateInventory(order.getItems());
    sendConfirmationEmail(order.getCustomer());
    generateInvoice(order);
}

private void validateOrder(Order order) {
    if (order == null || order.getItems().isEmpty()) {
        throw new IllegalArgumentException("Invalid order");
    }
}

private double calculateTotal(Order order) {
    return order.getItems().stream()
        .mapToDouble(item -> item.getPrice() * item.getQuantity())
        .sum();
}

// ... other focused methods

6. Use Interfaces for Flexibility

// Program to interfaces, not implementations
public interface NotificationService {
    void send(String message, String recipient);
}

public class EmailNotificationService implements NotificationService {
    @Override
    public void send(String message, String recipient) {
        System.out.println("Sending email to " + recipient);
    }
}

public class SMSNotificationService implements NotificationService {
    @Override
    public void send(String message, String recipient) {
        System.out.println("Sending SMS to " + recipient);
    }
}

// Client code depends on interface
public class OrderService {
    private NotificationService notificationService;
    
    public OrderService(NotificationService notificationService) {
        this.notificationService = notificationService;
    }
    
    public void placeOrder(Order order) {
        // Process order...
        notificationService.send("Order confirmed", order.getCustomerEmail());
    }
}

7. Handle Exceptions Properly

public class FileProcessor {
    
    // Don't swallow exceptions
    public void processFile(String filename) {
        try {
            // File processing logic
            readFile(filename);
        } catch (IOException e) {
            // BAD: Empty catch block
            // e.printStackTrace(); // Also bad in production
            
            // GOOD: Log and/or rethrow
            System.err.println("Failed to process file: " + filename);
            throw new RuntimeException("File processing failed", e);
        }
    }
    
    // Use specific exceptions
    public void withdraw(double amount) throws InsufficientFundsException {
        if (amount > balance) {
            throw new InsufficientFundsException("Balance: " + balance + ", Requested: " + amount);
        }
        balance -= amount;
    }
    
    private double balance = 100.0;
    
    private void readFile(String filename) throws IOException {
        // File reading logic
    }
}

// Custom exception
public class InsufficientFundsException extends Exception {
    public InsufficientFundsException(String message) {
        super(message);
    }
}

8. Use Proper Access Modifiers

public class Example {
    // Most restrictive access that makes sense
    
    private String internalData;        // Only within this class
    
    protected void helperMethod() { }   // This class and subclasses
    
    void packageMethod() { }            // Within same package
    
    public void publicMethod() { }      // Everywhere
}

9. Avoid God Classes

// BAD: God class doing everything
public class Application {
    public void manageUsers() { }
    public void processPayments() { }
    public void sendEmails() { }
    public void generateReports() { }
    public void manageInventory() { }
    // ... 50 more methods
}

// GOOD: Separate concerns
public class UserManager {
    public void createUser(User user) { }
    public void deleteUser(String userId) { }
}

public class PaymentProcessor {
    public void processPayment(Payment payment) { }
    public void refundPayment(String paymentId) { }
}

public class EmailService {
    public void sendEmail(String to, String subject, String body) { }
}

public class ReportGenerator {
    public Report generateSalesReport(Date startDate, Date endDate) { }
}

10. Document Complex Logic

public class TaxCalculator {
    
    /**
     * Calculates the total tax based on income and deductions.
     * 
     * Tax brackets:
     * - 0-10,000: 10%
     * - 10,001-50,000: 20%
     * - 50,001+: 30%
     * 
     * @param income The gross income
     * @param deductions Total deductions
     * @return The calculated tax amount
     */
    public double calculateTax(double income, double deductions) {
        double taxableIncome = income - deductions;
        double tax = 0;
        
        if (taxableIncome <= 10000) {
            tax = taxableIncome * 0.10;
        } else if (taxableIncome <= 50000) {
            tax = 1000 + (taxableIncome - 10000) * 0.20;
        } else {
            tax = 9000 + (taxableIncome - 50000) * 0.30;
        }
        
        return tax;
    }
}

11. Use Enums for Fixed Sets of Constants

// Instead of string constants
public class OrderStatus {
    public static final String PENDING = "PENDING";
    public static final String PROCESSING = "PROCESSING";
    public static final String SHIPPED = "SHIPPED";
    public static final String DELIVERED = "DELIVERED";
}

// Use enum
public enum OrderStatus {
    PENDING("Order is pending"),
    PROCESSING("Order is being processed"),
    SHIPPED("Order has been shipped"),
    DELIVERED("Order has been delivered"),
    CANCELLED("Order was cancelled");
    
    private String description;
    
    OrderStatus(String description) {
        this.description = description;
    }
    
    public String getDescription() {
        return description;
    }
}

// Usage
public class Order {
    private OrderStatus status;
    
    public void updateStatus(OrderStatus newStatus) {
        this.status = newStatus;
        System.out.println("Status updated: " + newStatus.getDescription());
    }
}

public class Main {
    public static void main(String[] args) {
        Order order = new Order();
        order.updateStatus(OrderStatus.PENDING);
        order.updateStatus(OrderStatus.PROCESSING);
        order.updateStatus(OrderStatus.SHIPPED);
    }
}

12. Implement equals() and hashCode() Properly

import java.util.Objects;

public class Person {
    private String id;
    private String name;
    private int age;
    
    public Person(String id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        
        Person person = (Person) obj;
        return age == person.age &&
               Objects.equals(id, person.id) &&
               Objects.equals(name, person.name);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(id, name, age);
    }
    
    @Override
    public String toString() {
        return "Person{id='" + id + "', name='" + name + "', age=" + age + "}";
    }
}

13. Use Collections Effectively

import java.util.*;

public class CollectionExamples {
    
    // Use interface types for declarations
    public void goodPractice() {
        List<String> names = new ArrayList<>();        // Good
        Set<Integer> numbers = new HashSet<>();         // Good
        Map<String, Integer> ages = new HashMap<>();    // Good
        
        // Not: ArrayList<String> names = new ArrayList<>();
    }
    
    // Choose appropriate collection
    public void chooseWisely() {
        // Need fast indexed access? Use ArrayList
        List<String> list = new ArrayList<>();
        
        // Need unique elements? Use HashSet
        Set<String> uniqueItems = new HashSet<>();
        
        // Need sorted elements? Use TreeSet
        Set<String> sortedItems = new TreeSet<>();
        
        // Need key-value pairs? Use HashMap
        Map<String, Integer> keyValue = new HashMap<>();
        
        // Need thread-safe collection? Use concurrent collections
        List<String> threadSafeList = Collections.synchronizedList(new ArrayList<>());
    }
    
    // Use generics properly
    public <T> List<T> filterList(List<T> list, Predicate<T> condition) {
        List<T> result = new ArrayList<>();
        for (T item : list) {
            if (condition.test(item)) {
                result.add(item);
            }
        }
        return result;
    }
}

interface Predicate<T> {
    boolean test(T item);
}

14. Use Immutability When Possible

// Immutable class
public final class ImmutablePerson {
    private final String name;
    private final int age;
    private final List<String> hobbies;
    
    public ImmutablePerson(String name, int age, List<String> hobbies) {
        this.name = name;
        this.age = age;
        // Create defensive copy of mutable object
        this.hobbies = new ArrayList<>(hobbies);
    }
    
    public String getName() {
        return name;
    }
    
    public int getAge() {
        return age;
    }
    
    public List<String> getHobbies() {
        // Return unmodifiable view
        return Collections.unmodifiableList(hobbies);
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        List<String> hobbies = Arrays.asList("Reading", "Gaming");
        ImmutablePerson person = new ImmutablePerson("Alice", 25, hobbies);
        
        // Cannot modify original
        hobbies.set(0, "Cooking"); // Doesn't affect person object
        
        // Cannot modify returned list
        // person.getHobbies().add("Swimming"); // Throws exception
        
        System.out.println(person.getName()); // Thread-safe, no worries!
    }
}

15. Follow the Law of Demeter (Principle of Least Knowledge)

// BAD: Chain of method calls (violates Law of Demeter)
public class BadExample {
    public void processOrder(Customer customer) {
        String street = customer.getAddress().getStreet().getName();
        // Too much knowledge about internal structure
    }
}

// GOOD: Ask, don't navigate
public class GoodExample {
    public void processOrder(Customer customer) {
        String street = customer.getStreetName();
        // Customer handles the internal navigation
    }
}

public class Customer {
    private Address address;
    
    // Provide focused methods instead of exposing internal structure
    public String getStreetName() {
        return address != null ? address.getStreetName() : "Unknown";
    }
}

public class Address {
    private Street street;
    
    public String getStreetName() {
        return street != null ? street.getName() : "Unknown";
    }
}

public class Street {
    private String name;
    
    public String getName() {
        return name;
    }
}

Summary

Object-Oriented Programming in Java revolves around these key concepts:

Core Principles:

  • Encapsulation: Bundle data and methods, control access
  • Inheritance: Reuse code through parent-child relationships
  • Polymorphism: Use one interface for different types
  • Abstraction: Hide complexity, show only essentials

SOLID Principles:

  • Single Responsibility: One class, one purpose
  • Open/Closed: Open for extension, closed for modification
  • Liskov Substitution: Subclasses should be substitutable for parent classes
  • Interface Segregation: Many specific interfaces better than one general
  • Dependency Inversion: Depend on abstractions, not concrete classes

Best Practices:

  • Use meaningful names following conventions
  • Keep methods small and focused
  • Favor composition over inheritance
  • Program to interfaces, not implementations
  • Handle exceptions properly
  • Use appropriate access modifiers
  • Document complex logic
  • Write immutable classes when possible
  • Follow design patterns for common problems

Design Patterns Covered:

  • Singleton: Ensure single instance
  • Factory: Create objects without specifying exact class
  • Builder: Construct complex objects step by step
  • Observer: Notify dependents of state changes
  • Strategy: Encapsulate interchangeable algorithms
  • Decorator: Add functionality dynamically

By mastering these OOP concepts and applying them consistently, you'll write cleaner, more maintainable, and more scalable Java applications. Remember that good design is iterative – refactor and improve your code as you learn and as requirements evolve.


Additional Resources

  • Official Java Documentation: https://docs.oracle.com/en/java/
  • Effective Java by Joshua Bloch (Book)
  • Head First Design Patterns by Freeman & Freeman (Book)
  • Clean Code by Robert C. Martin (Book)
  • Refactoring by Martin Fowler (Book)

Practice these concepts regularly, review your code, and always strive to write code that is easy to understand, maintain, and extend

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment