Pensé que me tomaría un buen rato responder mi propia pregunta. Lo que sigue es solo una forma de resolver los problemas 1-3 en mi pregunta original.
Descargo de responsabilidad: no siempre puedo usar los términos correctos al describir patrones o técnicas. Lo siento por eso.
Los objetivos:
- Cree un ejemplo completo de un controlador básico para ver y editar
Users
.
- Todo el código debe ser totalmente comprobable y simulable.
- El controlador no debe tener idea de dónde se almacenan los datos (lo que significa que se pueden cambiar).
- Ejemplo para mostrar una implementación de SQL (más común).
- Para obtener el máximo rendimiento, los controladores solo deben recibir los datos que necesitan, sin campos adicionales.
- La implementación debería aprovechar algún tipo de mapeador de datos para facilitar el desarrollo.
- La implementación debe tener la capacidad de realizar búsquedas complejas de datos.
La solución
Estoy dividiendo mi interacción de almacenamiento persistente (base de datos) en dos categorías: R (Leer) y CUD (Crear, Actualizar, Eliminar). Mi experiencia ha sido que las lecturas son realmente lo que hace que una aplicación se ralentice. Y aunque la manipulación de datos (CUD) es en realidad más lenta, ocurre con mucha menos frecuencia y, por lo tanto, es mucho menos preocupante.
CUD (Crear, Actualizar, Eliminar) es fácil. Esto implicará trabajar con modelos reales , que luego se pasan a mi Repositories
para persistencia. Tenga en cuenta que mis repositorios seguirán proporcionando un método de lectura, pero simplemente para la creación de objetos, no para mostrar. Más sobre eso más tarde.
R (Leer) no es tan fácil. No hay modelos aquí, solo objetos de valor . Use matrices si lo prefiere . Estos objetos pueden representar un modelo único o una combinación de muchos modelos, cualquier cosa realmente. No son muy interesantes por sí mismos, pero sí cómo se generan. Estoy usando lo que estoy llamando Query Objects
.
El código:
Modelo de usuario
Comencemos de manera simple con nuestro modelo de usuario básico. Tenga en cuenta que no hay extensión de ORM o material de base de datos en absoluto. Solo pura gloria modelo. Agregue sus captadores, establecedores, validación, lo que sea.
class User
{
public $id;
public $first_name;
public $last_name;
public $gender;
public $email;
public $password;
}
Interfaz de repositorio
Antes de crear mi repositorio de usuario, quiero crear mi interfaz de repositorio. Esto definirá el "contrato" que los repositorios deben seguir para que mi controlador pueda utilizarlos. Recuerde, mi controlador no sabrá dónde se almacenan realmente los datos.
Tenga en cuenta que mis repositorios solo contendrán estos tres métodos. El save()
método es responsable de crear y actualizar usuarios, simplemente dependiendo de si el objeto de usuario tiene o no un conjunto de identificación.
interface UserRepositoryInterface
{
public function find($id);
public function save(User $user);
public function remove(User $user);
}
Implementación del repositorio de SQL
Ahora para crear mi implementación de la interfaz. Como se mencionó, mi ejemplo iba a ser con una base de datos SQL. Tenga en cuenta el uso de un mapeador de datos para evitar tener que escribir consultas SQL repetitivas.
class SQLUserRepository implements UserRepositoryInterface
{
protected $db;
public function __construct(Database $db)
{
$this->db = $db;
}
public function find($id)
{
// Find a record with the id = $id
// from the 'users' table
// and return it as a User object
return $this->db->find($id, 'users', 'User');
}
public function save(User $user)
{
// Insert or update the $user
// in the 'users' table
$this->db->save($user, 'users');
}
public function remove(User $user)
{
// Remove the $user
// from the 'users' table
$this->db->remove($user, 'users');
}
}
Consultar interfaz de objeto
Ahora con CUD (Crear, Actualizar, Eliminar) atendido por nuestro repositorio, podemos centrarnos en la R (Leer). Los objetos de consulta son simplemente una encapsulación de algún tipo de lógica de búsqueda de datos. Son no generadores de consultas. Al abstraerlo como nuestro repositorio podemos cambiar su implementación y probarlo más fácilmente. Un ejemplo de un objeto de consulta podría ser un AllUsersQuery
o AllActiveUsersQuery
, o incluso MostCommonUserFirstNames
.
Puede estar pensando "¿no puedo simplemente crear métodos en mis repositorios para esas consultas?" Sí, pero aquí es por qué no estoy haciendo esto:
- Mis repositorios están diseñados para trabajar con objetos modelo. En una aplicación del mundo real, ¿por qué necesitaría obtener el
password
campo si estoy buscando una lista de todos mis usuarios?
- Los repositorios a menudo son específicos del modelo, pero las consultas a menudo involucran más de un modelo. Entonces, ¿en qué repositorio pones tu método?
- Esto mantiene mis repositorios muy simples, no una clase hinchada de métodos.
- Todas las consultas ahora están organizadas en sus propias clases.
- Realmente, en este punto, existen repositorios simplemente para abstraer mi capa de base de datos.
Para mi ejemplo, crearé un objeto de consulta para buscar "AllUsers". Aquí está la interfaz:
interface AllUsersQueryInterface
{
public function fetch($fields);
}
Implementación de objeto de consulta
Aquí es donde podemos usar un mapeador de datos nuevamente para ayudar a acelerar el desarrollo. Tenga en cuenta que estoy permitiendo un ajuste al conjunto de datos devuelto: los campos. Esto es lo más lejos que quiero llegar manipulando la consulta realizada. Recuerde, mis objetos de consulta no son constructores de consultas. Simplemente realizan una consulta específica. Sin embargo, como sé que probablemente usaré este mucho, en varias situaciones diferentes, me estoy dando la capacidad de especificar los campos. ¡Nunca quiero devolver campos que no necesito!
class AllUsersQuery implements AllUsersQueryInterface
{
protected $db;
public function __construct(Database $db)
{
$this->db = $db;
}
public function fetch($fields)
{
return $this->db->select($fields)->from('users')->orderBy('last_name, first_name')->rows();
}
}
Antes de pasar al controlador, quiero mostrar otro ejemplo para ilustrar cuán poderoso es esto. Tal vez tengo un motor de informes y necesito crear un informe para AllOverdueAccounts
. Esto podría ser complicado con mi mapeador de datos, y es posible que desee escribir algo real SQL
en esta situación. No hay problema, así es como podría verse este objeto de consulta:
class AllOverdueAccountsQuery implements AllOverdueAccountsQueryInterface
{
protected $db;
public function __construct(Database $db)
{
$this->db = $db;
}
public function fetch()
{
return $this->db->query($this->sql())->rows();
}
public function sql()
{
return "SELECT...";
}
}
Esto mantiene muy bien toda mi lógica para este informe en una clase, y es fácil de probar. Puedo burlarme del contenido de mi corazón, o incluso usar una implementación completamente diferente.
El controlador
Ahora la parte divertida: unir todas las piezas. Tenga en cuenta que estoy usando inyección de dependencia. Por lo general, las dependencias se inyectan en el constructor, pero en realidad prefiero inyectarlas directamente en mis métodos de controlador (rutas). Esto minimiza el gráfico de objetos del controlador, y en realidad lo encuentro más legible. Tenga en cuenta que si no le gusta este enfoque, simplemente use el método de constructor tradicional.
class UsersController
{
public function index(AllUsersQueryInterface $query)
{
// Fetch user data
$users = $query->fetch(['first_name', 'last_name', 'email']);
// Return view
return Response::view('all_users.php', ['users' => $users]);
}
public function add()
{
return Response::view('add_user.php');
}
public function insert(UserRepositoryInterface $repository)
{
// Create new user model
$user = new User;
$user->first_name = $_POST['first_name'];
$user->last_name = $_POST['last_name'];
$user->gender = $_POST['gender'];
$user->email = $_POST['email'];
// Save the new user
$repository->save($user);
// Return the id
return Response::json(['id' => $user->id]);
}
public function view(SpecificUserQueryInterface $query, $id)
{
// Load user data
if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
return Response::notFound();
}
// Return view
return Response::view('view_user.php', ['user' => $user]);
}
public function edit(SpecificUserQueryInterface $query, $id)
{
// Load user data
if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
return Response::notFound();
}
// Return view
return Response::view('edit_user.php', ['user' => $user]);
}
public function update(UserRepositoryInterface $repository)
{
// Load user model
if (!$user = $repository->find($id)) {
return Response::notFound();
}
// Update the user
$user->first_name = $_POST['first_name'];
$user->last_name = $_POST['last_name'];
$user->gender = $_POST['gender'];
$user->email = $_POST['email'];
// Save the user
$repository->save($user);
// Return success
return true;
}
public function delete(UserRepositoryInterface $repository)
{
// Load user model
if (!$user = $repository->find($id)) {
return Response::notFound();
}
// Delete the user
$repository->delete($user);
// Return success
return true;
}
}
Pensamientos finales:
Lo importante a tener en cuenta aquí es que cuando estoy modificando (creando, actualizando o eliminando) entidades, estoy trabajando con objetos de modelos reales y realizando la persistencia a través de mis repositorios.
Sin embargo, cuando estoy visualizando (seleccionando datos y enviándolos a las vistas) no estoy trabajando con objetos modelo, sino más bien con objetos de valor antiguo. Solo selecciono los campos que necesito y está diseñado para que pueda maximizar el rendimiento de mi búsqueda de datos.
Mis repositorios se mantienen muy limpios y, en cambio, este "desorden" se organiza en mis consultas modelo.
Utilizo un mapeador de datos para ayudar con el desarrollo, ya que es ridículo escribir SQL repetitivo para tareas comunes. Sin embargo, absolutamente puede escribir SQL donde sea necesario (consultas complicadas, informes, etc.). Y cuando lo haces, está bien escondido en una clase con el nombre adecuado.
¡Me encantaría escuchar tu opinión sobre mi enfoque!
Actualización de julio de 2015:
Me han preguntado en los comentarios dónde terminé con todo esto. Bueno, no tan lejos en realidad. A decir verdad, todavía no me gustan los repositorios. Los encuentro excesivos para búsquedas básicas (especialmente si ya está usando un ORM), y desordenados al trabajar con consultas más complicadas.
Por lo general, trabajo con un ORM de estilo ActiveRecord, por lo que a menudo solo haré referencia a esos modelos directamente en toda mi aplicación. Sin embargo, en situaciones donde tengo consultas más complejas, usaré objetos de consulta para hacerlos más reutilizables. También debo tener en cuenta que siempre inyecto mis modelos en mis métodos, lo que hace que sean más fáciles de burlar en mis pruebas.