Feeds:
Entradas
Comentarios

Archive for the ‘HATEOAS’ Category


Hola a todos, como os prometí seguimos avanzando con el tema de REST. En el anterior post vimos como definir operaciones de negocio siguiendo la filosofía REST y también tuvimos un pequeño guiño al mundo BPM. Ahora toca ver como conseguir sistemas autodescriptivos y automatizados al estilo REST.

Por dejar las cosas claras, cuando hablo de autodescripción y automatización de un sistema me refiero a lo siguiente:

  • Dado un único punto de entrada al sistema, conocido por el consumidor de servicios, dicho consumidor debe ser capaz de descubrir todos los servicios ofrecidos por el sistema remoto de forma automática.
  • Dado un servicio conocido por un consumidor, dicho consumidor debe ser capaz de obtener información sobre como se usa dicho servicio de forma automática.
  • Conseguir los dos puntos anteriores con el menor nivel de acoplamiento posible entre proveedor y consumidor de servicios. De esta forma ambas partes sólo se deben poner de acuerdo en un conjunto de protocolos y formatos mínimos.
  • Un consumidor dotado de las capacidades anteriores debería ser capaz de interoperar con varios sistemas que pertenezcan al mismo dominio de negocio, aunque dichos sistemas hayan sido implementados por organizaciones diferentes.

Como vemos, básicamente se trata de conseguir un protocolo mínimo que permita crear clientes genéricos que puedan consumir cualquier servicio REST de un sistema, o incluso de sistemas distintos, que sigan el mismo protocolo. Se pretende un nivel de interoperabilidad a nivel global dentro del mismo dominio de aplicación. En fin, los objetivos son bastante ambiciosos y no son nada sencillos. ¿Existe algún sistema que los haya conseguido? ¿Un sistema que haya conseguido aquello que SOAP y WS-* con todas sus toneladas de documentación y protocolos no ha conseguido? Obviamente sí, la web. Veamos en que consiste en realidad la web:

  • Un conjunto de páginas o documentos accesibles mediante protocolo HTTP. Cada una tiene su propia URI.
  • Cada página se representa en un formato, o tipo mime, estandarizado, que es procesable por cualquier aplicación cliente. Este formato es HTML.
  • Cada una estas páginas están interrelacionadas entre ellas mediante enlaces. Al acceder a una página, tenemos la capacidad de navegar por los enlaces para acceder a otras. Esta propiedad es la que da sentido al término «web».
  • Dichos enlaces están anotados para que sepamos que información nos van a proporcionar dicha página. De esta manera, no necesitamos navegar a ciegas, consultando las anotaciones de cada enlace podemos saber que tipo de información podemos esperar si navegamos a dicha página. En HTML las anotaciones son descripciones textuales de los enlaces, que se pueden poner dentro de éstos y/o como atributos.
  • Un buscador donde buscar las páginas web que nos interesen. O sea, el google, yahoo, etc.

Con estas premisas, puedes usar un único cliente, el browser o navegador web, para consumir todas las páginas de la web que te interesen. ¿A que te parecería extraño que tuvieras que usar un cliente diferente para acceder a las páginas de empresas distintas? A esto señores, se le llama interoperabilidad y el resto son tonterías. Obviamente se puede romper la interoperabilidad si una página usa algo no estándar, pues esa funcionalidad no estándar no se verá con todos los navegadores. De ahí la importancia de hacer páginas web siguiendo los estándares, y olvidarse de eso de «esta web sólo funciona con IE». Otro consumidor de páginas web, menos conocido, pero igual de importante, son los bots, arañas o crawlers, usados por los motores de búsqueda e indexadores de la web. Como se ve sólo necesitamos el concepto de enlaces y estandarización a nivel de formatos para alcanzar interoperabilidad.

¿Cómo debemos diseñar nuestros servicios REST para obtener todas éstas ventajas? Obviamente tenemos que tomar ejemplo de la web y diseñar nuestros servicios REST como si fueran páginas web. Ya hemos visto el patrón comando y el entidad/colección, pero eso no basta, debemos estructurar nuestros servicios como si fueran una web de servicios interrelacionados. Al igual que las páginas web, nuestros servicios deben mantener links entre ellos, links que estén anotados de alguna manera estandarizada y los interconecten para obtener una red semántica. Pero esto por si mismo no basta, ya que tenemos que usar tipos mime estandarizados que sean interpretables por el mayor número de clientes. En definitiva, si queremos obtener un nivel de autodescripción y automatización similar al de la web debemos seguir los siguientes principios de diseño:

  • No tener servicios aislados, sino interrelacionarlos mediante enlaces.
  • Proporcionar un servicio central, o home, que nos proporcione un catálogo con enlaces, debidamente anotados, a todos los servicios ofrecidos por el sistema. Alternativamente se puede proporcionar un buscador usando el patrón colección.
  • Soportar un conjunto de tipos mime estandarizados, de forma que los clientes puedan procesar las representaciones de los recursos REST de forma sencilla. Cuanto más estándar sea el tipo mime mejor. Si es totalmente estándar como por ejemplo, application/xml, application/json o application/atom+xml, obtendremos un grado de interoperabilidad máximo. Otra opción usar tipos mime especializados en nuestro dominio de aplicación, con lo que el grado de interoperabilidad es menor, o incluso bajar nuestros requisitos de interoperabilidad y usar tipos mime únicos de nuestro sistema. Lo importante es que todos los clientes que deban acceder a nuestro sistema deben saber interpretar el tipo mime. Otra ventaja de usar tipos mime estándar es que nos encontraremos con librerías que sepan interpretarlos, con lo que nuestros desarrollos se simplificarán enormemente.
  • Dentro del marco de los tipos mime a usar, ser capaz de definir anotaciones para los enlaces, que describan la acción o la información que se llevará a cabo si se usa el recurso enlazado.

Si nuestro sistema REST sigue éstas buenas prácticas, entonces podemos calificarlo como un sistema HATEOAS. Esto del HATEOAS es una sigla que se expande en la hermosa oración: Hypermedia As The Engine Of Application State. HATEOAS es el último «palabro» de moda, que está en boca de todos los gurús del REST para deslumbrar a las masas. Es una de las siglas más horrendas de la historia de la informática que alguien acuñó para referirse simplemente a un sistema REST que cumpla con todos los puntos expuestos anteriormente. Muchos gurus consideran que los verdaderos sistemas REST son aquellos que son HATEOAS, con lo que si no sigues los principios HATEOAS tu sistema es sólo pseudorest. Tal vez inventaron esta sigla para distinguir al verdadero REST del falso, pero yo no me meto en estas discusiones teológicas.

Como todo esto está quedando muy abstracto veámoslo con un ejemplo. Volvamos al caso de la tienda de libros, lo primero que debemos tener es un punto de entrada al sistema REST. A este punto de entrada le llamaremos servicio home. De esta manera nuestro cliente sólo necesita saber la URI de la home de nuestro sistema. ¿Qué información mantiene la home? Pues un conjunto de enlaces a los servicios que podemos usar en nuestro sistema. Veamos un ejemplo en JSON, de lo que devolvería el servidor si accedemos a la URI https://www.books.com/store/home:

[
  { 'rel':'bookServices', 'href':'/store/bookSrv', 'method':'GET' },
  { 'rel':'customerServices', 'href':'/store/customerSrv', 'method':'GET' },
  { 'rel':'orderServices', 'href':'/store/orders', 'method':'GET' }
]

Como vemos el documento devuelto contiene enlaces relativos, a los servicios REST que nos pueden servir para operar con libros, clientes, o pedidos. Nótese el uso del campo «rel» para anotar los enlaces. De esta forma un cliente automatizado que deseara acceder a los servicios de libros, buscaría en el recurso home el enlace con el campo rel con valor «bookServices». También es curioso el campo «method» que indica el método HTTP a usar para acceder a la URI. En este caso lo pongo por motivos didácticos, pero nos lo podemos ahorrar si el método a usar para realizar acción la acción es GET, que consideramos el método por defecto para navegar por un enlace. De esta manera el cliente REST no necesita saber las direcciones de todos los servicios expuestos en el servidor, ni que método HTTP usar en cada caso, sino sólo la URI del home, con el consiguiente desacoplamiento entre el cliente y el servidor. También debéis pensar que si cambiamos las URIs constructivas y entendibles que he puesto por otras URIs indescifrables, el sistema va a seguir funcionando. En este sentido, si la respuesta hubiera sido:

[
  { 'rel':'bookServices', 'href':'/store/wñu84v-4gsg', 'method':'GET' },
  { 'rel':'customerServices', 'href':'/store/uri-incognoscible', 'method':'GET' },
  { 'rel':'orderServices', 'href':'/store/ticket39caj2sv', 'method':'GET' }
]

Nuestro cliente no debería tener problemas para seguir funcionando. Estas URIs indescifrables se conocen como URIs opacas. Muchos gurus de REST mantienen un debate sobre si es mejor las URIs constructivas o las opacas. Los partidarios de las últimas sostienen que el uso de URIs opacas fuerza a los desarrolladores a diseñar sistemas HATEOAS. Yo prefiero las URIs constructivas, pero como hemos visto, si seguimos los preceptos de HATEOAS, ambas son intercambiables.

Volviendo al tema del acoplamiento, nos podemos fijar en las fuentes de acoplamiento que genera HATEOAS:

  • El formato a usar (en el ejemplo) es application/json. Ambas partes deben conocerlo. Sí, es un punto de acoplamiento, pero no conozco ningún sistema que pueda interoperar sin conocer el formato de los mensajes que intercambian. Obviamente como es REST podemos soportar varios formatos, tanto en el cliente como en el servidor, pero sólo es necesario que uno de dichos formatos sea común a ambos, con lo que la probabilidad de interoperar es mayor.
  • Que los enlaces están modelados con un subobjeto con tres campos: href, rel y method. El cliente y el servidor tienen que ponerse de acuerdo en cómo se usa cada campo y qué significa cada uno. Tal vez si hubiéramos usado otro tipo mime que soportara enlaces de forma nativa, esta información no sería necesaria.
  • Los valores de «rel» y lo que significan. Tenemos que tener un documento indicando que «bookServices» es el servicio de libros y «orderServices» para pedidos. Este nivel de acoplamiento puede ser similar al de conocer las URIs del servidor, pero nos da más flexibilidad. Son identificadores lógicos, parte del negocio y permiten desacoplarnos de las URIs usadas por el servidor, que pueden cambiar.

Como vemos el nivel de acoplamiento no es muy alto. ¿Qué ocurre si hacemos un GET sobre la URI https://www.books.com/store/bookSrv? De nuevo recibiremos un documento JSON:

{
  'home': {
    'description':'go to system home',
    'href':'/store/home'
  }, 'search': {
    'description':'search books',
    'href':'/store/books{?title,author,minPrice,maxPrice}'
  }, 'searchForm': {
    'description':'search books using a form',
    'href':'/store/books/searchForm'
  }, 'byId': {
    'description':'get book by id',
    'href':'/store/books/{bookId}'
  }, 'create': {
    'description':'create a new book',
    'href':'/store/books',
    'method':'POST'
  }, 'createForm': {
    'description':'create a new book using a form',
    'href':'/store/books/createForm'
  }, 'delete': {
    'description':'delete all books matching criteria',
    'href':'/store/books{?title,author,minPrice,maxPrice}',
    'method':'DELETE'
  }, 'search': {
    'description':'delete books using a form',
    'href':'/store/books/deleteForm'
  }
}

Mirad los enlaces, esta vez no uso los campos «rel», los he sustituido por el nombre que tienen dichos enlaces dentro del documento que los contiene. Esto lo puedo hacer porque estamos usando JSON, si fuera HTML tendría que conservar los campos «rel». Además he añadido un campo «description» a cada enlace, por si hay que generar una UI a un humano a partir de este documento. También he omitido el campo «method» en cuyo caso se toma como valor por defecto «GET». Además, si os fijáis, los nombre de los enlaces representan ahora operaciones, no recursos. Comentemos un poco más en detalle:

  • El enlace «search» no tiene una verdadera URI. Sino una URI Template. Las URI Template son un estándar de facto para definir URIs paramétricas y  nos indican como construir una URI en función de una serie de parámetros. En el caso que tenemos entre mano, nos indica como hacer búsquedas de libros y por que campos. En el ejemplo podemos usar los parámetros title, author, minPrice y maxPrice, que se adjuntan a la query string. Podéis encontrar más información sobre las URI Template aquí.
  • El enlace «byId» indica como acceder a una entidad. De nuevo usa la sintaxis URI Template.
  • Obviamente los enlaces «create» y «delete» para crear y borrar usando el patrón colección/entidad. Fijaros que en el caso de «delete» no se especifica ningún parámetro. Para crear un libro podemos usar el patrón colección/entidad y mandar en el cuerpo del POST el estado inicial del libro. En este caso el cliente y el servidor deben de estar acoplados en el sentido de que ambos deben estar de acuerdo sobre como se representa el estado de un libro mediante un documento JSON. Sin embargo veremos más adelante otra forma diferente de hacerlo.
  • La idea del enlace «searchForm» es dar una alternativa a la búsqueda mediante URI Template. En este caso usamos lo que yo llamo un patrón formulario. Consiste en que el servidor devuelve un formulario, el cliente rellena sus campos y lo devuelve a la URI apuntada por el enlace «submit». Los campos del formulario se pueden devolver en la query string, en caso de que el submit use GET o en el body.
  • De forma equivalente tenemos «createForm» y «deleteForm». Si accedemos a estos «formularios» vemos que estamos de nuevo ante un patrón formulario para crear y otro para borrar. Para ejecutar la operación tenemos que navegar por el enlace «submit». En el body de la petición hemos de mandar el formulario que hemos recibido pero con los campos rellenos. Veamos un ejemplo de lo que devuelve el servidor si hacemos un GET a la URI indicada por deleteForm:
    {
      'title': { // Definición campo de búsqueda
        'description':'title to match', // Descripción campo
        'value':'' // Valor inicial del campo
      }, 'minPrice': {
        'description':'minimum price',
        'value':0
      }, 'maxPrice': {
        'description':'maximum price',
        'value':0
      }, 'authors': {
        'description':'comma separated author list',
        'value':''
      }, 'execute': {
        'description':'set this field to true to execute operation or to false if you want to execute it later',
        'value':true
      }, 'submit': {
        /* Modo de empleo:
          * Usar este enlace para mandar el formulario
          * Simplemente devolver el formulario relleno a
          * la URI indicada usando el método PUT
          */
        'description':'create a new book',
        'href':'/store/books/deleteCommands/ticket-DFSG2QA3F9',
        'method':'PUT'
      }
    }

    Notad, que en este caso, el enlace «submit» apunta a un comando que usa el patrón ticket descrito en mi anterior artículo.

  • Finalmente fíjense en el enlace que nos devuelve de nuevo a la home. Como dije anteriormente los recursos deben estar interrelacionados. Tal vez el cliente llegó al sistema directamente por la URI del servicio de libros y no por la home.

Vemos que HATEOAS introduce otro pequeño grado de acoplamiento. O bien ambas partes saben manejar URI Templates, o bien ambas partes conocen el patrón «formulario». Sin embargo merece la pena, ya que tanto las URI Templates como el patrón formulario son mecanismos muy flexibles y genéricos y nos permiten autodescribir y automatizar multitud de operaciones. Normalmente se usan URI Templates para hacer consultas y operaciones sencillas, y formularios para operaciones «create» o el autodescubrimiento de comandos.

Continuemos con más ejemplos: ¿cómo sería una entidad que representara a un libro siguiendo el enfoque HATEOAS?

{
  'update': {
    'description':'update book data (collection/entity style)',
    'href':'/store/books/AS-d25RWV',
    'method':'PUT'
  }, 'delete': {
    'description':'delete book (collection/entity style)',
    'href':'/store/books/AS-d25RWV',
    'method':'DELETE'
  }, 'order': {
    'description':'order this book',
    'href':'/store/orders/ticket-sfsd35h-5qdt{?bookId=AS-d25RWV,amount,isGift,execute,customerId}',
    'method':'PUT'
  }, 'orderForm': {
    'description':'order this book using a form',
    'href':'/store/orderForm?ticket=ticket-sfsd35h-5qdt&bookId=AS-d25RWV',
    'method':'PUT'
  }
  /* Fields with book data not shown*/
}

Vemos que modelamos las operaciones «update» y «delete» mediante enlaces, ¿es esto necesario? Recordad que sabemos que update se hace con PUT sobre la URI de la entidad y delete se hace con DELETE sobre la URI de la entidad, sólo porque estamos aplicando el patrón colección/entidad. Tal vez nuestro cliente no haya sido diseñado para seguir ese patrón y sólo sepa de HATEOAS. De hecho el seguir el patrón colección/entidad es un punto de acoplamiento extra, ya que ambas partes deben conocerlo. Esto es innecesario si seguimos el enfoque HATEOAS. Además ya hemos visto antes, con el uso de formularios de búsqueda, creación y borrado, formas alternativas de operar, que no siguen el patrón colección/entidad.

También podemos observar el enlace «order» para dar de alta un comando de comprar el libro. Observemos como la URI Template está parcialmente rellena, indicando por nosotros el identificador del libro que queremos comprar. De nuevo usamos un patrón comando con tickets, aunque saltándonos el paso de creación del ticket con POST. Para completar tenemos un enlace alternativo a un formulario, llamado «orderForm» que nos permite comprar usando un formulario. Veamos este último:

{
  'bookId': {
    'description':'Book to order',
    'value':'AS-d25RWV'
  }, 'amount': {
    'description':'Number of books to order',
    'value':1
  }, 'isGift': {
    'description':'maximum price',
    'value':0
  }, 'customerId': {
    'description':'The id of the customer who orders the book',
    'value':'',
    'findCustomerId': {
        'description':'Search a customer id',
        'href':'/store/customers{?name,surname,dni}'
    }
  }, 'execute': {
    'description':'set this field to true to execute operation or to false if you want to execute it later',
    'value':true
  }, 'submit': {
    'description':'order the book',
    'href':'/store/orders/ticket-sfsd35h-5qdt',
    'method':'PUT'
  }
}

Si seguimos el enlace «submit» ejecutamos el formulario mediante un PUT. Si os dais cuenta la operación es exactamente la misma que si hubiéramos seguido el enlace «order» del libro, un PUT sobre la misma URI. Observar el detalle del enlace dentro de «customerId» para poder localizar el identificador de un cliente por nombre, apellidos y dni.

Otra variante, que yo no he seguido, sobre como interconectar los formularios y los comandos, es que el formulario haga un POST para crear un ticket y que este POST devuelva el ticket del nuevo comando. Posteriormente se trabaja siguiendo de forma clásica el patrón comando descrito en mi anterior artículo. Lo mismo vale con las URI Template y los comandos. Este diseño es más simple y sencillo de entender, pero es un poco más ineficaz, ya que necesita un POST extra, mientras que en el enfoque ilustrado en los ejemplos anteriores el POST no es necesario, ya que el ticket viene creado con el formulario o el enlace. Vosotros decidís.

Podéis ver que un cliente HATEOAS no necesita conocer como modelamos nuestro sistema mediante patrones comando, entidad/colección y tickets. Simplemente inspecciona los documentos devueltos por el servidor, sigue los enlaces que le interesa, rellena URI Templates o manda formularios. Si planteamos nuestro servidor con HATEOAS éste es explorable y usable por cualquier cliente HATEOAS, con una parametrización mínima.

Realmente todo esto del HATEOAS es un tema muy interesante y creo firmemente que es el único camino conocido para conseguir interoperabilidad a gran escala. También es muy productivo a la hora de desarrollar consumidores de servicios, ya que mediante un pequeño framework podemos atacar a una gran variedad de servidores REST. Ahora podéis entender porque no me interesa para nada ni el WSDL ni el UDDI. WSDL simplemente describe a nivel sintáctico los mensajes, define las URIs de cada servicio y cómo se intercambian los mensajes. ¿Con que fin? Con el de generar toneladas de código que supuestamente implementen todo ese complejo protocolo. UDDI es supuestamente un servicio de directorio donde encontrar los servicios SOAP, pero no he visto a nadie usarlo a gran escala. Con HATEOAS no necesitamos nada de eso, ya que lo tenemos todo unificado en un mismo enfoque. Sólo necesitamos ponernos de acuerdo en unos pocos formatos de datos, la URI de un servicio home que actúa como directorio, y compartir unos pocos patrones de diseño. El resto es seguir los enlaces… y navegar por una web de servicios REST. Al fin y al cabo, lo que queríamos desde un principio eran servicios web.

Desgraciadamente, una gran mayoría de arquitectos web, entre los que me incluyo, llevamos tanto tiempo pensando en tablas de bases de datos y en servicios RPC procedurales, que este estilo no está siendo muy difundido de momento. La mayoría de los sistemas REST que me encuentro no son HATEOAS, y ni siquiera implementan el patrón comando. La mayorías de los servicios REST que conozco usan de forma sencilla el patrón colección/entidad, pero poco más, y con poca interconexión entre recursos REST. Tal vez al diseñar dichos servicios los desarrolladores estuvieran pensando en tablas SQL y no en web. Tal vez estuvieran usando Grails… uy lo que he dicho, tranquilos, es broma };-)

ACTUALIZACIÓN: Un lector me comenta por el twitter la existencia de un framework REST basado en principios HATEOAS. Podéis echarle un vistazo aquí por si os interesa. A primera vista usa XML, con etiquetas link con atributo rel para autodescubrimiento de recursos. Sobre cada recursos puedes usar POST/PUT/DELETE/GET siguiendo el patrón colección/entidad. Al menos es lo que he entendido por la documentación. Fijaros como un enfoque muy sencillo puede llevar a un sistema autodescubrible con facilidad. Es interesante ver que no usa el patrón comando, ya que me comentan que no lo consideran REST puro. No estoy de acuerdo con esto último. Sin embargo, el que exista un patrón no significa que lo tengamos que usar forzosamente. Es mejor seguir el principio KISS, y por lo tanto si no le ven aplicación es bueno que no lo hayan usado.

Read Full Post »