DEV Community

Cover image for Inheritance in OOP: Solving Code Duplication or Creating New Problems?
Ashay Tiwari
Ashay Tiwari

Posted on

Inheritance in OOP: Solving Code Duplication or Creating New Problems?

Many developers learn:

Inheritance allows code reuse.

Technically true.

But if that's your primary reason for using inheritance, you'll often create bad designs.


❓ Why Was Inheritance Invented?

Imagine:

User
 ├── Customer
 ├── Admin
 └── Seller
Enter fullscreen mode Exit fullscreen mode

All of them have:

  • id
  • name
  • email
  • login()
  • logout()

Instead of duplicating these everywhere:

class Customer { ... }
class Admin { ... }
class Seller { ... }
Enter fullscreen mode Exit fullscreen mode

we extract common behavior:

class User {
  id: string;
  name: string;
  email: string;

  login() {}
  logout() {}
}
Enter fullscreen mode Exit fullscreen mode

Then:

class Customer extends User {}

class Admin extends User {}

class Seller extends User {}
Enter fullscreen mode Exit fullscreen mode

This creates an IS-A relationship.

Admin IS A User

Customer IS A User

Seller IS A User
Enter fullscreen mode Exit fullscreen mode

This is the most important rule of inheritance.

What Does extends Actually Do?

Consider:

class User {
  login() {
    console.log('login');
  }
}
Enter fullscreen mode Exit fullscreen mode
class Admin extends User {}
Enter fullscreen mode Exit fullscreen mode

Now:

const admin = new Admin();

admin.login();
Enter fullscreen mode Exit fullscreen mode

works.

Why?

Because JavaScript walks up the prototype chain:

admin
   ↓
Admin.prototype
   ↓
User.prototype
Enter fullscreen mode Exit fullscreen mode

and finds login().

Inheritance is fundamentally prototype delegation.

User
   ↓
Admin
Enter fullscreen mode Exit fullscreen mode

Admin has everything a User has, plus additional capabilities.

Example:

class User {
  login() {}
}
Enter fullscreen mode Exit fullscreen mode
class Admin extends User {
  banUser() {}
}
Enter fullscreen mode Exit fullscreen mode

Now:

admin.login();
admin.banUser();
Enter fullscreen mode Exit fullscreen mode

Overriding Methods

Children can change behavior.

class User {
  getRole() {
    return'User';
  }
}
Enter fullscreen mode Exit fullscreen mode
class Admin extends User {
  getRole() {
    return'Admin';
  }
}
Enter fullscreen mode Exit fullscreen mode

Now:

newUser().getRole();
// User

newAdmin().getRole();
// Admin
Enter fullscreen mode Exit fullscreen mode

Same method name.

Different behavior.

This becomes important later when we discuss polymorphism.


🤯 The Biggest Mistake with Inheritance

The Hidden Problem

Over time, developers started encountering a different challenge.

Imagine we have a Vehicle class:

class Vehicle {
  start() {}
  stop() {}
  lockDoors() {}
}
Enter fullscreen mode Exit fullscreen mode

Seems harmless.

But what happens when we introduce a Bicycle?

class Bicycle extends Vehicle {}
Enter fullscreen mode Exit fullscreen mode

Bicycles don't have doors.

Yet they've inherited door-related behavior.

Now our model starts feeling strange.

The problem becomes even worse as systems grow.

Developers often found themselves inheriting behavior they didn't actually need.

Changes in parent classes unexpectedly affected child classes.

The hierarchy became increasingly difficult to maintain.

What started as a solution to code duplication sometimes created tightly coupled systems.


⚔️ The Double-Edged Sword

Inheritance solves one problem very well:

Reusing common behavior.

But it can introduce another:

Tight coupling between parent and child classes.

The child depends heavily on decisions made by the parent.

As hierarchies become deeper, understanding the system becomes harder.

A small change in a base class can ripple through multiple child classes.

This is one reason why modern software design is often more cautious about using inheritance.


✅ When Inheritance Is Actually Good

Use inheritance when:

1. Real IS-A relationship exists

Admin IS A User

Manager IS An Employee

Dog IS An Animal
Enter fullscreen mode Exit fullscreen mode

2. Parent behavior is truly shared

class User {
  login() {}
  logout() {}
}
Enter fullscreen mode Exit fullscreen mode

All child classes genuinely need this.

3. Hierarchy is shallow

Good:

User
 ├─ Admin
 └─ Customer
Enter fullscreen mode Exit fullscreen mode

Bad:

User
 ↓
Employee
 ↓
Manager
 ↓
RegionalManager
 ↓
DistrictManager
 ↓
SuperDistrictManager
Enter fullscreen mode Exit fullscreen mode

⏭️ What's Next?

Inheritance helped developers reuse behavior.

But over time, many teams discovered that inheritance wasn't always the most flexible solution.

This led to an important shift in software design thinking:

Instead of inheriting behavior, what if we assembled behavior from smaller pieces?

In the next article, we'll explore Composition vs Inheritance and understand why many modern applications prefer composition when building complex systems.

Top comments (0)