Esto es una traducción libre de Refactoring the Cat API client – Part III
En la primera y segunda parte hemos estado trabajando en separar las preocupaciones que teníamos al principio combinadas en una sola función.
Los principales «personajes» en el escenario ya han sido identificados: un httpClient y una caché, utilizadas por diferentes implentaciones de CatApi para poder testar y realizar un cliente para The Cat Api
Representando datos
Hemos estado mirando el comportamiento y la estructura general del código. Pero todavía no hemos mirado que pasa alrededor. Actualmente todo es una cadena, el valor que devuelve CatApi::getRandomImage()
. Al llamar al método estamos «garantizando» que vamos a recuperar una cadena. Aunque en el caso de RealCatApi::getRandomImage()
podemos estar seguros de que es una cadena, porque ponemos específicamente el valor de retorno a una cadena, aunque bien es cierto que podríamos no ser útiles para la persona que llama a esta función. Ya que podríamos devolver una cadena vacía, o incluso algo como «No soy una URL».
class RealCatApi implements CatAPi
{
...
/**
* @return string URL of a random image
*/
public function getRandomImage()
{
try {
$responseXml = ...;
} catch (HttpRequestFailed $exception) {
...
}
$responseElement = new \SimpleXMLElement($responseXml);
return (string) $responseElement->data->images[0]->image->url;
}
}
Para hacer nuestro código más robusto y digno de confianza, nosotros podemos hacer un trabajo un poco mejor, asegurando que estamos devolviendo un valor adecuado.
Una cosa que podríamos hacer es verificar las post-condiciones de nuestro método.
$responseElement = new \SimpleXMLElement($responseXml);
$url = (string) $responseElement->data->images[0]->image->url;
if (filter_var($url, FILTER_VALIDATE_URL) === false) {
throw new \RuntimeException('The Cat Api did not return a valid URL');
}
return $url;
Aunque esto es correcto, sería bastante malo para facilitar la lectura. Sería empeorar si hay varias funciones y todas necesitan la misma validación. Tendríamos que tener una manera de reutilizar esta lógica de validación. En cualquier caso, el valor devuelto es todavía una cadena sin sentido. Sería bueno asegurar y dar a conocer que es una URL. De esta manera, cualquier otra parte de la aplicación que utilice el valor devuelto por CatApi::getRandomImage()
sería consciente de que es una URL y no otra cosa.
Un objeto (Value Object) que representa a una URL
En lugar de escribir post-condiciones para las implementaciones de CatApi::getRandomImage()
podríamos escribir precondiciones para las direcciones URL de imagen. ¿Como podemos asegurarnos de que un valor como una dirección de la imagen no puede existir, excepto cuando es valido? Por ejemplo convirtiendolo en un objeto y evitando construir este objeto con algo que no es una URL válida:
class Url
{
private $url;
public function __construct($url)
{
if (!is_string($url)) {
throw new \InvalidArgumentException('URL was expected to be a string');
}
if (filter_var($url, FILTER_VALIDATE_URL) === false) {
throw new \RuntimeException('The provided URL is invalid');
}
$this->url = $url;
}
}
Este tipo de objetos se llaman value object.
Crear URL con datos inválidos es imposible
new Url('I am not a valid URL');
// RuntimeException: "The provided URL is invalid"
Así que cada instancia de Url
que encontremos esta verificada como una URL válida. Ahora nosotros podemos cambiar el código de RealCatApi::getRandomImage()
para que Url
$responseElement = new \SimpleXMLElement($responseXml);
$url = (string) $responseElement->data->images[0]->image->url;
return new Url($url);
Por lo general, los value object tienen métodos para crearlos desde tipos primitivos y para convertirlos de nuevo a tipos primitivos. Con el fin de prepararlos o utilizarlos en persistencia. En este caso podrías terminar con un método fromString()
para crear y un método __toString()
. Esto allana el camino para métodos «paralelos de construcción» (por ejemplo fromParts($scheme, $host, $path, ...)
) y getters (host(), isSecure(), etc.
) Por supuesto no debemos implementar estos métodos hasta que realmente los necesitemos.
class Url
{
private $url;
private function __construct($url)
{
$this->url = $url;
}
public static function fromString($url)
{
if (!is_string($url)) {
...
}
...
return new self($url);
}
public function __toString()
{
return $this->url;
}
}
Por ultimo, necesitamos modificar el contrato de getRandomImage()
y estar seguro de que la imagen por defecto que devuelve es un objecto Url
:
class RealCatApi implements CatAPi
{
...
/**
* @return Url URL of a random image
*/
public function getRandomImage()
{
try {
$responseXml = ...;
} catch (HttpRequestFailed $exception) {
return Url::fromString('http://cdn.my-cool-website.com/default.jpg');
}
$responseElement = new \SimpleXMLElement($responseXml);
return Url::fromString((string) $responseElement->data->images[0]->image->url);
}
}
Por supuesto, este cambio también debe reflejarse en la interfaz Cache
y en cualquier clase de la aplicación, como FileCache
donde se debe convertir desde y hacia objetos URL:
class FileCache implements Cache
{
...
public function put(Url $url)
{
file_put_contents($this->cacheFilePath, (string) $url);
}
public function get()
{
return Url::fromString(file_get_contents($this->cacheFilePath));
}
}
Parseando la respuesta XML
Una última parte que me gustaría mejorar, es la siguiente:
$responseElement = new \SimpleXMLElement($responseXml);
$url = (string) $responseElement->data->images[0]->image->url;
A mi personalmente no me gusta SimpleXML, pero esto no es un problema. Lo que es realmente malo es el hecho de que asumimos que la respuesta es un XML válido, que contiene un elemento raíz, el cual contiene un elemento <data>
, que contiene un elemento <image>
, que contiene un elemento <url>
, cuyo valor supone que es una cadena (la URL de la imagen). En cualquier punto de esta cadena podría haber un error, que provocase un error PHP.
Queremos controlar esta situación y en vez de hacer que PHP devuelva errores por nosotros, definimos nuestras propias excepciones que podemos capturar si es necesario. Una vez más, debemos ocultar todos referente a los elemento y la jerarquía dentro de un objeto XML. El primer paso es introducir un simple DTO(objeto de transferencia de datos)q que representa una ‘imagen’ de respuesta de Cat API.
class Image
{
private $url;
public function __construct($url)
{
$this->url = $url;
}
/**
* @return string
*/
public function url()
{
return $this->url;
}
}
Como hemos visto, este DTO oculta el hecho de tenemos elementos <data>
,<images>
,<image>
, etc. en la respuesta original. Nosotros solo estamos interesados en la URL y en tener simples getter (url()
)
Ahora el código de getRandomImage()
sería:
$responseElement = new \SimpleXMLElement($responseXml);
$image = new Image((string) $responseElement->data->images[0]->image->url);
$url = $image->url();
Esto todavía no ayuda demasiado, ya que todavía estamos atascados en el recorrido del XML.
En lugar de crear el objeto DTO en linea, podríamos delegarlo en una «factorúa» que será el encargado de tener el conocimiento acerca de la jerarquía XML.
class ImageFromXmlResponseFactory
{
public function fromResponse($response)
{
$responseElement = new \SimpleXMLElement($response);
$url = (string) $responseElement->data->images[0]->image->url;
return new Image($url);
}
}
Tan solo hay que asegurarse de inyectar una instancia de ImageFromXmlResponseFactory
dentro de RealCatApi
y entonces reducir el código de `RealCatApi::getRandomImage():
$image = $this->imageFactory->fromResponse($responseXml);
$url = $image->url();
return Url::fromString($url);
Mover el código alrededor de esto nos da ula oportunidad de hacer algo mejor. Clases pequeñas y fáciles de probar. COmo mencionamos antes, las pruebas que hemos hecho hasta ahora, en general, solo tienen en cuenta el «camino feliz». Los casos extremos no estan cubiertos. Por ejemplo:
- El XML de respuesta viene vacío.
- El XML no es válido.
- El XML es válido, pero tiene una esctructura inesperada.
- …
Trasladar la logica de procesamiento XML a una clase diferente nos permite centrarnos por completo en el comportamiento que rodea a este tema en particular. Incluso se permite el uso de un cierto estilo de TDD, donde se definen situaciones (como las anteriores por ejemplo) y los resultados esperados.
Conclusión
Con esto se concluye la serie «Refactorizando el cliente de «Cat API»». Espero que les haya gustado.
Mi conclusión
Esta serie de post sobre como refactorizar una API ha sido interesante, ya que nos acerca al SOLID y sobre todo a la S (Single Responsability Principle) de una forma práctica. Hemos partido de un cliente muy simple y muy propenso a quebraderos de cabeza. Paso a paso, hemos ido dividiendo responsabilidades hasta llegar a una jerarquía de clases que nos permiten tener responsabilidades definidas, hacer test pequeños y clases entendibles. Espero que os haya gustado, si tenéis algún comentario o sugerencia podéis dejar un comentario