Loading

Back to blog

Interfaces and dependency inversion

  • Clean Architecture
  • PHP
  • SOLID
  • Design Pattern

When refactoring an existing application, it's not uncommon to find an external service used directly in the code. The problem is that if you want to switch services, or if the service provider changes its SDK or API, you have to rewrite a significant part of the code.

To avoid ending up in this situation in the future, you can adopt a more modular and scalable approach.

Here's one possible solution for a mail-sending service:

  1. Create a shared interface: Create an interface called MailerInterface, for example, that defines a send method (at minimum). This interface acts as a contract that all messaging services must respect.

  2. Implement specific services: Then all you have to do is implement this interface in the classes that use a mail-sending service: MailjetMailer and SendgridMailer, for example. Each class implements MailerInterface and uses a distinct email service provider. Both have a send method, which makes them easy to use and interchange in the code.

// Interface definition
interface MailerInterface {
    public function send($recipient, $subject, $body);
}

// Mailjet implementation
class MailjetMailer implements MailerInterface {
    public function send($recipient, $subject, $body) {
        // Code to send an email via Mailjet
    }
}

// Sendgrid implementation
class SendgridMailer implements MailerInterface {
    public function send($recipient, $subject, $body) {
        // Code to send an email via Sendgrid
    }
}

// A very simplistic example implementation
class ResetPasswordCommandHandler {
    private $mailer;
    // We can use whichever mailer we choose
    public function __construct(MailerInterface $mailer) {
        $this->mailer = $mailer;
    }

    public function __invoke(ResetPasswordCommand $resetPasswordCommand ): void {
        // Generate the password reset token (for example)
        $resetToken = //random;

        // Send the email — whatever the mailer, we call the send method
        $this->mailer->send(
            $resetPasswordCommand->email,
            'Reset your password',
            "Use this token to reset your password: $resetToken"
        );
    }
}

With this architecture, all you need to do is change the MailerInterface implementation in use to switch between external services without modifying the rest of the code.

By applying this method, we not only simplified service management in our application, we also adopted a better software development practice.

The benefits of this approach are clear:

  • Flexibility: We can easily change provider or service without touching the core logic of our application.
  • Easier maintenance: Changes are localised and don't require massive refactoring.
  • Extensibility: Adding a new service or provider only requires creating a new class that implements the defined interface.

This method allowed us to gain agility and responsiveness in the face of technological change and our application's evolving needs. It's a best practice I strongly recommend to all developers.