OOP Principles

How it works

OOP Principles Overview
How it works
  1. Input arrives
  2. Core component processes
  3. Result produced
  4. Observe and iterate

Learning Objectives

By the end of this topic, you will understand:

  • Apply encapsulation to protect object invariants and reduce coupling
  • Use abstraction to hide complexity and create clean interfaces
  • Implement inheritance hierarchies that follow the Liskov Substitution Principle
  • Leverage polymorphism for flexible and extensible code design
  • Design object relationships that promote maintainability and testability

Real-World Examples

Java Collections Framework: Uses abstraction (List, Set, Map interfaces) and polymorphism to provide interchangeable implementations

Android Views: Inheritance hierarchy from View → ViewGroup → specific widgets, encapsulating UI behavior

Spring Framework: Dependency injection and polymorphism enable flexible, testable enterprise applications

React Components: Encapsulation of state and behavior, composition over inheritance patterns

Overview

Object-Oriented Programming (OOP) organizes software design around data, or objects, rather than functions and logic. The four pillars guide how we structure modules and their interactions.

Encapsulation

Bundle data with methods that operate on that data and hide internal representation.

  • Private fields + public methods
  • Minimize surface area
  • Protect invariants

Abstraction

Expose the essential behavior and hide the complexity through interfaces and base classes.

  • Program to interfaces
  • Hide implementation details
  • Replaceable components

Inheritance

Derive new classes from existing ones to reuse common behavior—use judiciously.

  • Prefer composition to inheritance
  • Use final/sealed where appropriate
  • Avoid deep hierarchies

Polymorphism

Same interface, different implementations, enabling flexible and extensible code.

  • Strategy/State patterns
  • Virtual methods / interfaces
  • Open/closed principle

Implementation Notes

  • Use private fields and methods to enforce encapsulation boundaries
  • Design interfaces that focus on behavior rather than implementation details
  • Prefer composition over inheritance to avoid tight coupling
  • Use factory patterns and dependency injection for flexible object creation
  • Apply the "Tell, Don't Ask" principle to maintain proper encapsulation

Best Practices

  • Design classes with single, well-defined responsibilities
  • Use meaningful names that clearly express intent and behavior
  • Keep inheritance hierarchies shallow and focused on "is-a" relationships
  • Favor immutable objects to reduce complexity and improve thread safety
  • Design for testability by minimizing dependencies and using interfaces

Common Pitfalls

  • Overusing inheritance leading to fragile base class problems
  • Breaking encapsulation by exposing internal state through getters/setters
  • Creating god objects that violate single responsibility principle
  • Implementing polymorphism without proper abstraction design
  • Using inheritance for code reuse instead of composition

How it works

  1. Define interfaces capturing core behaviors
  2. Implement variants behind interfaces
  3. Compose objects to achieve higher-level behavior
  4. Protect invariants via encapsulation

Code example

Demonstrating encapsulation, abstraction, inheritance, and polymorphism with a simple Shape hierarchy in TypeScript:
interface Shape { area(): number }

class Rectangle implements Shape {
  constructor(private width: number, private height: number) {}
  area(): number { return this.width * this.height }
}

class Circle implements Shape {
  constructor(private radius: number) {}
  area(): number { return Math.PI * this.radius * this.radius }
}

// Polymorphism: both shapes implement the same interface
const shapes: Shape[] = [new Rectangle(2, 3), new Circle(1)];
const totalArea = shapes.reduce(function (sum, s) { return sum + s.area(); }, 0);
// totalArea = 6 + 3.1415...