Complete OOP Guide: Principles, Relationships & Real-World Examples
Master Object-Oriented Programming with detailed explanations of IS-A, HAS-A, USES-A relationships, Aggregation, Composition, Associations, and SOLID principles with real-world examples and diagrams.
Complete Object-Oriented Programming (OOP) Guide
📋 Table of Contents
- Introduction to OOP
- Core OOP Concepts
- OOP Relationships
- Real-World Examples
- SOLID Principles
- Best Practices
- Interview Questions
Introduction to OOP
Object-Oriented Programming (OOP) is a programming paradigm that organizes software design around objects rather than functions and logic.
Why OOP?
Benefits:
- Code Reusability
- Modularity
- Flexibility
- Maintainability
- Security through Encapsulation
Core OOP Concepts
1. Class and Object
Class: The Blueprint
public class BankAccount {
private String accountNumber;
private double balance;
public BankAccount(String accountNumber) {
this.accountNumber = accountNumber;
this.balance = 0.0;
}
public void deposit(double amount) {
balance += amount;
}
public double getBalance() {
return balance;
}
}
Object: The Instance
BankAccount account1 = new BankAccount("ACC001");
BankAccount account2 = new BankAccount("ACC002");
2. Encapsulation
Definition: Bundling data and methods together, restricting direct access.
public class Employee {
private String name;
private double salary;
public void setSalary(double salary) {
if (salary > 0) {
this.salary = salary;
}
}
public double getSalary() {
return salary;
}
}
3. Inheritance (IS-A Relationship)
Definition: Child class inherits properties from parent class.
public class Vehicle {
protected String brand;
public void start() {
System.out.println("Vehicle starting");
}
}
public class Car extends Vehicle {
private int doors;
public void honk() {
System.out.println("Beep beep!");
}
}
4. Polymorphism
Method Overloading (Compile-time)
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
}
Method Overriding (Runtime)
public class Payment {
public void process() {
System.out.println("Processing payment");
}
}
public class CreditCardPayment extends Payment {
@Override
public void process() {
System.out.println("Processing credit card payment");
}
}
5. Abstraction
Definition: Hiding implementation details, showing only essential features.
public abstract class Shape {
abstract double calculateArea();
public void display() {
System.out.println("Area: " + calculateArea());
}
}
public class Circle extends Shape {
private double radius;
@Override
double calculateArea() {
return Math.PI * radius * radius;
}
}
OOP Relationships
1. IS-A Relationship (Inheritance)
Example: Dog IS-A Animal
public class Animal {
public void eat() {
System.out.println("Eating");
}
}
public class Dog extends Animal {
public void bark() {
System.out.println("Barking");
}
}
Diagram:
Animal
│
├── Dog
├── Cat
└── Bird
2. HAS-A Relationship
Composition (Strong)
Example: Car HAS-A Engine
public class Engine {
public void start() {
System.out.println("Engine started");
}
}
public class Car {
private Engine engine;
public Car() {
this.engine = new Engine(); // Created with Car
}
}
Aggregation (Weak)
Example: Department HAS-A Student
public class Student {
private String name;
}
public class Department {
private List<Student> students; // Students exist independently
public void addStudent(Student student) {
students.add(student);
}
}
Diagram:
Composition:
┌─────┐
│ Car │──■ Engine (dies with Car)
└─────┘
Aggregation:
┌────────────┐
│ Department │◇─── Student (exists independently)
└────────────┘
3. USES-A Relationship (Dependency)
Example: Chef USES-A Recipe
public class Recipe {
private String name;
}
public class Chef {
public void cook(Recipe recipe) {
System.out.println("Cooking " + recipe.getName());
}
}
4. Association
Example: Doctor ↔ Patient (Many-to-Many)
public class Doctor {
private List<Patient> patients;
public void assignPatient(Patient patient) {
patients.add(patient);
}
}
public class Patient {
private List<Doctor> doctors;
public void assignDoctor(Doctor doctor) {
doctors.add(doctor);
}
}
5. Generalization
Definition: Extracting common features to create a parent class.
// Generalized from FullTimeEmployee, PartTimeEmployee, Contractor
public abstract class Employee {
protected String name;
protected double salary;
public abstract double calculateSalary();
}
Real-World Examples
Example 1: E-Commerce System
// Product hierarchy
public abstract class Product {
protected String id;
protected String name;
protected double price;
public abstract void displayDetails();
}
public class Electronics extends Product {
private int warrantyMonths;
@Override
public void displayDetails() {
System.out.println(name + " - $" + price);
System.out.println("Warranty: " + warrantyMonths + " months");
}
}
public class Clothing extends Product {
private String size;
@Override
public void displayDetails() {
System.out.println(name + " - $" + price);
System.out.println("Size: " + size);
}
}
// Shopping Cart (Aggregation)
public class ShoppingCart {
private List<Product> products;
public void addProduct(Product product) {
products.add(product);
}
public double calculateTotal() {
return products.stream()
.mapToDouble(p -> p.price)
.sum();
}
}
// Order (Composition)
public class Order {
private String orderId;
private ShoppingCart cart;
private Payment payment;
public Order(String orderId) {
this.orderId = orderId;
this.cart = new ShoppingCart();
}
public void processOrder(Payment payment) {
payment.process(cart.calculateTotal());
}
}
Example 2: Library Management System
// Book (Entity)
public class Book {
private String isbn;
private String title;
private String author;
private boolean isAvailable;
public void checkOut() {
isAvailable = false;
}
public void returnBook() {
isAvailable = true;
}
}
// Member (Entity)
public class Member {
private String memberId;
private String name;
private List<Book> borrowedBooks;
public void borrowBook(Book book) {
if (book.isAvailable()) {
borrowedBooks.add(book);
book.checkOut();
}
}
}
// Library (Aggregation)
public class Library {
private List<Book> books;
private List<Member> members;
public void addBook(Book book) {
books.add(book);
}
public void registerMember(Member member) {
members.add(member);
}
}
Example 3: Hospital Management System
// Person (Generalization)
public abstract class Person {
protected String id;
protected String name;
protected int age;
public abstract void displayInfo();
}
// Doctor IS-A Person
public class Doctor extends Person {
private String specialization;
private List<Patient> patients;
@Override
public void displayInfo() {
System.out.println("Dr. " + name + " - " + specialization);
}
public void diagnose(Patient patient) {
System.out.println("Diagnosing " + patient.getName());
}
}
// Patient IS-A Person
public class Patient extends Person {
private String condition;
private List<Doctor> doctors;
@Override
public void displayInfo() {
System.out.println("Patient: " + name + " - " + condition);
}
}
// Appointment (Association)
public class Appointment {
private Doctor doctor;
private Patient patient;
private LocalDateTime dateTime;
public void schedule(Doctor doctor, Patient patient, LocalDateTime dateTime) {
this.doctor = doctor;
this.patient = patient;
this.dateTime = dateTime;
}
}
SOLID Principles
SOLID is an acronym for five design principles that make software designs more understandable, flexible, and maintainable.
1. Single Responsibility Principle (SRP)
Definition: A class should have only ONE reason to change, meaning it should have only ONE job or responsibility.
Why It Matters:
- ✅ Easier to understand and maintain
- ✅ Reduces coupling between different parts
- ✅ Makes testing simpler
- ✅ Changes in one responsibility don't affect others
Real-World Example: User Management System
❌ Bad - Violates SRP:
// This class has TOO MANY responsibilities!
public class User {
private String username;
private String email;
// Responsibility 1: Data management
public void setUsername(String username) { this.username = username; }
// Responsibility 2: Validation
public boolean validateEmail(String email) {
return email.contains("@") && email.contains(".");
}
// Responsibility 3: Database operations
public void saveToDatabase() {
// SQL code here
}
// Responsibility 4: Email sending
public void sendWelcomeEmail() {
// Email sending code
}
// Responsibility 5: Report generation
public String generateReport() {
return "User Report: " + username;
}
}
✅ Good - Follows SRP:
// 1. User entity - Only manages user data
public class User {
private String username;
private String email;
public User(String username, String email) {
this.username = username;
this.email = email;
}
public String getUsername() { return username; }
public String getEmail() { return email; }
}
// 2. Email validator - Only validates emails
public class EmailValidator {
public boolean validate(String email) {
return email != null &&
email.contains("@") &&
email.contains(".");
}
}
// 3. User repository - Only handles database
public class UserRepository {
public void save(User user) {
// Database save logic
}
public User findByUsername(String username) {
// Database query logic
return null;
}
}
// 4. Email service - Only sends emails
public class EmailService {
public void sendWelcomeEmail(User user) {
String message = "Welcome " + user.getUsername();
// Send email logic
}
}
// 5. Report generator - Only generates reports
public class UserReportGenerator {
public String generate(User user) {
return "User Report: " + user.getUsername();
}
}
Benefits:
| Benefit | Description |
|---|---|
| Maintainability | Email changes don't affect database code |
| Testability | Can test EmailValidator independently |
| Reusability | EmailService can be used elsewhere |
| Clarity | Each class has one clear purpose |
2. Open/Closed Principle (OCP)
Definition: Software entities should be OPEN for extension but CLOSED for modification.
Why It Matters:
- ✅ Add new functionality without changing existing code
- ✅ Reduces risk of breaking existing features
- ✅ Promotes code stability
- ✅ Enables plugin architectures
Real-World Example: Payment Processing
❌ Bad - Requires Modification:
// Must MODIFY this class for every new payment method
public class PaymentProcessor {
public void process(String type, double amount) {
if (type.equals("CREDIT_CARD")) {
System.out.println("Processing credit card: $" + amount);
// Credit card logic
} else if (type.equals("PAYPAL")) {
System.out.println("Processing PayPal: $" + amount);
// PayPal logic
} else if (type.equals("BITCOIN")) {
// Need to MODIFY class again!
System.out.println("Processing Bitcoin: $" + amount);
}
}
}
✅ Good - Extension Without Modification:
// 1. Define abstraction
public interface PaymentMethod {
void processPayment(double amount);
String getName();
}
// 2. Implement concrete methods
public class CreditCardPayment implements PaymentMethod {
private String cardNumber;
@Override
public void processPayment(double amount) {
System.out.println("Processing Credit Card: $" + amount);
validateCard();
contactGateway();
}
@Override
public String getName() { return "Credit Card"; }
private void validateCard() { /* validation */ }
private void contactGateway() { /* gateway call */ }
}
public class PayPalPayment implements PaymentMethod {
private String email;
@Override
public void processPayment(double amount) {
System.out.println("Processing PayPal: $" + amount);
redirectToPayPal();
}
@Override
public String getName() { return "PayPal"; }
private void redirectToPayPal() { /* redirect */ }
}
// NEW payment method - NO modification needed!
public class BitcoinPayment implements PaymentMethod {
@Override
public void processPayment(double amount) {
System.out.println("Processing Bitcoin: $" + amount);
generateWallet();
}
@Override
public String getName() { return "Bitcoin"; }
private void generateWallet() { /* wallet */ }
}
// 3. Processor - CLOSED for modification
public class PaymentProcessor {
public void process(PaymentMethod method, double amount) {
System.out.println("Using: " + method.getName());
method.processPayment(amount);
}
}
Benefits:
| Benefit | Description |
|---|---|
| Stability | Existing code remains untouched |
| Extensibility | Easy to add new payment methods |
| Testing | Existing tests don't need changes |
| Plugin Support | Dynamic loading of implementations |
3. Liskov Substitution Principle (LSP)
Definition: Objects of a superclass should be replaceable with objects of its subclasses without breaking the application.
Why It Matters:
- ✅ Ensures inheritance is used correctly
- ✅ Prevents unexpected behavior
- ✅ Maintains contract consistency
- ✅ Enables safe polymorphism
Real-World Example: Shape Hierarchy
❌ Bad - Violates LSP:
public class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int getArea() { return width * height; }
}
// Square violates LSP!
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // Forces equal sides
}
@Override
public void setHeight(int height) {
this.width = height; // Forces equal sides
this.height = height;
}
}
// This breaks!
Rectangle rect = new Square();
rect.setWidth(5);
rect.setHeight(10);
System.out.println(rect.getArea()); // Expected 50, got 100!
✅ Good - Follows LSP:
// 1. Create proper abstraction
public interface Shape {
double calculateArea();
String getType();
}
// 2. Independent implementations
public class Rectangle implements Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double calculateArea() {
return width * height;
}
@Override
public String getType() { return "Rectangle"; }
}
public class Square implements Shape {
private double side;
public Square(double side) {
this.side = side;
}
@Override
public double calculateArea() {
return side * side;
}
@Override
public String getType() { return "Square"; }
}
// Works correctly!
Shape rect = new Rectangle(5, 10);
Shape square = new Square(5);
System.out.println(rect.calculateArea()); // 50 ✓
System.out.println(square.calculateArea()); // 25 ✓
Benefits:
| Benefit | Description |
|---|---|
| Predictability | Subclasses behave as expected |
| Reliability | No surprises when substituting |
| Polymorphism | Safe to use base references |
| Maintainability | Clear contracts |
4. Interface Segregation Principle (ISP)
Definition: Clients should not be forced to depend on interfaces they don't use.
Why It Matters:
- ✅ Prevents fat interfaces
- ✅ Reduces coupling
- ✅ Improves flexibility
- ✅ Makes code maintainable
Real-World Example: Worker System
❌ Bad - Fat Interface:
// Forces all workers to implement everything!
public interface Worker {
void work();
void eat();
void sleep();
void attendMeeting();
void submitTimesheet();
}
public class HumanWorker implements Worker {
// Can implement all
public void work() { }
public void eat() { }
public void sleep() { }
public void attendMeeting() { }
public void submitTimesheet() { }
}
public class RobotWorker implements Worker {
public void work() { }
// Forced to implement methods it doesn't need!
public void eat() {
throw new UnsupportedOperationException();
}
public void sleep() {
throw new UnsupportedOperationException();
}
public void attendMeeting() {
throw new UnsupportedOperationException();
}
public void submitTimesheet() {
throw new UnsupportedOperationException();
}
}
✅ Good - Segregated Interfaces:
// 1. Focused interfaces
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public interface Sleepable {
void sleep();
}
public interface Meetable {
void attendMeeting();
}
// 2. Implement only what's needed
public class HumanWorker implements Workable, Eatable, Sleepable, Meetable {
@Override
public void work() {
System.out.println("Human working");
}
@Override
public void eat() {
System.out.println("Human eating");
}
@Override
public void sleep() {
System.out.println("Human sleeping");
}
@Override
public void attendMeeting() {
System.out.println("Human in meeting");
}
}
public class RobotWorker implements Workable {
@Override
public void work() {
System.out.println("Robot working 24/7");
}
// No need to implement eat, sleep, attendMeeting!
}
public class Manager implements Workable, Meetable {
@Override
public void work() {
System.out.println("Manager managing");
}
@Override
public void attendMeeting() {
System.out.println("Manager leading meeting");
}
}
Benefits:
| Benefit | Description |
|---|---|
| Flexibility | Classes implement only what they need |
| Maintainability | Changes to one interface don't affect others |
| Clarity | Clear, focused interfaces |
| Reusability | Small interfaces are more reusable |
5. Dependency Inversion Principle (DIP)
Definition:
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
Why It Matters:
- ✅ Reduces coupling
- ✅ Increases flexibility
- ✅ Enables dependency injection
- ✅ Makes testing easier
Real-World Example: Notification System
❌ Bad - High-Level Depends on Low-Level:
// Low-level module
public class EmailService {
public void sendEmail(String to, String message) {
System.out.println("Sending email to: " + to);
}
}
// High-level module DIRECTLY depends on low-level
public class UserService {
private EmailService emailService = new EmailService(); // Tight coupling!
public void registerUser(String username, String email) {
// Registration logic
emailService.sendEmail(email, "Welcome!");
// Cannot switch to SMS without modifying this class!
}
}
✅ Good - Depend on Abstractions:
// 1. Define abstraction
public interface NotificationService {
void send(String recipient, String message);
String getType();
}
// 2. Low-level implementations
public class EmailService implements NotificationService {
@Override
public void send(String recipient, String message) {
System.out.println("Email to " + recipient + ": " + message);
}
@Override
public String getType() { return "Email"; }
}
public class SMSService implements NotificationService {
@Override
public void send(String recipient, String message) {
System.out.println("SMS to " + recipient + ": " + message);
}
@Override
public String getType() { return "SMS"; }
}
public class PushNotificationService implements NotificationService {
@Override
public void send(String recipient, String message) {
System.out.println("Push to " + recipient + ": " + message);
}
@Override
public String getType() { return "Push"; }
}
// 3. High-level depends on abstraction (Dependency Injection)
public class UserService {
private NotificationService notificationService;
// Dependency injected via constructor
public UserService(NotificationService notificationService) {
this.notificationService = notificationService;
}
public void registerUser(String username, String email) {
// Registration logic
System.out.println("Registering: " + username);
notificationService.send(email, "Welcome " + username + "!");
System.out.println("Used: " + notificationService.getType());
}
}
// 4. Usage - Easy to switch implementations!
public class Application {
public static void main(String[] args) {
// Use Email
UserService service1 = new UserService(new EmailService());
service1.registerUser("john", "[email protected]");
// Switch to SMS - NO code changes in UserService!
UserService service2 = new UserService(new SMSService());
service2.registerUser("jane", "+1234567890");
// Switch to Push - NO code changes in UserService!
UserService service3 = new UserService(new PushNotificationService());
service3.registerUser("bob", "device-token-123");
}
}
Benefits:
| Benefit | Description |
|---|---|
| Flexibility | Easy to swap implementations |
| Testability | Can inject mock objects for testing |
| Maintainability | Changes isolated to implementations |
| Decoupling | High-level code independent of details |
SOLID Principles Summary
| Principle | Key Point | Benefit |
|---|---|---|
| SRP | One class, one responsibility | Easy to maintain |
| OCP | Open for extension, closed for modification | Stable codebase |
| LSP | Subtypes must be substitutable | Safe polymorphism |
| ISP | No fat interfaces | Flexible design |
| DIP | Depend on abstractions | Loose coupling |
When to Apply SOLID:
- ✅ Designing new systems
- ✅ Refactoring existing code
- ✅ Building scalable applications
- ✅ Creating reusable libraries
Remember: SOLID principles are guidelines, not strict rules. Apply them judiciously based on your specific needs!
Best Practices
1. Favor Composition Over Inheritance
// Instead of inheritance
public class Bird {
public void fly() { }
}
public class Penguin extends Bird {
// Problem: Penguins can't fly!
}
// Use composition
public interface Flyable {
void fly();
}
public class Bird {
private Flyable flyBehavior;
public void performFly() {
if (flyBehavior != null) {
flyBehavior.fly();
}
}
}
2. Program to Interface, Not Implementation
// Good
List<String> names = new ArrayList<>();
// Instead of
ArrayList<String> names = new ArrayList<>();
3. Keep Classes Focused
// Each class has one clear purpose
public class User { }
public class UserValidator { }
public class UserRepository { }
public class UserService { }
Interview Questions
Q1: What are the four pillars of OOP?
Answer:
- Encapsulation - Data hiding
- Inheritance - Code reuse
- Polymorphism - Many forms
- Abstraction - Hide complexity
Q2: Difference between Composition and Aggregation?
Answer:
| Aspect | Composition | Aggregation |
|---|---|---|
| Relationship | Strong HAS-A | Weak HAS-A |
| Lifetime | Dependent | Independent |
| Example | Car-Engine | Department-Student |
Q3: What is the difference between IS-A and HAS-A?
Answer:
- IS-A (Inheritance): Dog IS-A Animal
- HAS-A (Composition/Aggregation): Car HAS-A Engine
Q4: Explain method overloading vs overriding?
Answer:
Overloading (Compile-time):
public int add(int a, int b) { }
public double add(double a, double b) { }
Overriding (Runtime):
class Parent {
void display() { }
}
class Child extends Parent {
@Override
void display() { }
}
Q5: What is the Diamond Problem?
Answer: The diamond problem occurs in multiple inheritance when a class inherits from two classes that have a common parent.
Java solves this by:
- Not allowing multiple class inheritance
- Allowing multiple interface implementation
- Using default methods in interfaces (Java 8+)
Q6: Explain SOLID principles briefly?
Answer:
- S - Single Responsibility
- O - Open/Closed
- L - Liskov Substitution
- I - Interface Segregation
- D - Dependency Inversion
Q7: When to use abstract class vs interface?
Answer:
Abstract Class:
- Common code to share
- Related classes
- Non-public members needed
Interface:
- Unrelated classes
- Multiple inheritance needed
- Define contract only
Q8: What is tight coupling vs loose coupling?
Answer:
Tight Coupling:
public class A {
private B b = new B(); // Tightly coupled
}
Loose Coupling:
public class A {
private IB b; // Loosely coupled through interface
public A(IB b) {
this.b = b;
}
}
Q9: Explain Association, Aggregation, and Composition?
Answer:
Association: General relationship
Doctor ↔ Patient
Aggregation: Weak HAS-A (independent lifecycle)
Department ◇─ Student
Composition: Strong HAS-A (dependent lifecycle)
Car ──■ Engine
Q10: What is the purpose of the final keyword?
Answer:
- final variable: Cannot be reassigned
- final method: Cannot be overridden
- final class: Cannot be inherited
public final class ImmutableClass {
private final int value;
public ImmutableClass(int value) {
this.value = value;
}
}
Summary
Key Takeaways
- OOP organizes code around objects
- Four pillars: Encapsulation, Inheritance, Polymorphism, Abstraction
- Relationships: IS-A, HAS-A, USES-A, Association
- SOLID principles guide good design
- Favor composition over inheritance
- Program to interfaces
Relationship Summary
| Relationship | Type | Example | Diagram |
|---|---|---|---|
| IS-A | Inheritance | Dog IS-A Animal | Parent → Child |
| HAS-A (Strong) | Composition | Car HAS-A Engine | Container ──■ Part |
| HAS-A (Weak) | Aggregation | Dept HAS-A Student | Container ◇─ Part |
| USES-A | Dependency | Chef USES-A Recipe | User ----→ Used |
| Association | Relationship | Doctor ↔ Patient | A ◆────◆ B |
Next Steps:
- Practice with real-world projects
- Study design patterns
- Review SOLID principles
- Build portfolio projects