Cuando en una clase necesitamos pasar múltiples datos, ya sea como entrada (parámetros) o como salida (retorno) se tiende a utilizar arrays. Dada su sencillez de manejo y adaptabilidad se utilizan para contener estos valores que son pasados de método a método, o incluso, de clase a clase. Esto hace que el código sea menos estructurado, más difícil de testear y sus errores menos trazables. Estos meses he podido leer varios argumentos a favor del uso de objetos de datos, que conceptualmente, podemos aproximarlos a la implementación de Value Object. Según la Wikipedia es un pequeño objeto que no es identificado por su clase en si, si no por su equidad, dos value object son iguales cuando contienen los mismos valores (==), no cuando son el mismo objeto (===), su cometido es mantener los datos inmutables para su transporte entre las piezas de la lógica. En realidad es uno de los elementos propios de diseño para Domain Driven Development, creo que podemos abstraer algun concepto de su implementación, como la inmutabilidad, para mejorar un poco lo que hacemos. Quizás el concepto sea muy estricto, solo lo voy a utilizar como referente, pero se acerca mucho al sentido de lo que me gustaría implementar.

Cuando utilizarlos

En general cuando tenemos un retorno complejo de una función que necesitamos estructurar en un array, o, en funciones con muchos parámetros de entrada, condensamos estos datos en las propiedades de un objeto destinado a transportar datos. De esta manera ganamos en control y claridad, tanto del código que se vuelve más verboso, como de la documentación, descrita en el propio fuente del objeto. Pongamos como ejemplo una búsqueda en la tabla de Blog desde un buscador (formulario HTML) con varios conceptos para realizar el filtro, pongamos puede ser acotada en un rango de fechas, puede filtrar un texto entre los títulos de los posts, también filtra por autor y categoría. El código de llamada al modelo podría acabar en algo así.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
namespace MyApp\Model;

class blog
{

public function getLastSearchParams()
{
$finderParams = $_SESSION['blogLastSeach'];

// $finderParams['dateInit'] = $_SESSION['blogLastSeach']['dateInit'];
// $finderParams['dateEnd'] = $_SESSION['blogLastSeach']['dateEnd'];
// $finderParams['dateInit'] = $_SESSION['blogLastSeach']['title'];
// ...

return $finderParams;
}

public function findPosts($dateInit, $dateEnd, $title, $author, $category, $limit, $page)
{
// Lógica aquí.
}
}

Como se puede apreciar en el primer método, la función hace una salida de datos compleja, generalmente esos datos se encapsulan en un array antes de ser devueltos. Pero, podemos reemplazar ese array de salida con un objeto alimentado a partir de los datos de la sesión. hay que hacer notar que al almacenar los valores en la sesión y recuperarlos no tenemos documentación ninguna de los datos que transmitimos al flujo, debemos extrapolarlos del propio código.

También es aplicable en el caso inverso, en el segundo método la búsqueda avanzada dispone de muchos parámetros, si necesitamos incluir también un campo para escoger las tags asociadas a los artículos entre los que buscar, tendríamos que modificar la estructura de la función, su documentación y todos los puntos donde se llama. Es más estructurado y legible asociar los parámetros específicos de la búsqueda a un objeto y los relativos a la paginación en otro. Incluso poniéndonos un poco puristas crearemos un objeto para contener los rangos de fechas.

Comencemos por el objeto de rango de fechas, en el constructor solo comprobamos que el propio rango sea válido, la fecha inicial menor que la final. Le dotamos de getters, constructor y de la función __sleep(). En el ejemplo, si queremos almacenar directamente el objeto en sesión tendremos que declarar el método mágico __sleep(), la sesión se almacena serializada y debemos indicar qué propiedades vamos a almacenar.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
namespace MyApp\Value;

use MyApp\Exception\ParametersException;

/**
* Class documentation
*/
class DateRange {
/**
* @var \DateTime Initial Date
*/
private $dateInit;

/**
* @var \DateTime End date
*/
private $dateEnd;

/**
* Main constructor
*
* @param \DateTime $dateInit Initial Date
* @param \DateTime $dateEnd End date
*
* @throws ParameterException
*/
public function __construct(\DateTime $dateInit, \DateTime $dateEnd)
{
if($dateInit > $dateEnd) {
throw new ParametersException(
'Date end can\'t be higher than date init'
);
}

$this->dateInit = $dateInit;
$this->dateEnd = $dateEnd;
}

/**
* date init getter
*
* @return \DateTime
*/
public function getDateInit()
{
return $this->dateInit;
}

/**
* date end getter
*
* @return \DateTime
*/
public function getDateEnd()
{
return $this->dateEnd;
}

/**
* Magic method to be called before serialization
*
* @return array
*/
public function __sleep()
{
return array('dateInit', 'dateEnd');
}

}

Ahora es el turno para el objeto de los parámetros de búsqueda. Contendrá el rango de fechas y el resto de parámetros.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
namespace MyApp\Value;

use MyApp\Value\DateRange;

/**
* Class documentation
*/
class FinderParams
{
/**
* @var DateRange Date Range to search
*/
private $dateRange;

/**
* @var string Post title
*/
private $title;

/**
* @var string Post author
*/
private $author;

/**
* @var integer Category identifier
*/
private $category_id;

/**
* Main constructor
*
* @param DateRange $dateRtange Date Range to search in
* @param string $title Post title
* @param string $author Post author
* @param integer $category_id Category identifier
*/
public function __construct(DateRange $dateRange, $title, $author, $category_id)
{
$this->dateRange = $dateRange;

$this->title = $title;
$this->author = $author;
$this->category_id = $category_id;
}

/**
* Magic method to be called before serialization
*
* @return array
*/
public function __sleep()
{
return array('dateRange', 'title', 'author', 'category_id');
}

}

La verdad que el código puede parecer más complicado, pero en realidad nos garantiza una capa de más de control en la aplicación, el ecosistema de clases se ha vuelto más rico, permitiendo una mejor documentación y testabilidad. Imaginemos que por algún motivo no se pueden realizar búsquedas anteriores al 2013 ya que implementamos una purga de la base de datos borrando la información anterior a X años, afectando a posts, clientes, productos, logs…. El punto habitual para implementar esta validación es en cada modelo que realiza una búsqueda por fechas, ya que suya es la responsabilidad de la lógica de negocio… pero aun así tendremos un punto más de intervención, común a todos los rangos de fechas.

Inmutabilidad

Como se puede ver los objetos solo disponen de getters, las variables son privadas y se alimentan únicamente desde el constructor. Así, una vez se les ha asignado valor no pueden ser modificadas. Por defecto el paso de objetos entre funciones y clases se realiza por referencia, es decir, siempre se pasa un “enlace” al objeto origen, un cambio en ese enlace se realiza sobre al objeto inicial y por lo tanto afecta los métodos posteriores que lo consuman. Con la inmutabilidad garantizamos que el valor del objeto será siempre el mismo, si es necesario modificar algún valor lo correcto seria devolver una nueva instancia con los valores modificados. Este comportamiento no es obligatorio, pero me parece útil y podría evitar errores futuros, la única función de estos objetos es el transporte de sus datos no su modificación. Pensemos por ejemplo que el objeto con el rango de fechas se asigna al template para ser representado en el calendario HTML, si tras ese paso, el valor de estas fechas es modificado en otro método, el HTML reflejará los cambios, ya que al ser renderizado (generalmente el último paso) se utilizará una referencia al objeto origen que ha sido modificado.

1
2
3
4
public function setDateEnd(\DateTime $dateEnd)
{
return new DateRange($this->dateInit, $dateEnd);
}

En el blog de qafoo anotan un método para refactorizar el código antiguo y reemplazarlo para usar estos objetos. De esta manera podemos cambiar las funciones antiguas encauzando el código a la nueva estructura, sin tener que duplicar la funcionalidad mientras vamos corrigiendo todos los puntos en que se llama a la función.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
namespace MyApp\Model;

use MyApp\Value\DateRange;
use MyApp\Value\FinderParams;

class blog
{

public function findPosts($dateInit, $dateEnd, $title, $author, $category_id, $limit, $page)
{
$params = new FinderParams(
new DateRange(
new \DateTime($dateInit),
new \DateTime($dateEnd)
),
$title,
$author,
$category_id
);

return $this->findPostsNew($params, $limit, $page);
}

public function findPostsNew(FinderParams $params, $limit, $page)
{
// Lógica aquí.
}

}

Es algo más de trabajo al definir y programar, pero finalmente, estructurar mejor nuestras aplicaciones nos hará ganar tiempo y mejorará su escalabilidad y facilidad de comprensión.