Basic principles of OOP

Encapsulation

Encapsulation is one of the core principles of Object-Oriented Programming (OOP). It means hiding the internal state of an object and only exposing what’s necessary through methods. This helps protect data and keeps the code clean and safe.

Simple Example

Let’s create a Person class with a private property name, and we’ll use get and set methods to access and update it.

class Person {
  private name: string;

  constructor(name: string) {
    this.name = name;
  }

  // Method to get the name
  getName(): string {
    return this.name;
  }

  // Method to set a new name
  setName(newName: string): void {
    if (newName.length > 0) {
      this.name = newName;
    }
  }
}

We are encapsulating the name variable by making it private. Access is only allowed through get and set methods. This way, we control who accesses the data and how it’s modified. It protects the internal state of the object and adds a layer of security.

Inheritance

Inheritance is a fundamental principle of Object-Oriented Programming (OOP) that allows one class to inherit the properties and methods of another class. This means you can create a new class based on an existing class, reusing its functionality without rewriting code.

Inheritance promotes code reuse and helps organize related classes in a hierarchical way, making the code easier to maintain and extend.

Simple Example

Let’s create a Student class that extends the Person class, inheriting its methods and properties, and adding some student-specific information:

class Person {
  private name: string;

  constructor(name: string) {
    this.name = name;
  }

  // Method to get the name
  getName(): string {
    return this.name;
  }

  // Method to set a new name
  setName(newName: string): void {
    if (newName.length > 0) {
      this.name = newName;
    }
  }
}

class Student extends Person {
  private course: string;

  constructor(name: string, course: string) {
    super(name);
    this.course = course;
  }

  getCourse(): string {
    return this.course;
  }

  setCourse(course: string): void {
    if (course.length > 0) {
      this.course = course;
    }
  }
}

In this example, Student inherits from Person, so it automatically has the getName and setName methods without rewriting them. We only add what’s specific to Student, like the course property.

Polymorphism

Polymorphism allows objects of different classes related by inheritance to be treated as objects of a common superclass. This means you can write code that works on the superclass but behaves differently depending on the actual subclass instance.

Simple Example

Let’s create an array of Person that contains both Person and Student objects. Then, we call a method defined in Person, which each class can override or extend if needed.

class Person {
  protected name: string;

  constructor(name: string) {
    this.name = name;
  }

  getName(): string {
    return this.name;
  }

  describe(): string {
    return `Person: ${this.name}`;
  }
}

class Student extends Person {
  private course: string;

  constructor(name: string, course: string) {
    super(name);
    this.course = course;
  }

  describe(): string {
    return `Student: ${this.name}, Course: ${this.course}`;
  }
}

const people: Person[] = [
  new Person("Alice"),
  new Student("Bob", "Mathematics"),
];

people.forEach((person) => {
  console.log(person.describe());
});

In this example, although people is an array of Person, each object behaves according to its actual class when calling describe(). This is polymorphism in action, the method call resolves to the correct version depending on the object’s type.

Abstraction

Abstraction means hiding complex implementation details and showing only the essential features of an object.
It allows you to focus on what an object does, not how it does it.

In OOP, abstraction is often done using abstract classes. You define a general structure, and specific classes fill in the details.

Simple Example

Let’s make Person an abstract class. It will define a method describe(), but leave the implementation to the subclasses.

abstract class Person {
  protected name: string;

  constructor(name: string) {
    this.name = name;
  }

  getName(): string {
    return this.name;
  }

  abstract describe(): string;
}

class Student extends Person {
  private course: string;

  constructor(name: string, course: string) {
    super(name);
    this.course = course;
  }

  describe(): string {
    return `Student: ${this.name}, Course: ${this.course}`;
  }
}

In this example, Person is abstract, it can’t be instantiated directly. It defines the method describe(), but doesn’t implement it. The Student class provides the specific implementation.

This is abstraction: we define a common interface (describe()), and each subclass decides how it works.

Object-Oriented Programming (OOP) helps us build modular, reusable, and maintainable code by organizing it around real-world concepts.

By understanding and applying its four core principles, encapsulation, inheritance, polymorphism, and abstraction, you can design better software that’s easier to understand and extend.

Mastering these fundamentals is the first step toward writing clean and scalable applications.

More Posts

DRY Principle in Software Design photo

DRY Principle in Software Design

Don’t Repeat Yourself: Writing reusable and maintainable code

KISS Principle in Software Design photo

KISS Principle in Software Design

Keep It Simple, Stupid: How simplicity leads to better code

Tailwind CSS Utilities and Best Practices photo

Tailwind CSS Utilities and Best Practices

A practical guide to improving your Tailwind CSS workflow using `clsx`, `tailwind-merge`, and a custom `cn` utility. Learn how to write cleaner.