OOP Basics

By the Syncfusion DevTutorials team · Last updated

Learn classes and objects, then the four big ideas of object-oriented code: encapsulation, abstraction, inheritance and polymorphism, with QuickBite examples you can run.

Look at a kitchen. You can see a fridge, a stove, and a mixer. Each one stores its own things inside and has a few buttons on the outside. You press "start" on the mixer. You do not open it and run the motor yourself.

Object-oriented programming works in a similar way. A program is made up of small parts called objects. Each object stores its own data and has a few buttons, called methods, on the outside.

In simple words: object-oriented programming (OOP) means building a program using small parts that take care of themselves.

This lesson explains the basics. The next lesson, SOLID, builds on all the ideas you learn here.

What You Will Learn

In this lesson, you will learn:

  1. What a class and an object are.
  2. Encapsulation: keeping data safe inside a class.
  3. Abstraction: showing what a class does while hiding how it works.
  4. Inheritance: creating a new class from an existing class.
  5. Polymorphism: using different classes in the same way.

The names in the pictures and the exercises in this lesson use TypeScript, because it runs directly in your browser. If you know Java or C#, the code will look familiar. The ideas are the same in any programming language that supports classes.

Classes and Objects

A class is a blueprint or plan. An object is a real thing created from that plan.

Think about a cookie cutter. You create the cutter once, and then you can make many cookies with it. Each cookie can have its own icing.

In programming, the class is the cookie cutter. Each object is a cookie. You create an object using the word new.

Your First Class

Here is a class that represents a dish on the QuickBite menu. It has two pieces of data: a name and a price. It also has one method called label(). Step through what happens when QuickBite makes two dishes from this one class:

Here is the same idea in code:

One class, two objects

class Dish {
  name: string;
  price: number;

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

  label(): string {
    return `${this.name} - Rs ${this.price}`;
  }
}

const dosa = new Dish("Masala dosa", 120);
const lassi = new Dish("Mango lassi", 80);

console.log(dosa.label());
console.log(lassi.label());

Expected output:

Masala dosa - Rs 120
Mango lassi - Rs 80

There is one class and two objects. Each object has its own name and price values. dosa.label() gives "Masala dosa - Rs 120" and lassi.label() gives "Mango lassi - Rs 80".

Fields, Methods and the Constructor

A class has three main parts:

  • Fields store data, such as name and price.
  • Methods perform actions, such as label().
  • The constructor runs once when you create an object using new. It sets the values of the fields.

The word this refers to the current object. So this.name means the name of this dish object, not the name of any other dish.

The Four Big Ideas

Four important ideas make classes useful. Each idea helps solve a different problem.

IdeaIn simple wordsQuickBite example
EncapsulationKeep data protected inside the classA wallet balance that cannot go below zero
AbstractionShow what it does and hide how it workspay(amount), without knowing which bank is used
InheritanceA new class reuses an existing classExpressDelivery builds on Delivery
PolymorphismDifferent classes can be used in the same wayCard, UPI and cash all use pay(amount)

Encapsulation: Protect the Data

A bank does not allow you to walk into the vault and change your balance yourself. Instead, you go to the counter, where the staff checks the rules before making any changes.

Encapsulation works in the same way. A class keeps its data private. Other code must use the class's methods to work with the data, and those methods make sure the rules are followed.

The Wallet Example

The Wallet class has a private balance field and two methods, add(amount) and spend(amount). The rule "you cannot spend more than you have" lives inside spend(). Watch two calls on a wallet that holds 300:

The code behind the animation:

Private data can only change through the class's own rules

class Wallet {
  private balance = 0;

  add(amount: number): void {
    if (amount <= 0) throw new Error("amount must be positive");
    this.balance += amount;
  }

  spend(amount: number): boolean {
    if (amount > this.balance) return false; // the rule lives here, once
    this.balance -= amount;
    return true;
  }

  get current(): number {
    return this.balance;
  }
}

const w = new Wallet();
w.add(300);
console.log(w.spend(500), w.current);
console.log(w.spend(120), w.current);

Expected output:

false 300
true 180

Here is what happened:

  • The wallet started with a balance of 300.
  • Spending 500 failed because there was not enough money in the wallet. spend returned false and the balance remained 300.
  • Spending 120 was successful. spend returned true and the balance became 180.

Why It Matters

If other code tries to write w.balance = -1000, TypeScript shows an error. This happens because the field is marked as private.

Without encapsulation, any part of the code could break the rule. You would then need to search through the entire application to find where the problem happened.

A public field can be changed by any code. This makes it easier for bugs to occur. Keep fields private unless you have a good reason to make them public.

Abstraction: Show What, Hide How

When you drive a car, you press the brake pedal. You do not need to know how the brake system works internally.

Abstraction works the same way in programming. A class provides a few simple methods to use, while the internal steps remain hidden.

The Payment Example

The checkout page only needs to call pay(amount). Finding the bank and writing logs are hidden helper methods. Follow one payment of 450 through the PaymentGateway class:

Here is the same idea in code:

The caller sees one method; the steps stay hidden

class PaymentGateway {
  // The one method other code is allowed to call.
  pay(amount: number): string {
    const bank = this.findBank();
    this.writeLog(`sending ${amount} to ${bank}`);
    return `paid ${amount}`;
  }

  // Hidden helpers. Other code cannot call these.
  private findBank(): string {
    return "QuickBank";
  }
  private writeLog(line: string): void {
    this.log.push(line);
  }
  private log: string[] = [];

  get lastLog(): string {
    return this.log[this.log.length - 1];
  }
}

const gateway = new PaymentGateway();
console.log(gateway.pay(450));
console.log(gateway.lastLog);

Expected output:

paid 450
sending 450 to QuickBank

The caller uses only one line: gateway.pay(450), and gets back "paid 450". It does not call findBank() or writeLog(). Since those methods are private, they cannot be accessed from outside the class.

Why It Matters

In the future, QuickBite might change to a different bank. In that case, only the hidden implementation needs to be updated. The checkout page can continue calling pay(amount) without any changes.

Without abstraction, every caller would need to know the banking steps. If the bank changes, all those callers would also need to be updated.

Inheritance: Build on an Existing Class

A child often shares many characteristics with a parent, but it can also have some differences.

Inheritance allows a new class to reuse everything from an existing class. The new class is called the child (or subclass), and the existing class is called the parent (or superclass). The child class can add new features or change existing behavior.

The Delivery Example

An express delivery is similar to a normal delivery, but it costs 30 more. So ExpressDelivery extends Delivery and changes only the fee() method. Follow one call to summary() on a 4 km express delivery:

The code behind the animation:

ExpressDelivery reuses Delivery and changes one thing

class Delivery {
  constructor(protected readonly distanceKm: number) {}

  fee(): number {
    return 30;
  }

  summary(): string {
    return `${this.distanceKm} km, fee ${this.fee()}`;
  }
}

class ExpressDelivery extends Delivery {
  // Only the fee is different. summary() is reused as it is.
  fee(): number {
    return super.fee() + 30;
  }
}

console.log(new Delivery(4).summary());
console.log(new ExpressDelivery(4).summary());

Expected output:

4 km, fee 30
4 km, fee 60

A normal 4 km delivery gives "4 km, fee 30". The express one gives "4 km, fee 60". ExpressDelivery did not copy the summary() method. It inherited it from Delivery automatically. The word super refers to the parent's version of a method, so super.fee() returns 30.

Use It With Care

Use inheritance only when the child class truly is a type of the parent class. An express delivery is a type of delivery, so inheritance makes sense here.

If the child class needs to change most of the parent's methods, inheritance is probably not the right choice. You will see an example of this in the SOLID lesson.

Polymorphism: Same Call, Different Results

Think about a TV remote. The power button works on both a Sony TV and a Samsung TV. You press the same button, but each TV responds in its own way.

In programming, an interface works like that remote. It defines the methods a class must provide, but it does not define how those methods should work. Multiple classes can follow the same interface and implement the methods in their own way.

The Payment Example

The PaymentMethod interface has one method: pay(amount). The classes Card, Upi and Cash all follow it. A loop goes through a list of one Card, one Upi and one Cash object and calls pay(450) on each. Step through it:

Here is the same idea in code:

One call, three behaviours

interface PaymentMethod {
  pay(amount: number): string;
}

class Card implements PaymentMethod {
  pay(amount: number): string { return `card charged ${amount}`; }
}
class Upi implements PaymentMethod {
  pay(amount: number): string { return `upi request ${amount}`; }
}
class Cash implements PaymentMethod {
  pay(amount: number): string { return `collect ${amount} in cash`; }
}

const methods: PaymentMethod[] = [new Card(), new Upi(), new Cash()];
for (const m of methods) console.log(m.pay(450));

Expected output:

card charged 450
upi request 450
collect 450 in cash

The loop calls pay(450) three times and gets "card charged 450", "upi request 450" and "collect 450 in cash". It does not check which payment method it is using. Each object responds in its own way.

Why It Matters

If you want to add a new payment method, you only need to create a new class. The loop does not need any changes.

In simple words: Polymorphism allows you to work with different objects in the same way.

Easy way to remember the four ideas: Encapsulation protects it. Abstraction hides it. Inheritance reuses it. Polymorphism replaces it. Think of "EAIP": protect, hide, reuse, replace.

Predict the Output

Here is a polymorphism question in C#. Read the code and try to predict the output before running it.

Common Mistakes

MistakeWrongCorrectWhy it happens
Public fieldsbalance can be changed from anywhere.Use private balance and update it through methods.Making fields public may seem easier because it requires less code.
Rules in many placesEvery caller checks if there is enough money.Keep the check in one place, inside spend().It can feel faster to add the check wherever it is needed.
Showing the inside stepsCallers use findBank() and writeLog() directly.Callers should use only pay(amount).In many languages, making everything public is the default behaviour.
Inheritance to borrow codeRider extends Restaurant just to reuse an address field.Use inheritance only when something is a kind of something else.Inheritance often looks like the easiest way to reuse code.
Type checks before every callif (m instanceof Card) ... else if (m instanceof Upi) ...Call m.pay(amount) and let each class handle its own behavior.Beginners may not realize that interfaces can handle this automatically.

Practice

What to Remember

  • A class is a blueprint or plan. An object is an actual thing created from that plan.
  • Encapsulation: keep data private and update it through methods.
  • Abstraction: provide simple methods and hide the internal details.
  • Inheritance: a child class reuses a parent class. Use it only when it represents an "is a kind of" relationship.
  • Polymorphism: use one method call, and each class responds in its own way.

Further Reading

These are the sources used to check this lesson:

Next: SOLID Principles. You will use these four ideas to fix a real QuickBite class.