OOP Principles
How it works
- Input arrives
- Core component processes
- Result produced
- 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
- Define interfaces capturing core behaviors
- Implement variants behind interfaces
- Compose objects to achieve higher-level behavior
- 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...