Przypadek użycia
Korzystamy z zewnętrznej biblioteki, gdzie parametrem metody, jednej z należącej do niej klas użytkowych jest zmienna, która może być wywołana jako funkcja. Zwykle w miejscu parametru tworzymy funkcję anonimową. Załóżmy, że sytuacja dzieje się w akcji kontrolera framework'a Symfony i wykorzystujemy zmyśloną zewnętrzną bibliotekę Datatables:
use Doctrine\ORM\QueryBuilder;
use ExternalLibrary\Datatables;
use App\Entity\User;
use App\Service\Service;
final class Controller
{
/** @var Service */ private $service;
public function __construct(Service $service)
{
$this->service = $service;
}
public function someAction(
Datatable $datatable,
User $user
): Response {
$datatable
->doSomething(
function(QueryBuilder $qb) use ($user): void {
// implementacja z wykorzystaniem
// zmiennej $user i pola $this->service
}
)
->getResponse();
}
}
Pojawia się tutaj kilka problemów które wynikają z natury funkcji anonimowych:
⚠️ co prawda posiadamy dostęp do obiektu $this będącego obecną instancją kontrolera, ale do zmiennej $user nie mamy dostępu w funkcji anonimowej i musimy ją już jawnie przekazać korzystając ze słowa kluczowego use. Prawdopodobnie będzie ona wykorzystana tylko wewnątrz funkcji anonimowej i do akcji kontrolera będzie wstrzyknięta tylko w celu pośrednim. Można by było wstrzyknąć pożądany obiekt klasy User do konstruktora kontrolera, ale wtedy byłby on w zasięgu każdej akcji które by tego nie wymagały co nie jest dobrym rozwiązaniem. Trzeba zadać sobie pytanie czy zależność do klasy User na pewno wymagana jest w naszym kontrolerze,
⚠️ przekazany do funkcji anonimowej parametr typu QueryBuilder sprawia, że kontroler jest zależny od klasy, która być może powinna znajdować się w świadomości klas należących do warstwy infrastruktury,
⚠️ zaimplementowana funkcja anonimowa nie może być wykorzystana w innym kontrolerze/klasie.
Zamiana funkcji anonimowej na publiczną metodę kontrolera
$datatable
->doSomething([$this, 'newMethodName'])
->getResponse;
Nowa klasa z jedną metodą publiczną
Klasa ta posiadałaby tylko jedną metodę o sygnaturze:
public function execute(QueryBuilder $queryBuilder): void;
Cechy tego rozwiązania:
✔ zależności specyficzne tylko dla tej klasy (klasy QueryBuilder, User wstrzyknięte do bezpośredni do konstruktora) nie wyciekały by do klasy kontrolera,
✔ dzięki zarejestrowaniu tej klasy w Kontenerze Zależności, możemy ją wielokrotnie wstrzykiwać do innych klas,
✔ rozwiązanie staje się łatwe w testowaniu,
✔ upraszcza kod akcji kontrolera,
✔ możliwość nadania nazwy klasie zdradzającej jej intencje,
❌ dodatkowy plik w projekcie,
❌ gdy metoda klasy z biblioteki zewnętrznej oczekuje parametru typu Callable lub Closure, nie będziemy mogli zastosować tego rozwiązania,
Kod nowej klasy:
use Doctrine\ORM\QueryBuilder;
use App\Entity\User;
use App\Service\Service;
final class ActiveUserProductsQueryBuilder
{
/** @var Service */ private $service;
/** @var User */ private $user;
public function __construct(
User $user,
Service $service
) {
$this->user = $user;
$this->service = $service;
}
public function execute(QueryBuilder $queryBuilder): void {
// implementacja
}
}
Kod kontrolera pozbawiony jest teraz zależności do klasy User, Service oraz QueryBuilder, a nowa zaimportowana klasa ActiveUserProductsQueryBuilder może znajdować się w warstwie infrastruktury:
use ExternalLibrary\Datatables;
use App\Infrastructure\ActiveUserProductsQueryBuilder;
final class Controller
{
public function someAction(
Datatable $datatable,
ActiveUserProductsQueryBuilder $queryBuilder
): Response {
$datatable
->doSomething([$queryBuilder, 'execute'])
->getResponse();
}
}
use ExternalLibrary\Datatables;
use App\Infrastructure\ActiveUserProductsQueryBuilder;
final class Controller
{
public function someAction(
Datatable $datatable,
ActiveUserProductsQueryBuilder $queryBuilder
): Response {
$datatable
->doSomething([$queryBuilder, 'execute'])
->getResponse();
}
}
Wykorzystanie metody magicznej __invoke()
W tym przypadku można było by jeszcze bardzie uprościć kod klasy jak i klienta stosując metodę magiczną __invoke() wprowadzoną do PHP w wersji 5.3. Instancje klasy z implementacją tej metody można przekazywać jako parametr metody która Type Hint'uje na Callable.
- uproszenie kodu klasy polega na tym, że gdy posiada ona tylko jedno zachowanie, samo wykorzystanie __invoke() o tym mówi i daje znak innym programistą w zespole, że dodawanie nowych metod w tym miejscu jest błędne.
- uproszenie kodu klienta sprowadza się do tego, że nie jesteśmy już zobowiązani wskazywać na konkretną metodę zmiennej $queryBuilder:
$datatable
->doSomething($queryBuilder)
->getResponse;
->getResponse;
Wyodrębnienie interfejsu
W wyniku refaktoryzacji została wyodrębniona klasa ActiveUserProductsQueryBuilder ze stałym zachowaniem - jedną metodą __invoke(). W związku z tym można by utworzyć wyodrębniony interfejs i zaimplementować go do wspomnianej klasy. Jego kod przedstawiam poniżej, ale w zasadzie nie dzieje się tutaj nic nowego:
use Doctrine\ORM\QueryBuilder;
interface UserProductQueryBuilderInterface
{
public function __invoke(QueryBuilder $queryBuilder): void;
}
Uaktualniony kod kontrolera różni się w zasadzie tylko jednym szczegółem od poprzedniej wersji: Type Hint na parametr (w akcji) $queryBuilder zmienia się na klasę interfejsu. Wiązanie klasy konkretnej ActiveUserProductsQueryBuilder z interfejsem UserProductQueryBuilderInterface odbywało by się w Kontenerze Zależności framework'a Symfony. Ten krok refaktoryzacyjny sprawia, że klasa kontrolera nie jest ściśle związana z konkretną implementacją i z łatwością możemy wprowadzić nowe funkcjonalności bez ingerowania w kod kontrolera. W przyszłości, do zestawu klas implementujących UserProductQueryBuilderInterface mogły by dołączyć kolejne zachowania, które reprezentują przykładowe klasy:
- NewestProductListQueryBuilder,
- DiscountProductListQueryBuilder,
Dzięki temu, że nowe klasy zawierają kontrakt z interfejsem i kontroler nie musi być modyfikowany, cała nasz praca będzie sprowadzała się utworzenia nowej klasy i zmodyfikowaniu klasy fabrycznej...
Klasa fabryczna tworząca konkretną implementacje
Poniżej schemat struktury klas/flow:
╔════════════════════════════════════════════════════╗
║ Kontener Zależności ║
╠════════════════════════════════════════════════════╣
║ ┌───── ProductListQueryBuilderFactory ──────┐ ║
║ │getInstance(): ProductListQueryBuilderInterface│ ║
║ └──────┬─────────────────────────┬──────────────┘ ║
╚═════════╪═════════════════════════╪════════════════╝
┌──────┴──────┐ ┌──────┴──────┐
│Wstrzykiwanie│ │Rejestrowanie│
└──────┬──────┘ └──────┬──────┘
╔═════════╧═════════╗ ╔═════════════╧══════════════════╗
║ ProductController ║ ║ ProductListQueryBuilderFactory ║
╠═══════════════════╣ ╚═════════════╤══════════════════╝
║action($productQb) ║┌──────────────┴──────────────────┐
╚═══════════════════╝│ProductListQueryBuilderInterface │
├─────────────────────────────────┤
│public __invoke(QueryBuilder $qb)│
│:void │
└──────────────┬──────────────────┘
┌─────────────┘
│╔═══════════════════════════════╗
├╢ActiveProductListQueryBuilder ║
│╚═══════════════════════════════╝
│╔═══════════════════════════════╗
├╢NewestProductListQueryBuilder ║
│╚═══════════════════════════════╝
│╔═══════════════════════════════╗
└╢DiscountProductListQueryBuilder║
╚═══════════════════════════════╝
Brak komentarzy:
Prześlij komentarz