La settimana scorsa ho scritto , ma ho promesso di spiegare come utilizzarlo parzialmente in modo utile. Per questo cercherò di analizzare come viene generalmente utilizzato questo modello nei progetti. Il set minimo necessario di metodi per il repository è:
<?php
interface PostRepository
{
public function getById($id): Post;
public function save(Post $post);
public function delete($id);
}Tuttavia, nei progetti reali, se si è deciso di utilizzare i repository, spesso vengono aggiunti metodi per le selezioni di record:
<?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);
}Questi metodi potrebbero essere implementati attraverso gli scope Eloquent, ma sovraccaricare le classi delle entità con le responsabilità di selezione non è la migliore idea e trasferire tale responsabilità nelle classi dei repository sembra logico. È corretto? Ho volutamente separato visivamente questa interfaccia in due parti. La prima parte dei metodi sarà utilizzata nelle operazioni di scrittura.
Le operazioni standard di scrittura sono:
- la costruzione di un nuovo oggetto e la chiamata PostRepository::save
- PostRepository::getById, manipolazioni con l'entità e la chiamata PostRepository::save
- la chiamata PostRepository::delete
Nelle operazioni di scrittura non si utilizzano metodi di selezione. Nelle operazioni di lettura, invece, si utilizzano solo i metodi get*. Se si legge riguardo al Principio di Segregazione delle Interfacce (la lettera I in SOLID), diventa chiaro che la nostra interfaccia è risultata troppo grande e svolge almeno due compiti diversi. È ora di dividerla in due. Il metodo getById è necessario in entrambe, ma con l'evolversi dell'applicazione le implementazioni saranno diverse. Questo lo vedremo tra poco. Riguardo all'inutilità della parte di scrittura ne ho parlato nell'articolo precedente, quindi in questo lo dimenticherò.
La parte di lettura, invece, non mi sembra affatto inutile, perché anche per Eloquent potrebbero esserci diverse implementazioni. Come chiamare la classe? Potremmo usare ReadPostRepository, ma ha davvero poco a che fare con il modello. Repository Si potrebbe semplicemente usare PostQueries:
<?php
interface PostQueries
{
public function getById($id): Post;
public function getLastPosts();
public function getTopPosts();
public function getUserPosts($userId);
}La sua implementazione con Eloquent è piuttosto semplice:
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();
}
}L'interfaccia deve essere collegata all'implementazione, ad esempio in AppServiceProvider:
app->bind(PostQueries::class,
EloquentPostQueries::class);
}
}Questa classe è già utile. Svolge la sua responsabilità, alleggerendo così o i controller o la classe dell'entità. In un controller può essere utilizzata in questo modo:
$postQueries->getLastPosts(),
]);
}
} Metodo PostsController::lastPosts richiede semplicemente un'implementazione di PostsQueries e lavora con essa. Nel provider abbiamo legato PostQueries alla classe EloquentPostQueries e nel controller verrà iniettata questa classe.
Immaginiamo che la nostra applicazione sia diventata molto popolare. Migliaia di utenti al minuto aprono la pagina con le ultime pubblicazioni. Anche le pubblicazioni più popolari vengono lette molto spesso. I database non gestiscono bene tali carichi, quindi si utilizza una soluzione standard: la cache. Oltre al database, un certo snapshot dei dati viene memorizzato in uno storage ottimizzato per determinate operazioni — memcached o redis.
La logica della cache di solito non è così complicata, ma implementarla in EloquentPostQueries non è proprio corretto (almeno per via del Principio di Responsabilità Unica). È molto più naturale utilizzare il modello Decoratore e implementare la cache come decorazione dell'azione principale:
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();
});
}
// altri metodi praticamente simili
}Non prestare attenzione all'interfaccia Repository nel costruttore. Per motivi a noi sconosciuti, hanno deciso di chiamare così l'interfaccia per la cache in Laravel.
Classe CachedPostQueries implementa solo la cache. $this->cache->remember controlla se questa registrazione è nella cache e, in caso contrario, chiama il callback e salva nella cache il valore restituito. Ora è solo necessario integrare questa classe nell'applicazione. Abbiamo bisogno che tutte le classi che nell'app richiedono l'implementazione dell'interfaccia PostQueries ricevano un'istanza della classe CachedPostQueries. Tuttavia, il CachedPostQueries come parametro nel costruttore deve ricevere la classe EloquentPostQueries, poiché non può funzionare senza una 'vera' implementazione. Cambiamo AppServiceProvider:
app->bind(PostQueries::class,
CachedPostQueries::class);
$this->app->when(CachedPostQueries::class)
->needs(PostQueries::class)
->give(EloquentPostQueries::class);
}
}Tutti i miei desideri sono descritti in modo abbastanza naturale nel provider. Così, abbiamo implementato la cache per le nostre query scrivendo solo una classe e cambiando la configurazione del contenitore. Il codice del resto dell'applicazione non è cambiato.
Naturalmente, per una piena implementazione della cache è necessario implementare anche l'invalidazione, in modo che l'articolo rimosso non rimanga sul sito per un altro po' di tempo ma venga eliminato immediatamente. Ma queste sono già piccolezze.
Il risultato: abbiamo usato non uno, ma ben due modelli. Il modello Command Query Responsibility Segregation (CQRS) propone di separare completamente le operazioni di lettura e scrittura a livello di interfacce. Sono arrivato a questo attraverso Principio di Segregazione delle Interfacce, il che dimostra che maneggio abilmente i modelli e i principi, deducendo uno dall'altro come un teorema 🙂 Naturalmente, non a tutti i progetti è necessaria tale astrazione per la selezione delle entità, ma condividerò con voi un trucco. All'inizio dello sviluppo dell'applicazione, si può semplicemente creare una classe PostQueries con una normale implementazione tramite Eloquent:
<?php
final class PostQueries
{
public function getById($id): Post
{
return Post::findOrFail($id);
}
// altri metodi
}Quando si presenterà la necessità di caching, sarà semplice creare un'interfaccia (o classe astratta) al posto di questa classe PostQueries, copiare la sua implementazione nella classe EloquentPostQueries e passare allo schema descritto precedentemente. Non è necessario modificare il resto del codice dell'applicazione.
Tutti questi trucchi con classi, interfacce, Dependency Injection e CQRS sono descritti in dettaglio in . Qui si trova anche la risoluzione del mistero del perché tutte le mie classi negli esempi di questo articolo siano contrassegnate come final.
Fonte: habr.com
