Na semana passada, escrevi , no entanto, prometi explicar como partes dele podem ser utilizadas com utilidade. Para isso, tentarei analisar como esse padrão costuma ser utilizado em projetos. O conjunto mínimo de métodos para um repositório é:
<?php
interface PostRepository
{
public function getById($id): Post;
public function save(Post $post);
public function delete($id);
}No entanto, em projetos reais, se realmente foi decidido usar repositórios, frequentemente são adicionados métodos para a seleção de registros:
<?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);
}Esses métodos poderiam ser implementados através de escopos Eloquent, mas sobrecarregar as classes de entidades com a responsabilidade de serem responsáveis pela seleção de si mesmas não é a melhor ideia, e transferir essa responsabilidade para as classes de repositórios parece lógico. Será que é assim? Eu dividi visualmente essa interface em duas partes. A primeira parte dos métodos será utilizada nas operações de gravação.
A operação padrão de gravação é:
- a construção de um novo objeto e a chamada PostRepository::save
- PostRepository::getById, manipulações com a entidade e a chamada PostRepository::save
- chamada PostRepository::delete
Nas operações de gravação não se utilizam métodos de seleção. Nas operações de leitura, apenas os métodos get* são usados. Se lermos sobre Interface Segregation Principle (a letra I em SOLID), ficará claro que nossa interface se tornou muito grande e desempenhando pelo menos duas responsabilidades diferentes. É hora de dividi-la em duas. O método getById é necessário em ambas, no entanto, com a complexidade crescente do aplicativo, suas implementações serão diferentes. Isso veremos um pouco mais tarde. Sobre a inutilidade da parte de gravação, escrevi no artigo anterior, portanto, neste, simplesmente vou esquecê-la.
A parte de leitura, ao contrário, não me parece tão inútil, pois mesmo para Eloquent pode haver várias implementações. Como nomear a classe? Pode-se ReadPostRepository, mas em relação ao padrão Repository já tem pouca relação. Pode-se simplesmente PostQueries:
<?php
interface PostQueries
{
public function getById($id): Post;
public function getLastPosts();
public function getTopPosts();
public function getUserPosts($userId);
}Sua implementação usando Eloquent é bastante simples:
<?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();
}
}A interface deve estar ligada à implementação, por exemplo em AppServiceProvider:
<?php
final class AppServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->bind(PostQueries::class,
EloquentPostQueries::class);
}
}Esta classe já é útil. Ela executa sua responsabilidade, aliviando assim ou os controladores ou a classe entidade. Em um controlador, pode ser utilizada assim:
<?php
final class PostsController extends Controller
{
public function lastPosts(PostQueries $postQueries)
{
return view('posts.last', [
'posts' => $postQueries->getLastPosts(),
]);
}
} Método PostsController::lastPosts simplesmente pede uma implementação qualquer para si PostsQueries e trabalha com ela. No provedor, vinculamos PostQueries à classe EloquentPostQueries e no controlador será inserida esta classe.
Vamos imaginar que nosso aplicativo se tornou muito popular. Milhares de usuários por minuto acessam a página com as últimas publicações. As publicações mais populares também são lidas com muita frequência. Os bancos de dados não lidam muito bem com tais cargas, então usam a solução padrão - cache. Além do banco de dados, uma cópia dos dados é armazenada em um repositório otimizado para determinadas operações - memcached ou redis.
A lógica de caching geralmente não é tão complexa, mas implementá-la em EloquentPostQueries não é muito correto (pelo menos por causa do Princípio da Responsabilidade Única). Seria muito mais natural usar o padrão Decorator e implementar o caching como uma decoração da ação principal:
<?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();
});
}
// outros métodos são praticamente os mesmos
}Não se preocupe com a interface Repository no construtor. Por uma razão desconhecida, decidiram nomear a interface de cache no Laravel.
Classe CachedPostQueries implementa apenas o cache. $this->cache->remember verifica se a entrada está no cache e, se não estiver, chama o callback e grava no cache o valor retornado. Resta apenas integrar esta classe na aplicação. Precisamos que todas as classes que solicitam a implementação da interface PostQueries passam a receber uma instância da classe CachedPostQueries. Contudo, o próprio CachedPostQueries como parâmetro no construtor deve receber a classe EloquentPostQueries, pois não pode funcionar sem uma implementação 'real'. Mudamos AppServiceProvider:
app->bind(PostQueries::class,
CachedPostQueries::class);
$this->app->when(CachedPostQueries::class)
->needs(PostQueries::class)
->give(EloquentPostQueries::class);
}
}Todos os meus desejos são naturalmente descritos no provedor. Assim, implementamos o cache para nossas consultas escrevendo apenas uma classe e alterando a configuração do contêiner. O código do restante da aplicação não mudou.
Claro, para uma implementação completa do cache, também é necessário implementar a invalidação, para que um artigo removido não permaneça no site por mais tempo e seja excluído imediatamente. Mas isso já são detalhes.
Resultado: usamos não apenas um, mas dois padrões. O padrão Command Query Responsibility Segregation (CQRS) propõe separar completamente as operações de leitura e gravação no nível das interfaces. Cheguei a isso através Interface Segregation Principle, o que demonstra que manipulo padrões e princípios habilmente e deduzo um a partir do outro como um teorema 🙂 Claro, nem todo projeto necessita de tal abstração para consultas de entidades, mas compartilharei com você uma dica. Na fase inicial do desenvolvimento da aplicação, pode-se simplesmente criar uma classe PostQueries com uma implementação comum via Eloquent:
<?php
final class PostQueries
{
public function getById($id): Post
{
return Post::findOrFail($id);
}
// outros métodos
}Quando surgir a necessidade de cache, pode-se facilmente criar uma interface (ou classe abstrata) no lugar desta classe PostQueries, copiar sua implementação para a classe EloquentPostQueries e seguir para o esquema que descrevi anteriormente. Não é necessário modificar o restante do código da aplicação.
Todos esses truques com classes, interfaces, Dependency Injection e CQRS estão detalhadamente descritos em . Lá está a solução para o mistério de por que todas as minhas classes nos exemplos deste artigo estão marcadas como final.
Fonte: habr.com
