1. Code
  2. PHP

Evolucionando hacia una capa de persistencia

Scroll to top
17 min read
This post is part of a series called Test-Driven PHP.
Mockery: A Better Way

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

High level schemaHigh level schemaHigh level schema

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

Gateway PatternGateway PatternGateway Pattern

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:

Active record patternActive record patternActive record pattern

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:

Gateway in high level schemaGateway in high level schemaGateway in high level schema

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.