Understanding SOLID Principles in Object-Oriented Programming

SOLID Image

Object-oriented programming (OOP) is one of the most widely used paradigms in modern software development. However, as systems grow larger, it is common for code to become difficult to maintain, test, and evolve. To avoid these issues, software development has adopted various best practices and design patterns, including the SOLID principles.

These principles were popularized by Robert C. Martin (Uncle Bob) and are intended to make code more flexible, reusable, and easier to understand. Let’s take a look at each one:

1. S – Single Responsibility Principle

“A class should have only one reason to change.”

This principle states that each class should have only one clear responsibility.

If a class handles many different tasks, a change to one functionality can impact others, making the code fragile.

Example: A class that manages users and also sends emails:

class User {
    public function saveToDatabase($data) {
        // code to save user
    }

    public function sendEmail($email, $message) {
        // code to send an email
    }
}

The correct approach would be to have one class for user management and another for sending emails:

class UserRepository {
    public function save($data) {
        // code to save a user to the database
    }
}

class EmailService {
    public function send($email, $message) {
        // code to send an email
    }
}

2. O – Open/Closed Principle

“Open for extension, closed for modification.”

In other words, you should be able to add new functionality without having to modify existing code. This reduces the risk of breaking functionality that already works.

3. L – Liskov Substitution Principle (Princípio da Substituição de Liskov)

“Objetos de uma classe derivada devem poder substituir objetos da classe base sem alterar o comportamento esperado.”

Example:

  • Use inheritance or interfaces to extend functionality without modifying the original class.

In the following case, if another payment method is introduced, we would need to modify the class:

class Payment {
    public function pay($type) {
        if ($type == 'paypal') {
            // PayPal logic
        } elseif ($type == 'pix') {
            // Pix logic
        }
    }
}

The correct approach would be the following, allowing you to create new payment classes without modifying the existing code:

interface PaymentMethod {
    public function pay();
}

class PayPalPayment implements PaymentMethod {
    public function pay() {
        echo "Payment via PayPal";
    }
}

class PixPayment implements PaymentMethod {
    public function pay() {
        echo "Payment via Pix";
    }
}

class PaymentProcessor {
    public function process(PaymentMethod $payment) {
        $payment->pay();
    }
}

If you use a subclass in place of its parent class, the system should continue to work correctly. Otherwise, it means that inheritance has been improperly applied.

Example:
  • If Bird has a fly() method, but you create a Penguin subclass that cannot fly, this violates the principle. In this case, it would be better to rethink the hierarchy.
Here, we violate the principle because Penguin does not behave like a Bird:
class Bird {
    public function fly() {
        echo "Flying...";
    }
}

class Penguin extends Bird {
    public function fly() {
        throw new Exception("Penguins don't fly!");
    }
}
The correct approach would be to avoid forcing Penguin to implement fly():
interface Bird {
    public function eat();
}

interface FlyingBird extends Bird {
    public function fly();
}

class Sparrow implements FlyingBird {
    public function eat() { echo "Eating seeds"; }
    public function fly() { echo "Flying..."; }
}

class Penguin implements Bird {
    public function eat() { echo "Eating fish"; }
}

4. I – Interface Segregation Principle

“A class should not be forced to implement interfaces it does not use.”

It is better to create smaller, more specific interfaces rather than a single “bloated” interface with many methods. This avoids unnecessary implementations.

Example:

  • Instead of having an IBird interface with fly() and swim() methods, create separate FlyingBird and SwimmingBird interfaces.

In this case, the Dog will never fly, but it is forced to implement fly():

interface Animal {
    public function fly();
    public function swim();
}

class Dog implements Animal {
    public function fly() { /* ??? */ }
    public function swim() { echo "Swimming"; }
}

Each class should implement only what it needs. In this case, the Dog will implement only canSwim:

interface CanFly {
    public function fly();
}

interface CanSwim {
    public function swim();
}

class Dog implements CanSwim {
    public function swim() { echo "Swimming"; }
}

5. D – Dependency Inversion Principle

“Depend on abstractions, not implementations.”

High-level classes should not depend directly on low-level classes, but rather on abstractions (interfaces). This makes the system more flexible and makes it easier to replace implementations.

Example:

  • Instead of having a Order class depend directly on MySQLRepository, it should depend on an IRepository interface, which can have different implementations (MySQL, MongoDB, etc.).

In this situation, if we switch the database to MongoDB, we would have to rewrite the code:

class MySQLRepository {
    public function getUsers() {
        return ['John', 'Mary'];
    }
}

class UserService {
    private $repository;

    public function __construct() {
        $this->repository = new MySQLRepository(); // direct dependency
    }

    public function listUsers() {
        return $this->repository->getUsers();
    }
}

To fix this, we can replace the repository implementation without modifying the UserService:

interface UserRepository {
    public function getUsers();
}

class MySQLRepository implements UserRepository {
    public function getUsers() {
        return ['John', 'Mary'];
} } class MongoRepository implements UserRepository { public function getUsers() { return ['Carlos', 'Ana']; } } class UserService { private $repository; public function __construct(UserRepository $repository) { $this->repository = $repository; } public function listUsers() { return $this->repository->getUsers(); } } // Example of usage: $service = new UserService(new MySQLRepository()); print_r($service->listUsers());

Why Use SOLID?

Following the SOLID principles helps create cleaner, more organized, and easier-to-maintain applications. Whether you’re working on large or small projects, applying these best practices is an investment that can prevent headaches in the future.

Scroll to Top