-
Notifications
You must be signed in to change notification settings - Fork 14
Logica
- Introducción
- La capa de lógica en nuestros proyectos
- Inyección de Dependencias
- Validación de las reglas de negocio
- Pruebas en la lógica
En la arquitectura de capas que estamos utilizando en nuestros proyectos, la capa de lógica de la aplicación o negocio se encarga de coordinar los llamados del API REST y el acceso a la capa de persistencia. Es una capa intermedia entre la implementación de los recursos y la base de datos.
Una de sus responsabilidades es ocuparse de validar reglas de negocio o de invocar servicios de otras aplicaciones. Algunas reglas de negocío, podrían ser, por ejemplo:
- Que el
ISBN
de un libro exista en el servicio internacional de ISBN. - Que la fecha de publicación del libro sea mayor que la fecha de nacimiento del autor.
En el contexto de Spring, la lógica de la aplicación está implementada mediante beans, es decir, clases Java cuyo ciclo de vida es manejado por el contenedor de aplicaciones. En los proyectos del curso, las clases de la lógica estarán anotadas por @Service
y como convención de nombramiento tendrań el sufijo Service, por ejemplo CompanyService
.
La Figura 1 presenta los elementos de la arquitectura de las aplicaciones que estamos desarrollando. La capa de lógica contiene las clases anotadas por @Service
y son la fachada para el uso de los servicios que ofrece el Backend de la aplicación. En los proyectos que estamos desarrollando el cliente del Backend es un API REST (o las pruebas junit). Los objetos que maneja el Backend y que transfiere entre las capas son entidades de persistencia, es decir, clase Java anotadas con @Entity
.
El Backend no conoce DTOs ni Json ni ninguna otra representación de los recursos.
Figura 1 |
El diagrama de clases de la Figura 2 presenta para dos módulos su fachada AuthorService
y EditorialService
, y su relación con la capa de persistencia. El diagrama no lo muestra pero, cuando la lógica invoca métodos en la persistencia, lo que viajan son entidades. En este caso AuthorEntity
y EditorialEntity
.
Figura 2 |
Veamos ahora cómo la lógica se comunica con la persistencia a través de la inyección de dependencias.
La mayoría de las aplicaciones en Java utilizan recursos y servicios, tales como fuentes de datos o servicios web externos. El uso de estos recursos se convierte en algo simple en Spring mediante la inyección de dependencias.
La inyección de dependencias permite que un recurso A
declare una dependencia a un recurso B
y delega la resolución de la misma al contenedor. El contenedor se ocupa de resolver y entregar una instancia del recurso B
y se lo "inyecta" al recurso A
.
Veamos dos ejemplos: i) el controlador inyectando la lógica y ii) la lógica inyectando la persistencia.
La clase de la lógica de company
define una variable para acceder a la persistencia de tipo CompanyRepository
. Utilizamos la anotación @Autowired
para indicarle al contenedor que en ejecución "inyecte", es decir, haga que la variable apunte a un objeto de una clase que CompanyRepository
.
@Service
public class CompanyService {
@Autowired
private CompanyRepository repository;
...
}
En el contexto de nuestro ejemplo tenemos que el API Rest implementado en los controladores, para el ejemplo, CompanyController
, define una variable cuyo tipo es una interface de la lógica. Utilizamos la anotación @Autowired
para indicarle al contenedor que en ejecución "inyecte", es decir, haga que la variable apunte a un objeto de una clase que implementa esa interfaz.
@RestController
@RequestMapping("/authors")
public class CompanyController {
@Autowired
private CompanyService companyService;
...
}
La lógica de la aplicación es responsable de comunicar la capa de controladores (o recursos) con la persistencia. Esta capa manipula objetos Entity
. Esta capa es responsable de verificar las reglas de negocio y de comunicarse con la persistencia para recuperar o modificar los datos.
Para ilustrar verificaciones de reglas de negocio en nuestro ejemplo hemos definido las siguientes:
- No puede haber dos compañías con el mismo nombre
- No puede haber dos departamentos de la misma compañía con el mismo nombre
- Un empleado no puede tener un salario superior a 50 millones.
Vamos a explicar la primera regla de negocio. Esta se debe validar en dos ocasiones: cuando se crea una nueva compañía y cuando se modifica una compañía que ya existe. El siguiente fragmento de código presenta el método en la lógica para crear una compañía.
La lógica recibe el objeto CompanyEntity
que se quiere crear. Este es un objeto enviado por el API. Lo primero que debe hacer es llamar a la persistencia para que consulte en la base de datos si ya existe una compañía con el nombre de la que se quiere crear (método findByName
). Si ya existe, significa que la regla de negocio no se está validando y en ese caso se dispara una excepción. Si no existe se procede con la creación de la nueva compañía a través de la persistencia.
@Service
public class CompanyService {
@Autowired
private CompanyRepository repository;
...
@Transactional
public CompanyEntity createCompany(CompanyEntity entity) throws IllegalOperationException {
List<CompanyEntity> alreadyExist = repository.findByName(entity.getName());
if(!alreadyExist.isEmpty()) {
throw new IllegalOperationException("Ya existe una compañía con ese nombre");
} else {
return repository.save(entity);
}
}
...
}
Una vez implementados los métodos de la lógica el siguiente paso es desarrollar las pruebas unitarias de esos métodos. Para esto se deberá definir una clase de pruebas por cada una de las clases de servicios. La convención de nombramiento que usará para esas clases sera XServiceTest, donde X hace referencia al nombre de la entidad.
En el contexto del curso usaremos JUnit como el framework para la construcción de las pruebas. JUnit es un conjunto de clases que permite realizar la ejecución de clases Java de manera controlada, para poder evaluar si el funcionamiento de cada uno de los métodos de la clase se comporta como se espera.
Las clases en donde se implementan las pruebas deben estar anotadas así:
- @ExtendWith(SpringExtension.class). Indica que la clase de pruebas extiende las características de pruebas que ofrece Spring Framework.
- @DataJpaTest. Indica que en la prueba se involucra el acceso a datos con JPA. Esto se hace porque queremos persistir y recuperar datos que son almacendados en una o varias tablas de una base de datos.
- @Transactional. Indica que los métodos en la prueba serán transaccionales para garantizar la consistencia de los datos.
- @Import(BookService.class). Indica la clase del servicio que se usará en el test.
En cada clase inyectamos el servicio que se va a probar y un Entity Manager. En este caso, por ejemplo, probaremos los métodos de la clase BookService. La inyección de dependencia se hace mediante la anotación @Autowired
.
@Autowired
private BookService bookService;
@Autowired
private TestEntityManager entityManager;
La primera inyección se usa para tener acceso a los métodos del servicio mientras que la segunda define un EntityManager para las pruebas (acceso a métodos para persistir y recuperar datos de la persistencia sin pasar por el servicio). De este modo se aisla la prueba al no depender de otros servicios.
También se agrega una referencia a Podam. Esta es una librería que facilita la creación de instancias de objetos con datos ficticios. Así se evita el cablear los datos directamente en la prueba.
private PodamFactory factory = new PodamFactoryImpl();
Como las pruebas no se ejecutan en un orden específico, se debe establacer una configuración (o setup) para garantizar que todos los tests se ejecutan bajo las mismas condiciones.
Esto se hace en un método anotado con @BeforeEach; que para nuestro caso es el método setUp. La anotación implica que ese método se ejecuta antes de cada uno de los tests. En este método se hace el llamado a los métodos clearData e insertData. El primero borra los datos de las tablas implicadas en las pruebas y el segundo se encarga de crear datos y persistirlos con ayuda del EntityManager. Estos datos se usarán, por ejemplo en las pruebas donde se quiere consultar el listado de recursos. Usualmente, almacenamos esos datos en una lista.
@BeforeEach
void setUp() {
clearData();
insertData();
}
Luego definiremos un método de prueba por cada uno de los métodos de la lógica. También se debe agregar un método de prueba cuando queremos probar una regla de negocio. Por ejemplo, si definimos una regla que indica que no puede haber dos compañías con el mismo nombre, iniciaremos con un test en el que se cree una compañía correctamente, y otro, en el que se cree una compañía con un nombre que ya exista.
Esta es la prueba para el primer escenario:
@Test
void testCreateCompany() {
CompanyEntity newEntity = factory.manufacturePojo(CompanyEntity.class);
CompanyEntity result = companyService.createCompany(newEntity);
assertNotNull(result);
CompanyEntity entity = entityManager.find(CompanyEntity.class, result.getId());
assertEquals(newEntity.getId(), entity.getId());
assertEquals(newEntity.getName(), entity.getName());
assertEquals(newEntity.getDescription(), entity.getDescription());
}
En esta prueba se inicia creando un nueva instancia de CompanyEntity con datos aleatorios generados por Podam. Con ayuda del método createCompany del servicio se persiste la compañía. Se espera que el retorno del método no sea nulo.
Con ayuda del entity manager se busca en la base de datos la compañía con el nuevo id y se validan que todos los atributos de la compañía almacenada correspondan con los datos del objeto newEntity.
Esta es la prueba para el segundo escenario:
@Test
void testCreateInvalidCompany() {
assertThrows(IllegalOperationException.class, () -> {
CompanyEntity newEntity = factory.manufacturePojo(CompanyEntity.class);
newEntity.setName(companyList.get(0).getName());
companyService.createCompany(newEntity);
});
}
En esta prueba también se inicia creando un nueva instancia de CompanyEntity con datos aleatorios generados por Podam. Ahora vamos a setear un nombre que ya está previamente almacenado en el primer item de la lista de prueba. Al guardar los datos haciendo uso del servicio esperamos que se lance una excepción de negocio: assertThrows(IllegalOperationException.class, () -> {});
. Note que en este caso la sintaxis de esta esta instrucción usa una expresión Lambda.
Este libro fue creado para el curso ISIS2603 Desarrollo de Software en Equipos en la Universidad de los Andes. Desarrollado por Rubby Casallas con la colaboración de César Forero, Kelly Garcés, Jaime Chavarriaga y José Bocanegra. Universidad de los Andes, Bogotá, Colombia. 2021.
Esta wiki fue creada para el curso ISIS2603 Desarrollo de Software en Equipos en la Universidad de los Andes. Desarrollado por Rubby Casallas con la colaboración de César Forero, Kelly Garcés, Jaime Chavarriaga y José Bocanegra. Universidad de los Andes, Bogotá, Colombia. 2021.
- Instalación del ambiente en máquina propia
- Configuración de la máquina virtual
- Ejecución del back
- Uso de Codespaces
- Clases
- Herencia
- Asociaciones
- Tipos de asociaciones
- Caso de estudio: la biblioteca
- Caso de estudio: la empresa
- Java Persistence API (JPA)
- Implementación paso a paso persistencia
- Ejemplo implementación persistencia
- Carga de datos en el Backend
- Relaciones compartidas (Shared) OneToOne
- Relaciones compartidas (Shared) OneToMany/ManyToOne
- Relaciones compuestas (Composite) OneToMany/ManyToOne
- Conceptos básicos de REST
- Diseño API REST
- Tutorial documentación diseño API
- Implementación API REST con Spring
- Tutorial implementación API