SOLID Examples

Single Responsibility Principle (SRP)

Suppose you are building a web application that allows users to sign up and log in to their accounts. To handle user authentication, you might create a class called Authenticator that has the following responsibilities:

Validate the user’s login credentials.
Generate a session token if the credentials are valid.
Store the session token in a cookie.
Redirect the user to the appropriate page.
However, this class violates SRP because it has more than one responsibility.

A better approach would be to split these responsibilities into separate classes:

A CredentialsValidator class that validates the user’s login credentials.
A SessionGenerator class that generates a session token.
A CookieManager class that stores the session token in a cookie.
A Redirector class that redirects the user to the appropriate page.


Each class has a single responsibility and can be tested and maintained separately.

You could then create an Authenticator class that depends on these classes and orchestrates the authentication process:

class Authenticator
{
    private $credentialsValidator;
    private $sessionGenerator;
    private $cookieManager;
    private $redirector;

    public function __construct(
        CredentialsValidator $credentialsValidator,
        SessionGenerator $sessionGenerator,
        CookieManager $cookieManager,
        Redirector $redirector
    ) {
        $this->credentialsValidator = $credentialsValidator;
        $this->sessionGenerator = $sessionGenerator;
        $this->cookieManager = $cookieManager;
        $this->redirector = $redirector;
    }

    public function authenticate($username, $password)
    {
        if ($this->credentialsValidator->isValid($username, $password)) {
            $sessionToken = $this->sessionGenerator->generate();
            $this->cookieManager->set('sessionToken', $sessionToken);
            $this->redirector->redirect('/dashboard');
        } else {
            $this->redirector->redirect('/login?error=invalid_credentials');
        }
    }
}

Unit Tests for SRP Example

This approach separates concerns and makes the code easier to test and maintain.

Open/Closed Principle (OCP)

Suppose you are building an e-commerce platform that allows customers to purchase products from multiple vendors. You might have a class called ShoppingCart that calculates the total cost of the items in the cart:

class ShoppingCart
{
    private $items = [];

    public function addItem($item)
    {
        $this->items[] = $item;
    }

    public function getTotal()
    {
        $total = 0;
        foreach ($this->items as $item) {
            $total += $item->getPrice();
        }
        return $total;
    }
}

However, you now want to offer a discount to customers who purchase a certain number of items from a specific vendor. To implement this feature, you might be tempted to modify the ShoppingCart class to include a vendor-specific discount calculation:

class ShoppingCart
{
    private $items = [];

    public function addItem($item)
    {
        $this->items[] = $item;
    }

    public function getTotal($vendorId)
    {
        $total = 0;
        $vendorItems = [];
        foreach ($this->items as $item) {
            if ($item->getVendorId() == $vendorId) {
                $vendorItems[] = $item;
            } else {
                $total += $item->getPrice();
            }
        }
        if (count($vendorItems) >= 5) {
            $vendorTotal = 0;
            foreach ($vendorItems as $item) {
                $vendorTotal += $item->getPrice();
            }
            $total += $vendorTotal * 0.9; // 10% discount
        } else {
            $total += $vendorTotal;
        }
        return $total;
    }
}

However, this violates the Open/Closed Principle (OCP) because the ShoppingCart class is now open for modification. Instead, you should create a new class that extends ShoppingCart and adds the vendor-specific discount calculation:

class VendorDiscountShoppingCart extends ShoppingCart
{
    public function getTotal($vendorId)
    {
        $total = parent::getTotal();
        $vendorItems = [];
        foreach ($this->items as $item) {
            if ($item->getVendorId() == $vendorId) {
                $vendorItems[] = $item;
            }
        }
        if (count($vendorItems) >= 5) {
            $vendorTotal = 0;
            foreach ($vendorItems as $item) {
                $vendorTotal += $item->getPrice();
            }
            $total -= $vendorTotal * 0.1; // 10% discount
        }
        return $total;
    }
}

Unit Tests for OCP Example

This approach extends the ShoppingCart class and adds the vendor-specific discount calculation without modifying the existing code. The VendorDiscountShoppingCart class is closed for modification but open for extension, allowing you to add new features without breaking existing code.

Liskov Substitution Principle (LSP)

Suppose you are building a library management system with a LibraryItem class hierarchy. The LibraryItem class has two derived classes: Book and DVD. Each of these classes has a method called getDuration(), which returns the duration of the item in minutes:

abstract class LibraryItem
{
    // ...
    abstract public function getDuration(): int;
    // ...
}

class Book extends LibraryItem
{
    // ...
    public function getDuration(): int
    {
        return 0; // Books don't have a duration
    }
    // ...
}

class DVD extends LibraryItem
{
    // ...
    public function getDuration(): int
    {
        return $this->duration;
    }
    // ...
}

Suppose you want to add a new type of library item called AudioBook. This class should also have a getDuration() method that returns the duration of the item in minutes. However, because audiobooks don’t have a fixed duration as DVDs do, the getDuration() method should always return zero:

class AudioBook extends LibraryItem
{
    // ...
    public function getDuration(): int
    {
        return 0; // Audio books don't have a fixed duration
    }
    // ...
}

Unit Tests for LSP Example

This class adheres to the Liskov Substitution Principle (LSP) because it can be substituted for its base class LibraryItem without affecting the correctness of the program. Clients of the LibraryItem class hierarchy can safely call the getDuration() method on an AudioBook object without worrying about the implementation details.

By adhering to the LSP, you can ensure that your code is flexible and extensible and that new derived classes can be added without breaking existing code.

Interface Segregation Principle (ISP)

Suppose you are building a CMS (Content Management System) that has a Content class hierarchy. The Content class has a method called render() that renders the content as HTML. You also have a class called Editor that allows users to edit content.

Initially, you might define an Editor interface that has a single method called edit(), which takes a Content object as a parameter:

interface Editor
{
    public function edit(Content $content);
}

However, this violates the Interface Segregation Principle (ISP) because the Editor interface is too broad. Not all types of Content objects can be edited in the same way. For example, a TextContent object might allow the user to edit the text content directly, while an ImageContent object might require the user to upload a new image file.

A better approach would be to define more specific interfaces for each type of Content object:

interface TextEditor
{
    public function editText(TextContent $content);
}

interface ImageEditor
{
    public function uploadImage(ImageContent $content, $file);
}

Now you can create concrete classes that implement these interfaces and define how to edit each type of Content object:

class SimpleTextEditor implements TextEditor
{
    public function editText(TextContent $content)
    {
        // Allow the user to edit the text content directly
    }
}

class ImageUploader implements ImageEditor
{
    public function uploadImage(ImageContent $content, $file)
    {
        // Allow the user to upload a new image file
    }
}

Unit Tests for ISP Example

This approach adheres to the ISP by defining small, focused interfaces that clients can use to interact with your classes, rather than large, monolithic interfaces that contain a lot of unnecessary methods. Clients of the Editor class hierarchy can now depend on the specific interfaces that they need, rather than depending on a broad interface that exposes unnecessary methods.

Dependency Inversion Principle (DIP)

Suppose you are building a web application that uses a Mailer class to send email notifications to users. The Mailer class depends on a concrete implementation of a MailerTransport interface, which defines how the mailer sends emails:

interface MailerTransport
{
    public function send(string $to, string $subject, string $body);
}

class SmtpTransport implements MailerTransport
{
    public function send(string $to, string $subject, string $body)
    {
        // Use SMTP to send the email
    }
}

class MailgunTransport implements MailerTransport
{
    public function send(string $to, string $subject, string $body)
    {
        // Use the Mailgun API to send the email
    }
}

class Mailer
{
    private $transport;

    public function __construct(MailerTransport $transport)
    {
        $this->transport = $transport;
    }

    public function sendEmail(string $to, string $subject, string $body)
    {
        $this->transport->send($to, $subject, $body);
    }
}

Initially, the Mailer class depended on concrete implementations of the MailerTransport interface, such as SmtpTransport and MailgunTransport. However, this violates the Dependency Inversion Principle (DIP) because it creates a tight coupling between the Mailer class and its dependencies.

To adhere to the DIP, you should invert the dependencies so that the Mailer class depends on abstractions, rather than concretions. One way to achieve this is by using a dependency injection container, which manages the dependencies for you:

interface MailerTransport
{
    public function send(string $to, string $subject, string $body);
}

class SmtpTransport implements MailerTransport
{
    public function send(string $to, string $subject, string $body)
    {
        // Use SMTP to send the email
    }
}

class MailgunTransport implements MailerTransport
{
    public function send(string $to, string $subject, string $body)
    {
        // Use the Mailgun API to send the email
    }
}

class Mailer
{
    private $transport;

    public function __construct(MailerTransport $transport)
    {
        $this->transport = $transport;
    }

    public function sendEmail(string $to, string $subject, string $body)
    {
        $this->transport->send($to, $subject, $body);
    }
}

$container = new Container();
$container->bind(MailerTransport::class, SmtpTransport::class); // or MailgunTransport::class
$mailer = $container->make(Mailer::class);
$mailer->sendEmail('user@example.com', 'Welcome!', 'Thanks for signing up.');

Unit Tests for DIP Example

Now the Mailer class depends on the MailerTransport interface, which is an abstraction, rather than a concrete implementation. The Container class manages the dependencies for you, allowing you to swap out implementations without modifying the Mailer class.

This approach adheres to the DIP by decoupling the Mailer class from its dependencies and making it more flexible and maintainable.

Leave a Reply