Inheritance Is Poisoning Your Code. Stop Overusing It.

Ahmed EBEN HASSINE
4 min readNov 18, 2024

--

Inheritance is often considered a cornerstone of object-oriented programming, but it can introduce complexities in code and conflict with SOLID principles, resulting in challenges such as tight coupling, difficulties in testing, and systems that are less maintainable and harder to evolve over time.

The Real Meaning of Inheritance

Inheritance is often touted as one of the core features of object-oriented programming (OOP). It allows child classes to inherit properties and methods from parent classes, which seems like a great way to reduce duplication and introduce polymorphism. But here’s the thing: inheritance, in its most common usage, doesn’t always align with Alan Kay’s original vision for OOP. Alan Kay, the creator of the OOP paradigm, believed that inheritance was supposed to model real-world relationships. Today, however, it is frequently misused, leading to designs that are more complex and harder to maintain.

Like a well-kept natural environment, clean and well-designed software grows with ease and evolves efficiently

How Inheritance Can Conflict with SOLID Principles

The SOLID principles were created to guide developers towards building maintainable, scalable, and flexible systems 🔧. Unfortunately, inheritance can undermine these principles, causing issues that often lead to messy code. Let’s break it down:

1. Single Responsibility Principle (SRP)

Inheritance can force parent classes to take on multiple responsibilities in order to serve the needs of all child classes. For instance, a User class used as a base for AdminUser or CustomerUser may end up doing more than it should. This results in a class that’s hard to maintain and harder to modify without affecting other parts of the code.

2. Open/Closed Principle (OCP)

"Let’s be clear that being 'open for extension' does not mean using 'extends' keyword."

Inheritance often requires you to modify the parent class to extend its functionality, which directly contradicts the Open/Closed Principle. This principle states that classes should be open for extension but closed for modification. When inheritance is overused, this becomes impossible.

3. Liskov Substitution Principle (LSP)

LSP states that child classes should be able to replace their parent classes without changing the system’s behavior. Unfortunately, when child classes override parent methods to change their behavior, they break this principle. Inheritance becomes a source of unexpected behavior 😕.

4. Interface Segregation Principle (ISP)

Inheritance often leads to child classes inheriting methods they don’t need. Take a look at this example:

Example of ISP Violation:

abstract class AbstractRepository extends ServiceEntityRepository
{
public function find($id): ?object;
public function save(object $entity): void;
public function remove(object $entity): void;
// Methods unused by some children
}

class ProductRepository extends AbstractRepository
{
// Uses only certain methods
}

A Better Solution with Segregated Interfaces:

interface ReadableRepositoryInterface
{
public function find(int $id): ?object;
}

interface WritableRepositoryInterface
{
public function save(object $entity): void;
}

final class ProductRepository implements ReadableRepositoryInterface, WritableRepositoryInterface
{
// Implements only what's needed
}

5. Dependency Inversion Principle (DIP)

Inheritance often creates tight coupling between classes, making the system rigid and hard to modify. When child classes depend heavily on the parent class, it becomes difficult to change one part of the system without affecting others ⚠️ and this forces us to rely on concrete implementations rather than abstractions.

Example of DIP Violation:

class OrderProcessor extends BaseProcessor
{
public function process()
{
parent::process();
$this->sendEmail();
}
}

Solution that Respects DIP:

interface ProcessorInterface
{
public function process(Order $order): void;
}

interface EmailSenderInterface
{
public function send(Email $email): void;
}

class OrderProcessor implements ProcessorInterface
{
public function __construct(
private readonly ProcessorInterface $baseProcessor,
private readonly EmailSenderInterface $emailSender
) {}

public function process(Order $order): void
{
$this->baseProcessor->process($order);
$this->emailSender->send(new Email(/* ... */));
}
}

By introducing interfaces, we decouple dependencies and make the system easier to modify, which respects the DIP and promotes better maintainability.

The Hidden Dangers of Inheritance

1. Tight Coupling

When child classes are tightly coupled to their parent classes, it becomes risky to change one part of the system because the impact on other parts can be unpredictable.

2. Testing Complexities

Testing a child class often requires testing its parent class as well. This adds unnecessary complexity to unit tests and can slow down development ⏳.

This makes it difficult to create isolated mocks for the child class, as the behaviors of the parent class must be taken into account.

Why Composition is a Better Alternative

Instead of relying on inheritance, composition encourages assembling objects with well-defined responsibilities. This leads to more flexible, reusable, and testable code.

Example of Composition in Image Management:

interface FileValidatorInterface { /*...*/ }
interface ImageProcessorInterface { /*...*/ }
interface FileStorageInterface { /*...*/ }

class ProductImageHandler
{
public function __construct(
private readonly FileValidatorInterface $validator,
private readonly ImageProcessorInterface $processor,
private readonly FileStorageInterface $storage
) {}

public function handle(UploadedFile $file): string
{
$this->validator->validate($file);
$processedImage = $this->processor->process($file);
return $this->storage->store($processedImage);
}
}

In this example, composition allows each class to take on a specific responsibility, making the code easier to understand and maintain.

Useful Design Patterns:

  • Strategy Pattern: Enables dynamic switching of behaviors.
  • Decorator Pattern: Adds new functionality without changing the original structure.
  • Dependency Injection: Helps decouple dependencies and makes the system more flexible 🔌 it helps promote dependency inversion principle, which will provide better interoperability in our system.

Conclusion

Inheritance can also be easily modeled after real-world situations. A crude example is your children — literal children — inheriting your genes, behaviors, and behavioral cues from the parents, while overriding or extending some of these traits as they grow older. For instance, a rectangle is a shape, and a rabbit is an animal. This approach works best when the parent class defines shared behaviors or properties, and the child classes extend functionality without violating the Liskov Substitution Principle (LSP). However, be careful not to overuse inheritance and instead prefer composition whenever possible.

Key Takeaways:

  • Final classes promote composition. So use them when it' possible.
  • Leverage interfaces for defining specific behaviors.
  • Encourage the use of design patterns such as Decorator, Composite, Strategy, and Bridge to conceptualize your application.

--

--

Ahmed EBEN HASSINE
Ahmed EBEN HASSINE

Written by Ahmed EBEN HASSINE

Certified Symfony developer 🚀 | Passionate about open source 🌟 | Contributor to Symfony & API Platform | Crafting robust web apps with clean architecture & SO