Dependency Inversion Principle

Example
Last from SOLID principles, this rule is:
- High-level modules should not import anything from low-level modules. Both should depend on abstractions (e.g., interfaces).
- Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
What does it mean? We should reduce dependencies to specific implementations but rely on interfaces. If we make any change to the interface (it violates the open/close principle), this change necessitates changes in the implementations of this interface. But if we need to change a specific implementation, we probably don't need to change our interface.
To illustrate the problem, let's go over this PHP example.
class DatabaseLogger
{
public function logError(string $message)
{
// ..
}
}
Here we have a class that logs some information to the database. Now use this class.
class MailerService
{
private DatabaseLogger $logger;
public function __construct(DatabaseLogger $logger)
{
$this->logger = $logger;
}
public function sendEmail()
{
try {
// ..
} catch (SomeException $exception) {
$this->logger->logError($exception->getMessage());
}
}
}
Here is the PHP class that sends e-mails, in case of an error, error details are logged to the database using the logger we have just seen above.
It breaks the principle of dependency inversion. Our e-mail-sending service uses a specific logger implementation. What if we want to log information about errors to a file or Sentry? We will have to change MailerService. This is not a flexible solution, such a replacement becomes problematic.
So what should it look like?
According to this principle, MailerService should rely on abstraction rather than detailed implementation. Therefore, we are adding the LoggerInterface interface.
interface LoggerInterface
{
public function logError(string $message): void;
}
And we use it in our DatabaseLogger:
class DatabaseLogger implements LoggerInterface
{
public function logError(string $message): void
{
// ..
}
}
Now, we can take advantage of Symfony Dependency Injection.
class MailerService
{
private LoggerInterface $logger;
public function sendEmail()
{
try {
// ..
} catch (SomeException $exception) {
$this->logger->logError($exception->getMessage());
}
}
}
In this way, we can freely replace the logs in the database with logs wherever we want, as long as the detailed implementation implements the LoggerInterface. This change will not require modifying MailerService, because it does not depend on it, it depends only on the interface.
Conclusion
The principle of dependency inversion is critically important for the construction of code that is resilient to change.
