W zeszłym tygodniu napisałem , jednak obiecałem opowiedzieć, jak można go częściowo wykorzystać z korzyścią. W tym celu spróbuję przeanalizować, jak zazwyczaj stosuje się ten wzorzec w projektach. Minimalny zestaw metod dla repozytorium to:
<?php
interface PostRepository
{
public function getById($id): Post;
public function save(Post $post);
public function delete($id);
}Jednak w rzeczywistych projektach, jeśli zdecydowano się na stosowanie repozytoriów, często dodaje się do nich metody do wyszukiwania rekordów:
<?php
interface PostRepository
{
public function getById($id): Post;
public function save(Post $post);
public function delete($id);
public function getLastPosts();
public function getTopPosts();
public function getUserPosts($userId);
}Te metody mogłyby być zaimplementowane za pomocą Eloquent scopes, ale przeciążanie klas encji obowiązkami związanymi z ich wyszukiwaniem — to niewłaściwy pomysł, a przeniesienie tej odpowiedzialności do klas repozytoriów wydaje się logiczne. Czy tak jest? Specjalnie wizualnie podzieliłem ten interfejs na dwie części. Pierwsza część metod będzie używana w operacjach zapisu.
Standardowe operacje zapisu to:
- konstruowanie nowego obiektu i wywołanie PostRepository::save
- PostRepository::getById, manipulacje encją i wywołanie PostRepository::save
- wywołanie PostRepository::delete
W operacjach zapisu nie korzysta się z metod wyszukiwania. W operacjach odczytu z kolei używa się tylko metod get*. Jeśli poczytać o Interface Segregation Principle (litera I do SOLID), to jasne jest, że nasz interfejs okazał się zbyt duży i spełnia co najmniej dwa różne zadania. Czas go podzielić na dwa. Metoda getById jest potrzebna w obu, jednak w miarę komplikowania aplikacji jej implementacje będą różne. To zobaczymy nieco później. O bezużyteczności części write pisałem w poprzednim artykule, dlatego w tym po prostu o niej zapomnę.
Część Read nie wydaje mi się tak całkowicie bezużyteczna, ponieważ nawet dla Eloquent mogą istnieć różne implementacje. Jak nazwano by klasę? Można ReadPostRepository, ale do wzorca Repozytorium ma on już niewiele wspólnego. Można po prostu PostQueries:
<?php
interface PostQueries
{
public function getById($id): Post;
public function getLastPosts();
public function getTopPosts();
public function getUserPosts($userId);
}Jego implementacja przy pomocy Eloquent jest dość prosta:
<?php
final class EloquentPostQueries implements PostQueries
{
public function getById($id): Post
{
return Post::findOrFail($id);
}
/**
* @return Post[] | Collection
* /
public function getLastPosts()
{
return Post::orderBy('created_at', 'desc')
->limit(
/*some limit*/
)
->get();
}
/**
* @return Post[] | Collection
* /
public function getTopPosts()
{
return Post::orderBy('rating', 'desc')
->limit(
/*some limit*/
)
->get();
}
/**
* @param int $userId
* @return Post[] | Collection
* /
public function getUserPosts($userId)
{
return Post::whereUserId($userId)
->orderBy('created_at', 'desc')
->get();
}
}Interfejs powinien być związany z implementacją, na przykład w AppServiceProvider:
<?php
final class AppServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->bind(PostQueries::class,
EloquentPostQueries::class);
}
}Ta klasa już jest użyteczna. Realizuje swoją odpowiedzialność, odciążając w ten sposób albo kontrolery, albo klasę encji. W kontrolerze może być używana w ten sposób:
<?php
final class PostsController extends Controller
{
public function lastPosts(PostQueries $postQueries)
{
return view('posts.last', [
'posts' => $postQueries->getLastPosts(),
]);
}
} Metoda PostsController::lastPosts po prostu prosi o jakąś implementację PostsQueries i pracuje z nią. W providerze połączyliśmy PostQueries z klasą EloquentPostQueries i w kontrolerze zostanie wstawiona ta klasa.
Załóżmy, że nasza aplikacja stała się bardzo popularna. Tysiące użytkowników na minutę otwierają stronę z najnowszymi publikacjami. Najpopularniejsze publikacje są również często czytane. Bazy danych nie radzą sobie dobrze z takimi obciążeniami, dlatego wykorzystuje się standardowe rozwiązanie — cache. Oprócz bazy danych, pewien odcisk danych jest przechowywany w magazynie zoptymalizowanym do określonych operacji — memcached lub redis.
Logika pamięci podręcznej zwykle nie jest zbyt skomplikowana, ale implementacja jej w EloquentPostQueries nie byłaby poprawna (choćby z powodu Zasady Pojedynczej Odpowiedzialności). Znacznie bardziej naturalne byłoby użycie wzorca Dekorator i zaimplementowanie cache jako dekorowania głównej akcji:
<?php
use IlluminateContractsCacheRepository;
final class CachedPostQueries implements PostQueries
{
const LASTS_DURATION = 10;
/** @var PostQueries */
private $base;
/** @var Repository */
private $cache;
public function __construct(
PostQueries $base, Repository $cache)
{
$this->base = $base;
$this->cache = $cache;
}
/**
* @return Post[] | Collection
* /
public function getLastPosts()
{
return $this->cache->remember('last_posts',
self::LASTS_DURATION,
function(){
return $this->base->getLastPosts();
});
}
// inne metody praktycznie takie same
}Nie zwracaj uwagi na interfejs Repozytorium w konstruktorze. Z nieznanych przyczyn tak nazwano interfejs do cache'owania w Laravel.
Klasa CachedPostQueries realizuje tylko cache'owanie. $this->cache->remember sprawdza, czy dana rekord jest w cache'u, a jeśli nie, wywołuje callback i zapisuje w cache'u zwróconą wartość. Pozostaje tylko wdrożyć tę klasę w aplikacji. Musimy zapewnić, aby wszystkie klasy, które w aplikacji proszą o realizację interfejsu PostQueries zaczęły otrzymywać instancję klasy CachedPostQueries. Jednak sam CachedPostQueries jako parametr w konstruktorze musi otrzymać klasę EloquentPostQueries, ponieważ nie może działać bez „prawdziwej” realizacji. Zmieniamy AppServiceProvider:
app->bind(PostQueries::class,
CachedPostQueries::class);
$this->app->when(CachedPostQueries::class)
->needs(PostQueries::class)
->give(EloquentPostQueries::class);
}
}Wszystkie moje życzenia opisane są dość naturalnie w providerze. W ten sposób zrealizowaliśmy cache'owanie naszych zapytań, pisząc tylko jedną klasę i zmieniając konfigurację kontenera. Kod reszty aplikacji nie zmienił się.
Oczywiście, dla pełnej realizacji cache'owania konieczne jest jeszcze wdrożenie unieważniania, aby usunięty artykuł nie pozostawał na stronie przez jakiś czas, lecz znikał od razu. Ale to już drobnostki.
Podsumowanie: użyliśmy nie jednego, a aż dwóch wzorców. Wzorzec Command Query Responsibility Segregation (CQRS) proponuje całkowite oddzielenie operacji odczytu i zapisu na poziomie interfejsów. Doszedłem do niego przez Interface Segregation Principle, co wskazuje, że umiejętnie manipuluję wzorcami i zasadami, wyprowadzając jeden z drugiego jak teoremat 🙂 Oczywiście, nie każdy projekt wymaga takiej abstrakcji dla pobierania encji, ale podzielę się z wami trikiem. Na wstępnym etapie rozwoju aplikacji można po prostu stworzyć klasę PostQueries z zwykłą realizacją przez Eloquent:
<?php
final class PostQueries
{
public function getById($id): Post
{
return Post::findOrFail($id);
}
// inne metody
}Gdy zajdzie potrzeba cache'owania, łatwo można stworzyć interfejs (lub klasę abstrakcyjną) na miejsce tej klasy PostQueries, jego realizację skopiować do klasy EloquentPostQueries i przejść do schematu, o którym pisałem wcześniej. Resztę kodu aplikacji nie trzeba zmieniać.
Wszystkie te sztuczki z klasami, interfejsami, Wstrzykiwanie zależności i CQRS są szczegółowo opisane w Tam znajduje się rozwiązanie zagadki, dlaczego wszystkie moje klasy w przykładach tego artykułu są oznaczone jako final.
Źródło: habr.com
