Desacoplando la lógica del negocio de los controladores

Quiero darle las gracias a Sergio F por sus acertadas recomendaciones. Código chido está enfocado en aprender y enseñar a implementar proyectos cada vez mejor diseñados y que se adapten a equipos de desarrollo actuales. Desacoplar la lógica de negocio del controlador es un paso hacia lo anterior.

Vamos a usar Oracle 11g para construir nuestra API. Si tienes dudas o hay pasos que no te quedan claros puedes revisar nuestro post anterior al mismo tiempo que sigues este tutorial ya que por cuestiones prácticas hay cosas que en el anterior mencionamos pero en este omitiremos.

¿Por qué desacoplar la lógica del negocio del controlador?

Imaginemos una torre médica de 10 pisos más una planta baja que funge como recepción únicamente, en la planta baja una persona sin formación médica canaliza a las personas según corresponda y les pide que se registren en una bitácora. En cada uno de los pisos superiores existen 5 consultorios y una secretaria o asistente que colabora con 5 médicos, el tener la lógica del negocio dentro del controlador es como si la persona encargada de la recepción cumpliera con las siguientes funciones:
- Agenda una cita
- Diagnostica nuestra enfermedad
- Elabora un tratamiento
- Cobra el monto de la consulta
- Da paletitas al final de la consulta
- Limpia el consultorio y apaga la luz cuando sale…

Parece algo extraño (al menos en nuestro universo) que la recepcionista haga todo eso, ¿o no? Pues dentro de nuestra API, los controladores son exactamente “recepcionistas” de nuestra torre médica y su única función debería de ser el redirigir las peticiones HTTP que recibe hacia otros métodos que contienen código capaz de ejecutar tareas complejas (lógica del negocio). Es por esta razón que contar con interfaces que contengan la lógica del negocio nos brinda un sistema, sí más complejo en cuanto a diseño y estructura, pero mucho más fácil de mantener.

Digamos que un controlador con la lógica acoplada tiene 150 lineas, y al desacoplar la lógica tenemos ahora no 1 archivo con 150 lineas sino 3 archivos con unas 100, 20, y 60 lineas , esto sólo para cumplir la misma funcionalidad. Lo anterior, si tomáramos como métrica de un “buen diseño” la cantidad de lineas de código y dijéramos que a menos lineas el sistema es “más eficiente” y a mayor número de lineas el sistema se vuelve “menos mantenible” estaríamos en un error; un buen diseño de un sistema generalmente sigue una regla bastante sencilla “mantenlo simple”.

Ahora cambiemos de universo, estamos en el universo todólogo lactia , en él absolutamente toooodas las torres médicas tienen recepcionistas y no necesitan médicos. En todólogo lactia los programadores son full stack desde que nacen hasta que mueren y no sólo eso, son analistas, diseñadores, ingenieros de prueba, capacitan al usuario, mantienen los sistemas y no se mueren de sueño o hambre … todo al mismo tiempo. Si los recepcionistas solicitan un sistema que les ayude con la administración integral de su negocio (quieren un ERP) y los programadores implementan un sistema bastante rápido pero bastante mal analizado y diseñado, constantemente tendrán que modificar los controladores y es muy probable que tengan conflictos porque dos de ellos modificaron la misma porción de código y porque están usando GIT o SubVersion (olvidé decirlo). Si tú fueras ese programador, cada conflicto que resolvieras al tratar de mergear tus commits sería un recordatorio de que pudiste evitarlo desde un inicio pero que por alguna razón (seguramente tiempo) no lo hiciste así.

Créenos cuando te decimos que desacoplar la lógica de los controladores es una buena idea. Es mucho más fácil encontrar un error en archivos con menos líneas de código que en un loop infinito de scroll, de igual forma modificar un proceso o implementar nueva funcionalidad resulta más ágil.

Requerimientos

Esta vez vamos a suponer que tenemos un conjunto de sistemas que comparten catálogos de países, estados, municipios, algunos referentes a la media filiación de personas,entre otros. Vamos a crear una API que contenga los catálogos generales de nuestra organización. Esta es (más o menos) la idea de los micro servicios, pero esto no es una clase de micro servicios, eso vendrá después, por lo pronto este ejemplo servirá.

Análisis y diseño

Nuestro diagrama de clases será el siguiente

Diagrama de clases

Tenemos una clase padre llamada Catalogo y de ella heredan todas las demás, también existe una relación de composición entre Municipio y Estado. Nuestro archivo de migraciones sería el siguiente:

databaseChangeLog:
  - changeSet:
      id: 0
      author: paklei
      changes:
        - createTable:
            tableName: bancos
            columns:
              - column:
                  name: id
                  type: int
                  constraints:
                    primaryKey: true
                    nullable: false
              - column:
                  name: nombre
                  type: varchar(100)
                  constraints:
                    nullable: false
              - column:
                  name: activo
                  type: boolean
        - createSequence:
            sequenceName: sec_id_bancos
            incrementBy: 1
            maxValue: 9999999999
            minValue: 1
            startValue: 1
            ordered: true
            cycle: false
            
        // createTable bocas
        // createSequence sec_id_bocas
            
        // createTable colores_ojo
        // createSequence sec_id_colores_ojo
            
        // createTable companias_telefono
        // createSequence sec_id_companias_telefono
            
        // createTable estados
        // createSequence sec_id_estados
            
        - createTable:
            tableName: municipios
            columns:
              - column:
                  name: id
                  type: int
                  constraints:
                    primaryKey: true
                    nullable: false
              - column:
                  name: nombre
                  type: varchar(100)
                  constraints:
                    nullable: false
              - column:
                  name: activo
                  type: boolean
                  constraints:
                    nullable: false
              - column:
                  name: estado_id
                  type: int
                  constraints:
                    nullable: false
                    foreignKeyName: fk_municipios_estado
                    references: estados(id)
        - createSequence:
            sequenceName: sec_id_municipios
            incrementBy: 1
            maxValue: 9999999999
            minValue: 1
            startValue: 1
            ordered: true
            cycle: false
            
        // createTable frentes
        // createSequence sec_id_frentes
            
        // createTable narices
        // createSequence sec_id_narices
            
        // createTable orejas
        // createSequence sec_id_orejas
            
        // createTable paises
       	// createSequence sec_id_paises
            
        // createTable colores_piel
        // createSequence sec_id_colores_piel
            
        // createTable tipos_cabello
        // createSequence sec_id_tipos_cabello
            
        // createTable tipos_telefono
        // createSequence sec_id_tipos_telefono              

En el diagrama de clases tenemos una composición que se traduce en una columna y una llave foránea en la tabla de municipios. Un Estado puede tener muchos Municipios y un Municipio pertenece a un Estado

Ahora … vamos a darle fuego a los municipios y estados!

Nuestros modelos

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@Table(name = "municipios")
public class Municipio extends Catalogo implements Serializable {

	@Id
	@GeneratedValue(strategy = GenerationType.SEQUENCE, generator="sec_id_municipios")
	@SequenceGenerator(name="sec_id_municipios", sequenceName = "sec_id_municipios", allocationSize=1)
	private Integer id;

	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "estado_id")
	@JsonIgnore
	private Estado estado;

	//getters y setters de id y estado
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@Table(name = "estados")
public class Estado extends Catalogo implements Serializable {

	@Id
	@GeneratedValue(strategy = GenerationType.SEQUENCE, generator="sec_id_estados")
	@SequenceGenerator(name="sec_id_estados", sequenceName = "sec_id_estados", allocationSize=1)
	private Integer id;

	@OneToMany(mappedBy = "estado", cascade = CascadeType.ALL, orphanRemoval = true)
	private List<Municipio> municipios;

	//getters y setters de id y municipios

Los Repositorios serían de la siguiente forma:

@Repository
public interface MunicipioRepositorio extends JpaRepository<Municipio, Integer>{
	List<Municipio> findByEstadoId(Integer estadoId);
}
@Repository
public interface EstadoRepositorio extends JpaRepository<Estado, Integer>{
}

Ahora necesito toda tu atención en lo que viene: el motivo de esta fiesta … el desacople!!

Vamos a crear tres paquetes: servicio,servicioImplementacion y controlador en el mismo nivel de nuestros paquetes modelo,exception y repositorio por lo que la estructura de nuestro proyecto sería como se muestra a continuación:

Estructura del proyecto

Hasta ahora no hay nada nuevo, todo es como los tutoriales anteriores, pero veamos el resultado del controlador de municipios:

@RestController
@RequestMapping("/api")
public class MunicipioControlador {
	@Autowired
	MunicipioServicio municipios;

	@GetMapping("/municipios")
	public List<Municipio> getMunicipios() {
	    return municipios.getMunicipios();
	}

	@GetMapping("/municipios/estado/{estadoId}")
	public List<Municipio> getMunicipiosByEstadoId(@PathVariable (value = "estadoId") Integer estadoId ) {
	    return municipios.getMunicipiosbyEstadoId(estadoId);
	}

	@PostMapping("/municipios/estado/{estadoId}")
	public Municipio guardarMunicipio(@PathVariable (value = "estadoId") Integer estadoId,
										 @Valid @RequestBody Municipio municipio) {
	    return municipios.guardarMunicipio(municipio, estadoId);
	}

	@GetMapping("/municipios/{id}")
	public Municipio getMunicipioById(@PathVariable(value = "id") Integer municipioId) {
	    return municipios.getMunicipioById(municipioId);
	}

	@PutMapping("/municipios/{municipioId}")
	public Municipio actualizarMunicipio(@PathVariable(value = "municipioId") Integer municipioId,
											@Valid @RequestBody Municipio nuevoMunicipio) {
	    return municipios.actualizarMunicipio(nuevoMunicipio, municipioId);
	}

	@DeleteMapping("/municipios/{id}")
	public ResponseEntity<?> borrarMunicipio(@PathVariable(value = "id") Integer municipioId) {
		municipios.borrarMunicipio(municipioId);
	    return ResponseEntity.ok().build();
	}
}

A diferencia de lo que ya sabemos hacer, ahora existe una interfaz que se llama MunicipioServicio que tiene los métodos para ejecutar tareas específicas de búsqueda, registro, modificación y borrado, mientras que nuestro controlador sólo tiene rutas mapeadas con las anotaciones @GetMapping,etc. y utiliza la interfaz de MunicipioServicio según se necesite en cada ruta. El código de nuestra interfaz es de la siguiente manera:

public interface MunicipioServicio {
	public Municipio guardarMunicipio(Municipio municipio,Integer estadoId);
	public List<Municipio> getMunicipios();
	public List<Municipio> getMunicipiosbyEstadoId(Integer estadoId);
	public Municipio getMunicipioById(Integer id);
	public Municipio actualizarMunicipio(Municipio nuevoMunicipio, Integer municipioId);
	public void borrarMunicipio(Integer id);
}

Finalmente, nuestra lógica de negocio está escrita en la implementación de nuestras interfaces de servicio. A grandes rasgos podemos decir que nuestro proyecto funciona de la siguiente manera:

1 - Un controlador recibe una petición HTTP
2 - Ese controlador conoce una interfaz y la utiliza para resolver la petición
3 - La interfaz se implementa en un archivo que utiliza un repositorio
4 - El repositorio utiliza un modelo (Entity) que se encarga de hacer la conversión objeto-tabla entre la API y la base de datos

La implementación de MunicipioServicio es como se muestra a continuación, mira cómo ahora la implementación de la interfaz es la que utiliza un repositorio y no el controlador directamente como lo haciamos antes.

@Service
public class MunicipioServicioImp implements MunicipioServicio {

	@Autowired
	private MunicipioRepositorio municipioRepositorio;

	@Autowired
	private EstadoRepositorio estadoRepositorio;

	@Override
	public Municipio guardarMunicipio(Municipio municipio,Integer estadoId) {
		estadoRepositorio.findById(estadoId).map(estado -> {
            municipio.setEstado(estado);
            return municipio;
        }).orElseThrow(() -> new ResourceNotFoundException("Estado ","id",estadoId));

		return municipioRepositorio.save(municipio);
	}

	@Override
	public List<Municipio> getMunicipios() {
		return municipioRepositorio.findAll();
	}

	@Override
	public List<Municipio> getMunicipiosbyEstadoId(Integer estadoId) {
		return municipioRepositorio.findByEstadoId(estadoId);
	}

	

	@Override
	public Municipio getMunicipioById(Integer id) {
		return municipioRepositorio.findById(id)
	            .orElseThrow(() -> new ResourceNotFoundException("Municipio", "id", id));
	}

	@Override
	public Municipio actualizarMunicipio(Municipio nuevoMunicipio, Integer municipioId) {
		return municipioRepositorio.findById(municipioId).map(municipio -> {
            municipio.setNombre(nuevoMunicipio.getNombre());
            municipio.setActivo(nuevoMunicipio.getActivo());
            return municipioRepositorio.save(municipio);
        }).orElseThrow(() -> new ResourceNotFoundException("Municipio ","id", municipioId));
	}

	@Override
	public void borrarMunicipio(Integer id) {
		Municipio municipio = municipioRepositorio.findById(id)
	            .orElseThrow(() -> new ResourceNotFoundException("Municipio", "id", id));
		
		municipioRepositorio.delete(municipio);

	}

}

Finalmente probamos nuestra creación con postman. Nosotros ejecutamos previamente algunos POST y al final un GET para obtener lo siguiente: GET

Puedes descargar el código completo de este ejemplo a través de este link

Les pedimos que compartan nuestro contenido, si tienen dudas o sugerencias de próximos tutoriales o artículos pueden dejarnos un comentario en la parte inferior.


comments powered by Disqus