El patrón Service Layer

En muchos frameworks de software enterprise se favorece el diseño en torno a componentes o servicios. Este patrón de organización de código permite que aplicaciones grandes donde puede haber varios endpoints o puntos de comunicación en general que utilicen una misma funcionalidad, separen esas lógicas en clases directoras independientes, lo que facilita su uso cuando es necesario volver a aprovechar una función ya hecha sobre un endpoint nuevo.

El patrón Service layer, conocido en español como patrón de la capa de servicios es un patrón de diseño que determina la forma en la que se organiza el código fuente de una aplicación.

Un servicio (o service) va a consistir en un proveedor de lógica de negocio. Codifica cualquier reacción que quieras que ocurra en tu sistema de software cuando ocurre una acción.

Por ejemplo, si estás desarrollando una red social (justo lo que necesitábamos), tal vez quieras establecer un completo sistema de reglas que decida si enviar una notificación cuando se cree una publicación.

Si la publicación menciona a alguien, entonces debe enviar una notificación a cada una de las personas mencionadas, salvo que esas personas te hayan metido en su lista de cuentas ignoradas. Pero además, cualquier persona que tenga marcada la campana de notificaciones en tu cuenta también debe recibir una notificación, salvo que tu publicación contenga alguna de las palabras que tiene en su lista de palabras ignoradas.

En definitiva, este tipo de lógica contiene algoritmos donde suele haber muchos condicionales y ramificaciones, y a menudo codifican reglas muy concretas que sin embargo es vital que se cumplan.

En el caso de una empresa, y del software empresarial, como puede ser una tienda online o un sistema de información para los empleados, tal vez haya reglas como que una venta deba recibir un descuento adicional si su valor es superior a una cantidad de precio concreta, o que la venta deba ser confirmada manualmente si incluye un producto de una marca exclusiva. Esto es lo que típicamente asociamos con la definición de lógica de negocio, un flujo nada universal y unas decisiones que están puestas por personas concretas en la empresa y que se reflejan en el software porque así lo han pedido ellas.

Si por ejemplo, durante el desarrollo de una aplicación web, decides codificar todas estas reglas en el controlador que atiende las peticiones HTTP, está visto en base a la experiencia que al final esa lógica va a ser más frágil:

  • Por ejemplo, va a ser más dificil de probar, porque la única forma de automatizar algo que te diga si lo estás haciendo bien o mal, es disparando peticiones HTTP, por lo que necesitas un servidor de pruebas más complejo que si hicieses únicamente pruebas unitarias.

  • Pero además, en una circunstancia como esta, esa lógica estará tan acoplada al controlador concreto que estés usando, que ciertas operaciones como tener un segundo controlador que haga la misma lógica (por ejemplo, una versión gRPC de un controlador web que previamente habías programado), va a requerir copiar y pegar mucho código, haciendo que ahora aumente la deuda técnica de tu proyecto.

Un servicio, en este sentido, va a ser una clase independiente, que no depende de la capa de presentación (como puede ser el controlador de la aplicación web), donde se codifica la lógica de una manera que sea independiente y reusable.

Realmente, no tiene mucho misterio. Es mover el código de esa lógica desde el controlador a una clase separada, y luego importarla en tu controlador cuando la necesites utilizar. Esto resuelve ambos problemas.

Por ejemplo, muchas aplicaciones web empresariales utilizan repositorios para acceder a la información de una base de datos. Si haces aplicaciones con Spring o con frameworks MicroProfile como Quarkus, supongo que no tendré que hablarte mucho de los Repository. Son clases e interfaces que permiten unificar el acceso a datos.

Por ejemplo, para interactuar con la información de clientes, podrías tener una clase que lo único que haga es crear, obtener, modificar o borrar clientes de un sistema:

interface ClientRepository {
  void createClient(Client c);
  Optional<Client> getClientByEmail(String email);
  void updateClient(Client c);
  void deleteClient(Client c);
}

Esta clase es un servicio. Aporta una lógica de forma independiente y reusable, sin depender de la capa de presentación, por lo que puede ser usada y probada por separado al resto de la aplicación web. En concreto, esto es lo que típicamente llamaríamos CRUD. Aquí estoy ofreciendo un ejemplo en Java porque considero que es el que más fácil hace ver este ejemplo de servicio concreto, pero seguro que el ejemplo se puede adaptar a C#, Go o TypeScript de forma sencilla. Lo interesante, de hecho, es que al ser una interfaz, podrían existir múltiples implementaciones. Una implementación podría acceder a una base de datos mediante SQL, pero podrían haber implementaciones que utilicen un cliente HTTP para pedir o enviar los datos a una API externa.

Aquí la belleza es que estos servicios son reusables. Por ejemplo, mediante composición podríamos tener un servicio que ahora exponga una API de más alto nivel para interactuar con clientes, que por debajo emplee un ClientRepository para obtener los clientes como tal.

public class MarketingService {
  private ClientRepository clients;

  public MarketingService(ClientRepository clients) {
    this.clients = clients;
  }

  public void changeEmail(String oldEmail, String newEmail) {
    var client = this.clients.getClientByEmail(oldEmail);
    client.ifPresent((client) -> {
      client.setEmail(newEmail);
      this.clients.updateClient(client);
    });
  }
}

Como ves, los servicios deberían ser independientes y reusables, y es mejor que se delege en un servicio que ya existe una acción para la que ese servicio sea experta, en vez de crear copias del mismo código. Por eso, si un servicio tiene que realizar una operación para la que ya existe otro, posiblemente te interese simplemente traerte una instancia del mismo y delegar en él ese comportamiento. Patrones como el de inyección de dependencia o principios como el de inversión de dependencia aquí vendrán bien.

Y, finalmente, podrías instanciar o importar esta clase en tu controlador, para así que el controlador todo lo que haga sea extraer de los parámetros la información necesaria para que el servicio haga sus cosas, y luego convertir la respuesta del servicio en una respuesta de capa presentación, como puede ser una página HTML o un payload JSON para mandar desde una API.

Así que, en resumen, cuando usamos servicios, solucionamos los problemas que mencionaba anteriormente:

  • Si lo combinas con otros patrones, como el de inyección de dependencia, o con principios como los que nos aporta SOLID, vas a tener una forma más clara de probar tu código mediante pruebas unitarias, para asegurarte de que si deja de funcionar como cabe esperar tras hacer algunos cambios, te enteres sin mucha fricción.

  • Y, lógicamente, con esto se acaba el problema de reutilizar la lógica si necesitas un segundo controlador que ejecute el mismo código pero bajo otro paradigma, como una versión gRPC de lo que antes era un controlador REST.

La capa de servicios no es algo que te haga falta todo el tiempo. Es verdad que en proyectos grandes que van a evolucionar con el tiempo, resulta útil y conveniente para asegurarse el futuro de la aplicación, mediante un conjunto de clases que puedan evolucionar independientemente y sobre las que incluso podamos aplicar más principios SOLID. Sin embargo, en pequeños protitpos es posible que lo único que hagan es agregar una capa de indirección más sin ofrecer ventajas evidentes.

Lamento que la conclusión final sea tú verás si quieres usar esto o si te va a hacer más mal que bien, pero por otra parte esta es la realidad. El patrón service layer tiene que ver con la arquitectura de software con la que fabricas la aplicación. La arquitectura de un sistema de software nunca es algo categórico y siempre es necesario evaluar las necesidades y requisitos del proyecto antes de poder dar una respuesta definitiva. Deberás evaluar si el proyecto tiene suficiente lógica de negocio, o si hay que hacer demasiados flujos, como para que salga a cuenta tener clases de servicio mediante las cuales puedas aislar el código de una interacción antes que hacerlo directamente sobre el propio controlador.

Lista de reproducción
  1. 1
    El patrón Iterator
    8 minutos
  2. 2
    El patrón Service Layer
    9 minutos
  3. 3
    Inversión de Control e Inyección de dependencia no son lo mismo
    8 minutos