Sobes.tech
Middle

What are SOLID principles?

sobes.tech AI

Answer from AI

SOLID is a mnemonic acronym representing five core principles of object-oriented design aimed at creating flexible, maintainable, and scalable systems:

  • Single Responsibility Principle (SRP)
  • Open/Closed Principle (OCP)
  • Liskov Substitution Principle (LSP)
  • Interface Segregation Principle (ISP)
  • Dependency Inversion Principle (DIP)

Single Responsibility Principle (SRP): A class should have only one reason to change, meaning it should perform only one specific task or have only one responsibility.

<?php
// Bad: Customer class manages customer data and email sending
class BadCustomer {
    public function saveData(array $data) { /* ... */ }
    public function sendEmail(string $message) { /* ... */ }
}

// Good: responsibilities are separated
class CustomerData {
    public function saveData(array $data) { /* ... */ }
}

class EmailService {
    public function sendEmail(string $message) { /* ... */ }
}
?>

Open/Closed Principle (OCP): Software entities (classes, modules, functions) should be open for extension but closed for modification. This is achieved through the use of abstractions (interfaces, abstract classes).

<?php
// Bad: adding a new shape type requires modifying the AreaCalculator class
class BadAreaCalculator {
    public function calculateArea(array $shapes): float {
        $totalArea = 0;
        foreach ($shapes as $shape) {
            if ($shape instanceof Rectangle) {
                $totalArea += $shape->getWidth() * $shape->getHeight();
            } elseif ($shape instanceof Circle) {
                $totalArea += pi() * $shape->getRadius() ** 2;
            }
            // What if we add Triangle? We need to change this class
        }
        return $totalArea;
    }
}

// Good: using an interface for extension without modification
interface Shape {
    public function calculateArea(): float;
}

class Rectangle implements Shape {
    private float $width;
    private float $height;

    public function __construct(float $width, float $height) {
        $this->width = $width;
        $this->height = $height;
    }

    public function calculateArea(): float {
        return $this->width * $this->height;
    }
}

class Circle implements Shape {
    private float $radius;

    public function __construct(float $radius) {
        $this->radius = $radius;
    }

    public function calculateArea(): float {
        return pi() * $this->radius ** 2;
    }
}

class GoodAreaCalculator {
    public function calculateTotalArea(array $shapes): float {
        $totalArea = 0;
        foreach ($shapes as $shape) {
            // All shapes implementing Shape work uniformly
            $totalArea += $shape->calculateArea();
        }
        return $totalArea;
    }
}
?>

Liskov Substitution Principle (LSP): Objects in a program should be replaceable with instances of their subtypes without altering the correctness of the program. Derived classes should extend, not replace, the behavior of the base class.

<?php
// Bad: Square is not a full substitute for Rectangle, violates invariants
class Rectangle {
    protected float $width;
    protected float $height;

    public function setWidth(float $width): void { $this->width = $width; }
    public function setHeight(float $height): void { $this->height = $height; }
    public function calculateArea(): float { return $this->width * $this->height; }
}

class Square extends Rectangle {
    public function setWidth(float $width): void {
        $this->width = $width;
        $this->height = $width; // Violates expectations for Rectangle
    }

    public function setHeight(float $height): void {
        $this->width = $height;
        $this->height = $height; // Violates expectations for Rectangle
    }
}

function enforceRectangleArea(Rectangle $r): void {
    $r->setWidth(5);
    $r->setHeight(10);
    // Expect area 50, but for Square will be 100
    echo "Area: " . $r->calculateArea() . "\n";
}

// Good: use separate classes or reconsider hierarchy
?>

Interface Segregation Principle (ISP): Clients should not be forced to depend on interfaces they do not use. It's better to have many small, specific interfaces than one large, general interface.

<?php
// Bad: One interface for all worker types, including those who do not eat
interface BadWorker {
    public function work();
    public function eat();
}

class HumanWorker implements BadWorker {
    public function work() { /* ... */ }
    public function eat() { /* ... */ }
}

class RobotWorker implements BadWorker {
    public function work() { /* ... */ }
    public function eat() { // Robots do not eat, but must implement the method
        throw new Exception("Robots don't eat!");
    }
}

// Good: Segregated interfaces
interface Workable {
    public function work();
}

interface Eatable {
    public function eat();
}

class HumanWorkerISP implements Workable, Eatable {
    public function work() { /* ... */ }
    public function eat() { /* ... */ }
}

class RobotWorkerISP implements Workable {
    public function work() { /* ... */ }
    // Does not implement Eatable
}
?>

Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details; details should depend on abstractions.

<?php
// Bad: High-level class Report directly depends on low-level DatabaseConnection class
class DatabaseConnection {
    public function connect() { /* ... */ }
    public function getData() { /* ... */ return ['data']; }
}

class ReportGenerator {
    private DatabaseConnection $dbConnection;

    public function __construct() {
        $this->dbConnection = new DatabaseConnection(); // Tight dependency
    }

    public function generateReport(): array {
        $this->dbConnection->connect();
        return $this->dbConnection->getData();
    }
}

// Good: High-level class Report depends on an abstraction, which is also implemented by the low-level class DatabaseConnection
interface DataProvider {
    public function getData(): array;
}

class DatabaseConnectionDIP implements DataProvider {
    public function connect() { /* ... */ }
    public function getData(): array { /* ... */ return ['data from db']; }
}

class ApiConnectionDIP implements DataProvider {
    public function getData(): array { /* ... */ return ['data from api']; }
}

class ReportGeneratorDIP {
    private DataProvider $dataProvider;

    public function __construct(DataProvider $dataProvider) {
        $this->dataProvider = $dataProvider; // Dependency is inverted via injection
    }

    public function generateReport(): array {
        return $this->dataProvider->getData();
    }
}
?>

Applying SOLID principles helps create code that is more understandable, testable, and easily extendable.