Dajmy dwa scenariusze, niepodąrzający za DIP i taki z nim zgodny. Na początek klasa którą będziemy chcieli przetestować:
<?php
class Authenticator
{
private $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function authenticate(string $username, string $password): bool
{
$user = $this->userRepository->findByUsername($username);
// Na potrzeby przykładu hasło nie jest hashowane.
if ($user !== null && $user->password == $password) {
// Ogarnij jakąś sesje, jakieś dane etc.
return true;
}
return false;
}
}
I teraz pierwsza implementacja, niezgodna z DIP:
<?php
class UserRepository
{
private $pdo;
public __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
public function findByUsername(string $username): ?User
{
// Zrób prepared statement z pdo, zbinduj nazwę użytkownika
// i zwróć wynik lub nulla jeżeli nie istnieje.
}
}
Jeżeli będziesz chciał to przetestować, to będziesz musiał zapewnić dla Authenticator
a klasę UserRepository
, która to będzie potrzebowała PDO
, a to wymaga od Ciebie bazy danych... i tak dalej. Złe!
Natomiast podejście oddzielające warstwy abstrakcji wyglądałoby tak:
<?php
interface UsersRepository
{
public function findByUsername(string $username): ?User;
}
class PDOUsersRepository implements UsersRepository
{
// I tutaj możesz sobie korzystać z PDO
}
class InMemoryUsersRepository implements UsersRepository
{
// Ooo, specjalnie na potrzeby testów repozytorium które działa tylko w pamięci? :)
}
I w tym miejscu Twój Authenticator
nie jest już zależny od tego w jaki sposób pobierane są dane użytkownika. Jest zależny tylko od repozytorium, którego implementacja jest całkowicie dowolna i nie interesuje Twojego Authenticator
a. Takie repozytorium w testach możesz mockować albo dostarczać jakieś specjalnie przeznaczone dla testów, jak np. InMemoryUsersRepository
z przykładu wyżej.