Spanish (Español) translation by Elías Nicolás (you can also view the original English article)
Uno de los patrones de diseño más confusos es la persistencia. La necesidad de que una aplicación persista en su estado interno y en los datos es tan grande que es probable que existan decenas, sino cientos, de diferentes tecnologías para abordar este problema. Desafortunadamente, ninguna tecnología es una bala mágica. Cada aplicación, y a veces cada componente de la aplicación, es única a su manera, por lo tanto, requiere una solución única.
En este tutorial, le enseñaré algunas de las mejores prácticas para ayudarlo a determinar qué enfoque tomar, cuando trabaje en aplicaciones futuras. Discutiré brevemente algunas inquietudes y principios de diseño de alto nivel, seguidos de una vista más detallada sobre el patrón de diseño Active Record, combinado con unas pocas palabras sobre el patrón de diseño Table Data Gateway.
Por supuesto, no solo te enseñaré la teoría detrás del diseño, sino que también te guiaré a través de un ejemplo que comienza como un código aleatorio y se transforma en una solución de persistencia estructurada.
Dos cuentos de una sola aplicación
La base de datos es para datos, no para código
Hoy, ningún programador puede entender este sistema arcaico.
El
proyecto más antiguo en el que tengo que trabajar comenzó en el año
2000. En aquel entonces, un equipo de programadores comenzó un nuevo
proyecto evaluando diferentes requisitos, pensando en las cargas de
trabajo que la aplicación tendría que manejar, probando diferentes
tecnologías y llegando a una conclusión: todos el código PHP de la
aplicación, excepto el archivo index.php, debe residir en una base de
datos MySQL. Su decisión puede sonar escandalosa hoy, pero fue aceptable
hace doce años (OK ... tal vez no).
Comenzaron por crear sus tablas base, y luego otras tablas para cada página web. La solución funcionó ... por un tiempo. Los autores originales sabían cómo mantenerlo, pero luego cada autor se fue uno a uno, dejando el código base en manos de otros recién llegados.
Hoy, ningún programador puede entender este sistema arcaico. Todo comienza con una consulta MySQL de index.php. El resultado de esa
consulta devuelve un código PHP que ejecuta aún más consultas. El
escenario más simple implica al menos cinco tablas de base de datos. Naturalmente, no hay pruebas o especificaciones. Modificar algo es un
no-go, y simplemente tenemos que reescribir todo el módulo si algo sale
mal.
Los desarrolladores originales ignoraron el hecho de que una base de datos solo debería contener datos, no lógica comercial o presentación. Mezclaron el código PHP y HTML con MySQL e ignoraron los conceptos de diseño de alto nivel.
El HTML es solo para presentación y presentación
Todas las aplicaciones deben concentrarse en respetar un diseño limpio y de alto nivel.
Con el paso del tiempo, los nuevos programadores necesitaron agregar características adicionales al sistema mientras que, al mismo tiempo, corrigieron viejos errores. No había forma de continuar usando las tablas de MySQL para todo, y todos los involucrados en mantener el código coincidieron en que su diseño era terriblemente defectuoso. De modo que los nuevos programadores evaluaron diferentes requisitos, pensaron en las cargas de trabajo que la aplicación debería manejar, probaron diferentes tecnologías y llegaron a una conclusión: decidieron mover la mayor cantidad de código posible a la presentación final. Una vez más, esta decisión puede sonar escandalosa hoy, pero fue años luz del diseño escandaloso anterior.
Los desarrolladores adoptaron un marco de plantillas y basaron la aplicación en torno a él, comenzando cada nueva característica y módulo con una nueva plantilla. Fue fácil; la plantilla era descriptiva y sabían dónde encontrar el código que realiza una tarea específica. Pero así es como terminaron con los archivos de plantilla que contienen el lenguaje específico del dominio (DSL) del motor, HTML, PHP y, por supuesto, las consultas de MySQL.
Hoy, mi equipo solo mira y se pregunta. Es un milagro que muchas de las vistas realmente funcionen. Puede tomar una gran cantidad de tiempo solo para determinar cómo se obtiene la información de la base de datos a la vista. Al igual que su predecesor, ¡todo es un gran desastre!
Esos desarrolladores ignoraron el hecho de que una vista no debería contener lógica comercial o de persistencia. Mezclaron el código PHP y HTML con MySQL e ignoraron los conceptos de diseño de alto nivel.
Diseño de aplicaciones de alto nivel


Un simulacro es un objeto que actúa como su contraparte real, pero no ejecuta el código real.
Todas las aplicaciones deben concentrarse en respetar un diseño limpio y de alto nivel. Esto no siempre se puede lograr, pero debe ser una alta prioridad. Un buen diseño de alto nivel tiene una lógica empresarial bien aislada. La creación, persistencia y entrega de objetos están fuera del núcleo y las dependencias apuntan solo hacia la lógica de negocios.
Aislar la lógica de negocios abre la puerta a grandes posibilidades, y todo se convierte en algo así como un complemento, si las dependencias externas siempre apuntan hacia la lógica de negocios. Por ejemplo, puede cambiar la pesada base de datos MySQL con una base de datos liviana SQLite3.
- Imagine poder abandonar su marco MVC actual y reemplazarlo por otro, sin tocar la lógica comercial.
- Imagine entregar los resultados de su aplicación a través de una API de terceros y no a través de HTTP, o cambiar cualquier tecnología de terceros que use hoy (excepto el lenguaje de programación, por supuesto) sin tocar la lógica comercial (o sin mucha molestia).
- Imagina hacer todos estos cambios y tus pruebas aún pasarían.
Implementando una solución de trabajo para persistir en una publicación de blog
Para
identificar mejor los problemas con un diseño malo, aunque funcional,
comenzaré con un simple ejemplo de, lo adivinaron, un blog. A
lo largo de este tutorial, seguiré algunos principios de desarrollo
impulsados por pruebas (TDD) y haré que las pruebas sean fácilmente
comprensibles, incluso si no tienes experiencia con TDD. Imaginemos que
usa un framework MVC. Al guardar una publicación de blog, un controlador
llamado BlogPost ejecuta un método save(). Este método se conecta a
una base de datos SQLite para almacenar una publicación de blog en la
base de datos.
Vamos a crear una carpeta, llamada Datos Data en la carpeta de nuestro código y busque ese directorio en la consola. Crea una base de datos y una tabla, como esta:
1 |
$ sqlite3 MyBlog
|
2 |
SQLite version 3.7.13 2012-06-11 02:05:22 |
3 |
Enter ".help" for instructions |
4 |
Enter SQL statements terminated with a ";"
|
5 |
sqlite> create table BlogPosts (
|
6 |
title varchar(120) primary key, |
7 |
content text, |
8 |
published_timestamp timestamp); |
Nuestro método save() obtiene los valores del formulario como una matriz, llamada $data:
1 |
class BlogPostController { |
2 |
|
3 |
function save($data) { |
4 |
$dbhandle = new SQLite3('Data/MyBlog'); |
5 |
|
6 |
$query = 'INSERT INTO BlogPosts VALUES("' . $data['title'] . '","' . $data['content'] . '","' . time(). '")'; |
7 |
$dbhandle->exec($query); |
8 |
}
|
9 |
|
10 |
}
|
Este código funciona, y puedes verificarlo llamándolo desde otra clase, pasando una matriz predefinida $data, como esta:
1 |
$this->object = new BlogPostController; |
2 |
|
3 |
$data['title'] = 'First Post Title'; |
4 |
$data['content'] = 'Some cool content for the first post'; |
5 |
$data['published_timestamp'] = time(); |
6 |
|
7 |
$this->object->save($data); |
El contenido de la variable $data fue efectivamente guardado en la base de datos:
1 |
sqlite> select * from BlogPosts; |
2 |
First Post Title|Some cool content for the first post|1345665216 |
Pruebas de caracterización
La herencia es el tipo más fuerte de dependencia.
Una prueba de caracterización describe y verifica el comportamiento actual del código preexistente. Se utiliza con mayor frecuencia para caracterizar el código heredado, y hace que la refacturación de ese código sea mucho más fácil.
Una prueba de caracterización puede probar un módulo, una unidad, o recorrer todo el camino desde la interfaz de usuario a la base de datos; todo depende de lo que queremos probar. En nuestro caso, dicha prueba debe ejercer el controlador y verificar el contenido de la base de datos. Esta no es una prueba de unidad, funcional o de integración típica, y generalmente no se puede asociar con ninguno de esos niveles de prueba.
Las pruebas de caracterización son una red de seguridad temporal, y normalmente las eliminamos después de que el código se refactoriza adecuadamente y se prueba la unidad. Aquí hay una implementación de una prueba, colocada en la carpeta Prueba Test :
1 |
require_once '../BlogPostController.php'; |
2 |
|
3 |
class BlogPostControllerTest extends PHPUnit_Framework_TestCase { |
4 |
private $object; |
5 |
private $dbhandle; |
6 |
|
7 |
function setUp() { |
8 |
$this->object = new BlogPostController; |
9 |
$this->dbhandle = new SQLite3('../Data/MyBlog'); |
10 |
}
|
11 |
|
12 |
|
13 |
function testSave() { |
14 |
$this->cleanUPDatabase(); |
15 |
|
16 |
$data['title'] = 'First Post Title'; |
17 |
$data['content'] = 'Some cool content for the first post'; |
18 |
$data['published_timestamp'] = time(); |
19 |
$this->object->save($data); |
20 |
|
21 |
$this->assertEquals($data, $this->getPostsFromDB()); |
22 |
|
23 |
}
|
24 |
|
25 |
private function cleanUPDatabase() { |
26 |
$this->dbhandle->exec('DELETE FROM BlogPosts'); |
27 |
}
|
28 |
|
29 |
private function getPostsFromDB() { |
30 |
$result = $this->dbhandle->query('SELECT * FROM BlogPosts'); |
31 |
return $result->fetchArray(SQLITE3_ASSOC); |
32 |
}
|
33 |
|
34 |
}
|
Esta prueba crea un nuevo objeto controlador y ejecuta su método save(). La prueba luego lee la información de la base de datos y la compara con
la matriz $data[] predefinida. Preformamos
esta comparación utilizando el método $this->assertEquals(), una
afirmación que supone que sus parámetros son iguales. Si son diferentes,
la prueba falla. Además, limpiamos la tabla de la base de datos
BlogPosts cada vez que ejecutamos la prueba.
El código heredado es código no probado. - Michael Feathers
Con nuestra prueba en
funcionamiento, limpiemos un poco el código. Abra la base de datos con
el nombre completo del directorio y use sprintf() para componer la
cadena de consulta. Esto da como resultado un código mucho más simple:
1 |
class BlogPostController { |
2 |
|
3 |
function save($data) { |
4 |
$dbhandle = new SQLite3(__DIR__ . '/Data/MyBlog'); |
5 |
|
6 |
$query = sprintf('INSERT INTO BlogPosts VALUES ("%s","%s","%s")', $data['title'], $data['content'], time()); |
7 |
|
8 |
$dbhandle->exec($query); |
9 |
}
|
10 |
|
11 |
}
|
El patrón Table Data Gateway


Reconocemos
que nuestro código debe trasladarse desde el controlador a la lógica de
negocios y la capa de persistencia, y el patrón de puerta de enlace
puede ayudarnos a comenzar a recorrer esa ruta. Aquí está el método revisado testSave():
1 |
function testItCanPersistABlogPost() { |
2 |
$data = array('title' => 'First Post Title', 'content' => 'Some content.', 'timestamp' => time()); |
3 |
$blogPost = new BlogPost($data['title'], $data['content'], $data['timestamp']); |
4 |
|
5 |
$mockedPersistence = $this->getMock('SqlitePost'); |
6 |
$mockedPersistence->expects($this->once())->method('persist')->with($blogPost); |
7 |
|
8 |
$controller = new BlogPostController($mockedPersistence); |
9 |
$controller->save($data); |
10 |
}
|
Esto representa cómo queremos usar el método save() en el controlador. Esperamos que el controlador llame a un método llamado persist($blogPostObject) en el objeto de la puerta de enlace. Cambiemos nuestro
BlogPostController para hacer eso:
1 |
class BlogPostController { |
2 |
private $gateway; |
3 |
|
4 |
function __construct(Gateway $gateway = null) { |
5 |
$this->gateway = $gateway ? : new SqlitePost(); |
6 |
}
|
7 |
|
8 |
function save($data) { |
9 |
$this->gateway->persist(new BlogPost($data['title'], $data['content'], $data['timestamp'])); |
10 |
}
|
11 |
|
12 |
}
|
Un buen diseño de alto nivel tiene una lógica bien aislada.
¡Bonito! Nuestro BlogPostController se volvió mucho más simple. Utiliza
la puerta de enlace (ya sea suministrada o instanciada) para persistir
los datos llamando a su método persist(). No hay absolutamente ningún
conocimiento sobre cómo persisten los datos; la lógica de persistencia
se volvió modular.
En
la prueba anterior, creamos el controlador con un objeto de
persistencia falso, asegurando que los datos nunca se escriban en la
base de datos cuando se ejecuta la prueba. En el
código de producción, el controlador crea su propio objeto persistente
para persistir los datos utilizando un objeto SqlitePost. Un simulacro
es un objeto que actúa como su equivalente real, pero no ejecuta el
código real.
Ahora recuperemos una publicación de blog del almacén de datos. Es tan fácil como guardar datos, pero tenga en cuenta que modifiqué un poco la prueba.
1 |
require_once '../BlogPostController.php'; |
2 |
require_once '../BlogPost.php'; |
3 |
|
4 |
require_once '../SqlitePost.php'; |
5 |
|
6 |
class BlogPostControllerTest extends PHPUnit_Framework_TestCase { |
7 |
private $mockedPersistence; |
8 |
private $controller; |
9 |
private $data; |
10 |
|
11 |
function setUp() { |
12 |
$this->mockedPersistence = $this->getMock('SqlitePost'); |
13 |
$this->controller = new BlogPostController($this->mockedPersistence); |
14 |
$this->data = array('title' => 'First Post Title', 'content' => 'Some content.', 'timestamp' => time()); |
15 |
}
|
16 |
|
17 |
function testItCanPersistABlogPost() { |
18 |
$blogPost = $this->aBlogPost(); |
19 |
$this->mockedPersistence->expects($this->once())->method('persist')->with($blogPost); |
20 |
|
21 |
$this->controller->save($this->data); |
22 |
}
|
23 |
|
24 |
function testItCanRetrievABlogPostByTitle() { |
25 |
$expectedBlogpost = $this->aBlogPost(); |
26 |
$this->mockedPersistence->expects($this->once()) |
27 |
->method('findByTitle')->with($this->data['title']) |
28 |
->will($this->returnValue($expectedBlogpost)); |
29 |
|
30 |
$this->assertEquals($expectedBlogpost, $this->controller->findByTitle($this->data['title'])); |
31 |
}
|
32 |
|
33 |
public function aBlogPost() { |
34 |
return new BlogPost($this->data['title'], $this->data['content'], $this->data['timestamp']); |
35 |
}
|
36 |
|
37 |
}
|
Y la implementación en BlogPostController es solo un método de declaración:
1 |
function findByTitle($title) { |
2 |
return $this->gateway->findByTitle($title); |
3 |
}
|
¿No es genial? La clase BlogPost ahora es parte de la lógica empresarial (recuerde el
esquema de diseño de alto nivel de arriba). La UI / MVC crea objetos
BlogPost y utiliza implementaciones concretas de Gateway para conservar
los datos. Todas las dependencias apuntan a la lógica de trabajo.
Solo
queda un paso: crear una implementación concreta de Gateway. La
siguiente es la clase SqlitePost:
1 |
require_once 'Gateway.php'; |
2 |
|
3 |
class SqlitePost implements Gateway { |
4 |
private $dbhandle; |
5 |
|
6 |
function __construct($dbhandle = null) { |
7 |
$this->dbhandle = $dbhandle ? : new SQLite3(__DIR__ . '/Data/MyBlog'); |
8 |
}
|
9 |
|
10 |
public function persist(BlogPost $blogPost) { |
11 |
$query = sprintf('INSERT INTO BlogPosts VALUES ("%s","%s","%s")', $blogPost->title, $blogPost->content, $blogPost->timestamp); |
12 |
$this->dbhandle->exec($query); |
13 |
}
|
14 |
|
15 |
public function findByTitle($title) { |
16 |
$SqliteResult = $this->dbhandle->query(sprintf('SELECT * FROM BlogPosts WHERE title = "%s"', $title)); |
17 |
$blogPostAsString = $SqliteResult->fetchArray(SQLITE3_ASSOC); |
18 |
return new BlogPost($blogPostAsString['title'], $blogPostAsString['content'], $blogPostAsString['timestamp']); |
19 |
}
|
20 |
}
|
Nota: La prueba para esta implementación también está disponible en el código fuente, pero, debido a su complejidad y duración, no la incluí aquí.
Avanzando hacia el patrón de registro activo
Active Record es uno de los patrones más controvertidos. Algunos lo abrazan (como Rails y CakePHP), y otros lo evitan. Muchas aplicaciones de mapeo relacional de objetos (ORM) usan este patrón para guardar objetos en tablas. Aquí está su esquema:


Como puede ver, los objetos basados en Active Record pueden
persistir y recuperarse. Esto generalmente se logra al extender una
clase ActiveRecordBase, una clase que sabe cómo trabajar con la base de
datos.
El mayor problema con Active Record es la extensión de la dependencia. Como todos sabemos, la herencia es el tipo más fuerte de dependencia, y lo mejor es evitarla la mayor parte del tiempo.
Antes de ir más allá, aquí es donde estamos ahora:


La
interfaz de puerta de enlace pertenece a la lógica de negocios, y sus
implementaciones concretas pertenecen a la capa de persistencia. Nuestro
BlogPostController tiene dos dependencias, ambas apuntan hacia
la lógica comercial: la puerta de enlace SqlitePost y la clase
BlogPost.
Ir por registro activo
Hay muchos otros patrones, como el patrón de proxy, que están estrechamente relacionados con la persistencia.
Si
tuviéramos que seguir el patrón Active Record exactamente como lo
presenta Martin Fowler en su libro de 2003, Patterns of Enterprise
Application Architecture, entonces tendríamos que mover las consultas
SQL a la clase BlogPost. Esto,
sin embargo, tiene el problema de violar tanto el Principio de
Inversión de Dependencia como el Principio Abierto Cerrado. El Principio
de Inversión de Dependencia establece que:
- Los módulos de alto nivel no deben depender de módulos de bajo nivel. Ambos deberían depender de abstracciones.
- Las abstracciones no deberían depender de detalles. Los detalles deben depender de las abstracciones.
Y el Principio Abierto Cerrado declara: las entidades de software (clases, módulos, funciones, etc.) deben estar abiertas para la extensión, pero cerradas para su modificación. Tomaremos un enfoque más interesante e integraremos el portal en nuestra solución Active Record.
Si
intenta hacer esto por su cuenta, probablemente ya se haya dado cuenta
de que agregar el patrón de registro activo al código arruinará las
cosas. Por
esta razón, tomé la opción de deshabilitar el controlador y las pruebas
de SqlitePost para concentrarme solo en la clase BlogPost. Los
primeros pasos son: haga que BlogPost se cargue a sí mismo
estableciendo su constructor como privado y conéctelo a la interfaz de
la puerta de enlace. Aquí está la primera versión del archivo
BlogPostTest:
1 |
require_once '../BlogPost.php'; |
2 |
require_once '../InMemoryPost.php'; |
3 |
require_once '../ActiveRecordBase.php'; |
4 |
|
5 |
class BlogPostTest extends PHPUnit_Framework_TestCase { |
6 |
|
7 |
|
8 |
function testItCanConnectPostToGateway() { |
9 |
$blogPost = BlogPost::load(); |
10 |
$blogPost->setGateway($this->inMemoryPost()); |
11 |
$this->assertEquals($blogPost->getGateway(), $this->inMemoryPost()); |
12 |
}
|
13 |
|
14 |
function testItCanCreateANewAndEmptyBlogPost() { |
15 |
$blogPost = BlogPost::load(); |
16 |
$this->assertNull($blogPost->title); |
17 |
$this->assertNull($blogPost->content); |
18 |
$this->assertNull($blogPost->timestamp); |
19 |
$this->assertInstanceOf('Gateway', $blogPost->getGateway()); |
20 |
}
|
21 |
|
22 |
private function inMemoryPost() { |
23 |
return new InMemoryPost(); |
24 |
}
|
25 |
|
26 |
}
|
Comprueba que una publicación de blog se inicializó correctamente y que puede tener una puerta de enlace si está configurada. Es una buena práctica usar múltiples aseveraciones cuando todos prueban el mismo concepto y lógica.
Nuestra segunda prueba tiene varias
afirmaciones, pero todas se refieren al mismo concepto común de
publicación de blog vacía. Por supuesto, la clase BlogPost también se ha
modificado:
1 |
class BlogPost { |
2 |
private $title; |
3 |
private $content; |
4 |
private $timestamp; |
5 |
private static $gateway; |
6 |
|
7 |
private function __construct($title = null, $content = null, $timestamp = null) { |
8 |
$this->title = $title; |
9 |
$this->content = $content; |
10 |
$this->timestamp = $timestamp; |
11 |
}
|
12 |
|
13 |
function __get($name) { |
14 |
return $this->$name; |
15 |
}
|
16 |
|
17 |
function setGateway($gateway) { |
18 |
self::$gateway = $gateway; |
19 |
}
|
20 |
|
21 |
function getGateway() { |
22 |
return self::$gateway; |
23 |
}
|
24 |
|
25 |
static function load() { |
26 |
if(!self::$gateway) self::$gateway = new SqlitePost(); |
27 |
return new self; |
28 |
}
|
29 |
}
|
Ahora tiene un método load() que devuelve un nuevo objeto con una puerta de enlace válida. A
partir de este momento, continuaremos con la implementación de un
método load($title) para crear un nuevo BlogPost con información de la
base de datos. Para una prueba fácil, implementé una clase InMemoryPost
para la persistencia. Simplemente mantiene una lista de objetos en la
memoria y devuelve la información como se desee:
1 |
class InMemoryPost implements Gateway { |
2 |
private $blogPosts = array(); |
3 |
|
4 |
public function findByTitle($blogPostTitle) { |
5 |
return array( |
6 |
'title' => $this->blogPosts[$blogPostTitle]->title, |
7 |
'content' => $this->blogPosts[$blogPostTitle]->content, |
8 |
'timestamp' => $this->blogPosts[$blogPostTitle]->timestamp); |
9 |
|
10 |
}
|
11 |
|
12 |
public function persist(BlogPost $blogPostObject) { |
13 |
$this->blogPosts[$blogPostObject->title] = $blogPostObject; |
14 |
}
|
15 |
}
|
Luego, me
di cuenta de que la idea inicial de conectar BlogPost a una puerta de
enlace a través de un método separado era inútil. Entonces, modifiqué las pruebas, en consecuencia:
1 |
class BlogPostTest extends PHPUnit_Framework_TestCase { |
2 |
|
3 |
function testItCanCreateANewAndEmptyBlogPost() { |
4 |
$blogPost = BlogPost::load(); |
5 |
$this->assertNull($blogPost->title); |
6 |
$this->assertNull($blogPost->content); |
7 |
$this->assertNull($blogPost->timestamp); |
8 |
}
|
9 |
|
10 |
function testItCanLoadABlogPostByTitle() { |
11 |
$gateway = $this->inMemoryPost(); |
12 |
$aBlogPosWithData = $this->aBlogPostWithData($gateway); |
13 |
|
14 |
$gateway->persist($aBlogPosWithData); |
15 |
|
16 |
$this->assertEquals($aBlogPosWithData, BlogPost::load('some_title', null, null, $gateway)); |
17 |
}
|
18 |
|
19 |
private function inMemoryPost() { |
20 |
return new InMemoryPost(); |
21 |
}
|
22 |
|
23 |
private function aBlogPostWithData($gateway = null) { |
24 |
return BlogPost::load('some_title', 'some content', '123', $gateway); |
25 |
}
|
26 |
|
27 |
}
|
Como puede ver, cambié radicalmente la forma en que se usa BlogPost.
1 |
class BlogPost { |
2 |
private $title; |
3 |
private $content; |
4 |
private $timestamp; |
5 |
|
6 |
private function __construct($title = null, $content = null, $timestamp = null) { |
7 |
$this->title = $title; |
8 |
$this->content = $content; |
9 |
$this->timestamp = $timestamp; |
10 |
}
|
11 |
|
12 |
function __get($name) { |
13 |
return $this->$name; |
14 |
}
|
15 |
|
16 |
static function load($title = null, $content = null, $timestamp = null, $gateway = null) { |
17 |
$gateway = $gateway ? : new SqlitePost(); |
18 |
|
19 |
if(!$content) { |
20 |
$postArray = $gateway->findByTitle($title); |
21 |
if ($postArray) return new self($postArray['title'], $postArray['content'], $postArray['timestamp']); |
22 |
}
|
23 |
|
24 |
return new self($title, $content, $timestamp); |
25 |
}
|
26 |
}
|
El método load() comprueba el parámetro $content para un valor y crea un nuevo BlogPost si se proporcionó un valor. Si no, el método intenta encontrar una publicación de blog con el título
dado. Si se encuentra una publicación, se devuelve; si no hay ninguno,
el método crea un objeto de BlogPost vacío.
Para que este código
funcione, también tendremos que cambiar el funcionamiento de la puerta
de enlace. Nuestra
implementación necesita devolver una matriz asociativa con elementos title, content, y timestamp en lugar del objeto en sí. Esta
es una convención que he elegido. Puede encontrar otras variantes, como
una matriz simple, más atractiva. Aquí están las modificaciones en
SqlitePostTest:
1 |
function testItCanRetrieveABlogPostByItsTitle() { |
2 |
[...] |
3 |
//we expect an array instead of an object
|
4 |
$this->assertEquals($this->blogPostAsArray, $gateway->findByTitle($this->blogPostAsArray['title'])); |
5 |
}
|
6 |
private function aBlogPostWithValues() { |
7 |
//we use static load instead of constructor call
|
8 |
return $blogPost = BlogPost::load( |
9 |
$this->blogPostAsArray['title'], |
10 |
$this->blogPostAsArray['content'], |
11 |
$this->blogPostAsArray['timestamp']); |
12 |
}
|
Y los cambios de implementación son:
1 |
public function findByTitle($title) { |
2 |
$SqliteResult = $this->dbhandle->query(sprintf('SELECT * FROM BlogPosts WHERE title = "%s"', $title)); |
3 |
//return the result directly, don't construct the object
|
4 |
return $SqliteResult->fetchArray(SQLITE3_ASSOC); |
5 |
}
|
Casi terminamos. Agregue un método persist() al BlogPost y llame a todos los métodos
recién implementados desde el controlador. Aquí está el método persist() que simplemente usará el método persist() de la puerta de enlace:
1 |
private function persist() { |
2 |
$this->gateway->persist($this); |
3 |
}
|
Y el controlador:
1 |
class BlogPostController { |
2 |
|
3 |
function save($data) { |
4 |
$blogPost = BlogPost::load($data['title'], $data['content'], $data['timestamp']); |
5 |
$blogPost->persist(); |
6 |
}
|
7 |
|
8 |
function findByTitle($title) { |
9 |
return BlogPost::load($title); |
10 |
}
|
11 |
|
12 |
}
|
El BlogPostController se volvió tan simple que eliminé todas sus pruebas. Simplemente llama al método persist() del objeto BlogPost. Naturalmente, querrá agregar pruebas si, y cuándo, tiene más código en
el controlador. La descarga del código todavía contiene un archivo de
prueba para BlogPostController, pero se comenta su
contenido.
Conclusión
Esto es sólo la punta del iceberg.
Has visto dos
implementaciones de persistencia diferentes: los patrones de puerta de
enlace y registro activo. A
partir de este punto, puede implementar una clase abstracta de
ActiveRecordBase para extender para todas sus clases que necesitan
persistencia. Esta clase abstracta puede usar diferentes puertas de
enlace para
persistir en los datos, y cada implementación puede incluso usar una
lógica diferente para satisfacer sus necesidades.
Pero esto es solo la punta del iceberg. Hay muchos otros patrones, como el patrón de proxy, que están estrechamente relacionados con la persistencia; cada patrón funciona para una situación particular. Recomiendo que siempre implemente la solución más simple primero, y luego implemente otro patrón cuando sus necesidades cambien.
Espero que hayan disfrutado este tutorial, y espero ansiosamente sus opiniones e implementaciones alternativas a mi solución en los comentarios a continuación.



