Esta sección expone recomendaciones para organizar un proyecto de software. Como referencia se utilizará la tecnología java con el framework springboot.
Como práctica recomendada en la industria del desarrollo de software, se debe utilizar una estructura de carpetas bien organizada para mejorar la capacidad de mantenimiento del código, la colaboración entre los miembros del equipo y el proceso de desarrollo general.
A continuación se detallan algunas razones clave para organizar la estructura de carpetas de un protecto Spring Boot:
Una estructura de carpetas bien definida proporciona un diseño claro de dónde encontrar los diferentes componentes de su aplicación. Facilita a los desarrolladores la localización de archivos, paquetes y recursos específicos, lo que reduce el tiempo dedicado a buscar código.
Spring Boot sigue un enfoque modular en el que la aplicación se divide en diferentes componentes lógicos como controladores, servicios, repositorios, configuraciones, etc. Tener una estructura de carpetas adecuada le permite organizar estos componentes de manera efectiva, lo que hace que la aplicación sea más fácil de entender y mantener.
La estructura de carpetas impone una separación de responsabilidades, lo que permite a los desarrolladores centrarse en partes específicas de la aplicación sin preocuparse por código no relacionado. Por ejemplo, la lógica empresarial reside en la capa de servicio, el código de acceso a datos en la capa de repositorio y el manejo de solicitudes web en la capa de controlador.
A medida que la aplicación crece, una estructura de carpetas bien organizada facilita la incorporación de nuevas características y funcionalidades sin convertir el proyecto en un desastre inmanejable. Facilita la adición de nuevos módulos o componentes manteniendo una arquitectura general coherente.
Una organización adecuada fomenta la creación de componentes reutilizables. Cuando los desarrolladores pueden encontrar y comprender fácilmente el código existente, es más probable que lo reutilicen en lugar de duplicar esfuerzos.
Cuando varios desarrolladores trabajan en un proyecto, seguir una estructura de carpetas estandarizada garantiza la coherencia y facilita la colaboración. Todos los miembros del equipo saben dónde colocar el código nuevo y pueden localizar rápidamente el código agregado por otros. Compilación e implementación: herramientas como sistemas de compilación, integración continua y scripts de implementación pueden aprovechar una estructura de carpetas predecible para automatizar los procesos de compilación e implementación de manera eficiente.
Una estructura de carpetas organizada simplifica la escritura de pruebas y la depuración de la aplicación. Las clases y recursos de prueba se pueden colocar junto a las clases correspondientes, lo que facilita la comprensión de la cobertura del conjunto de pruebas.
Cuando nuevos desarrolladores se unen al equipo, una estructura de carpetas bien definida les ayuda a ponerse al día rápidamente. Pueden comprender rápidamente la arquitectura y localizar secciones de código relevantes. Base de código mantenible: una base de código limpia y bien estructurada es más fácil de mantener a largo plazo. Reduce la deuda técnica y hace que sea menos probable que se introduzcan errores o regresiones al realizar cambios.
A continuación se comparte el siguiente gráfico de referencia que proponen una estructura de organización de un proyecto springboot.
Contiene clases de configuración, donde configura los ajustes de la aplicación o otras configuraciones a nivel de aplicación.
/** * Copyright © 2023 ADA Corporation. All rights reserved. * * This component is protected by copyright. * * Use of this component is subject to the terms of the license agreement. */ package com.ada.viatico.proceso.config; import java.util.List; import java.util.TimeZone; import javax.annotation.PostConstruct; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Contact; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.License; import io.swagger.v3.oas.models.servers.Server; @Configuration public class ViaticosConfig { /** The log. */ private static Logger log = LoggerFactory.getLogger(ViaticosConfig.class); /** The url. */ @Value("${openapi.url}") private String url; /** The application title. */ @Value("${application.title}") private String applicationTitle; /** The application summary. */ @Value("${application.version}") private String applicationVersion; @PostConstruct public void init() { log.info("Establicendo Zona Horaria..."); TimeZone.setDefault(TimeZone.getTimeZone("America/Bogota")); log.info("Zona Horaria establecida a {}", TimeZone.getDefault().getID()); } /** * Property sources placeholder configurer. * * @return the property sources placeholder configurer */ @Bean public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { return new PropertySourcesPlaceholderConfigurer(); } /** * My open API. * * @return the open API */ @Bean public OpenAPI myOpenAPI() { Server server = new Server(); server.setUrl(url); server.setDescription("Server URL environment"); Contact contact = new Contact(); contact.setEmail("carlos.torres@ada.co; daniel.lopez@ada.co"); contact.setName("Carlos Torres | Daniel López"); contact.setUrl("www.ada.co"); License mitLicense = new License().name("ADA License").url("www.ada.co/licenses/"); Info info = new Info() .title(applicationTitle) .version("v" + applicationVersion) .summary("Microservicio para gestionar Viáticos.") .description("Servicio que realiza las operaciones de creación/aprobación de Solicitudes y Legalización de Viáticos.") .contact(contact) .termsOfService("https://www.ada.co/terms") .license(mitLicense); return new OpenAPI().info(info).servers(List.of(server)); } }
Contiene sus clases de controlador RESTful. Estas clases manejan solicitudes HTTP entrantes y definen los puntos finales de la API.
/** * Copyright © 2023 ADA Corporation. All rights reserved. * * This component is protected by copyright. * * Use of this component is subject to the terms of the license agreement. */ package com.ada.viatico.proceso.controller.v2; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import com.ada.viatico.proceso.dto.DatosGenerarAprobacion; import com.ada.viatico.proceso.dto.GenericResponseDto; import com.ada.viatico.proceso.repository.IMestroCompromisoRepo; import com.ada.viatico.proceso.service.Impl.TicketsImp; import com.ada.viatico.proceso.service.Impl.ViaticoSolicitudImpl; import com.ada.viatico.proceso.service.v2.ViaticosServiceV2; import com.ada.viatico.proceso.sfcomercial.dto.AprobacionLegalizacionDto; import com.ada.viatico.proceso.utiliti.AdelantoSolicitud; import com.ada.viatico.proceso.utiliti.GuardarLegalizacion; import com.ada.viatico.proceso.utiliti.ProcesoGuardar; import com.ada.viatico.proceso.utiliti.ProcesoViaticoAprobacion; import com.ada.viatico.proceso.utiliti.ViaticosException; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; /** * The Class ViaticosController. * @version 2.0 * @author carlos.torres@ada.co */ @CrossOrigin(origins = "*", methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.DELETE}) @RestController @RequestMapping("v2") public class ViaticosControllerV2 { /** The log. */ private Logger log = LoggerFactory.getLogger(ViaticosControllerV2.class); /** The ticketsimpl. */ @Autowired TicketsImp ticketsimpl; /** The guardar. */ @Autowired ProcesoGuardar guardar; /** The aprobacion. */ @Autowired ProcesoViaticoAprobacion aprobacion; /** The solicitud repo. */ @Autowired ViaticoSolicitudImpl solicitudRepo; /** The savelega. */ @Autowired GuardarLegalizacion savelega; /** The repo compromiso. */ @Autowired IMestroCompromisoRepo repoCompromiso; /** The solicitud adelanto. */ @Autowired AdelantoSolicitud solicitudAdelanto; /** The viaticos service. */ @Autowired ViaticosServiceV2 viaticosServiceV2; /** * Aprobar legalizacion. * * @param aprobacionLegalizacionDto the aprobacion legalizacion dto * @return the response entity * @throws ViaticosException the viaticos exception */ @Operation(summary = "Aprobar Legalización.", description = "Aprueba la Legalización de un Viático.") @ApiResponse(responseCode = "201", content = { @Content(schema = @Schema(implementation = GenericResponseDto.class)) }) @ApiResponse(responseCode = "500", content = { @Content(schema = @Schema(implementation = ViaticosException.class)) }) @Parameter(name = "aprobacionLegalizacionDto", description = "Componente que contiene los parametros necesarios para el proceso de legalización.", in = ParameterIn.QUERY, required = true) @PostMapping("/aprobarlegalizacion") public ResponseEntity<GenericResponseDto> aprobarLegalizacion(@RequestBody AprobacionLegalizacionDto aprobacionLegalizacionDto) throws ViaticosException { var result = viaticosServiceV2.aprobarLegalizacion(aprobacionLegalizacionDto); if(result.getCode() == -1L) { log.info("approve legalization executed with errors."); return new ResponseEntity<>(result, HttpStatus.INTERNAL_SERVER_ERROR); } log.info("approve legalization executed correctly."); return new ResponseEntity<>(result, HttpStatus.CREATED); } /** * Guardar viatico. * * @param datos the datos * @return the response entity * @throws ViaticosException the viaticos exception */ @Operation(summary = "Aprobar Solicitud.", description = "Aprueba la solicitud de un Viático.") @ApiResponse(responseCode = "201", content = { @Content(schema = @Schema(implementation = GenericResponseDto.class)) }) @ApiResponse(responseCode = "500", content = { @Content(schema = @Schema(implementation = ViaticosException.class)) }) @Parameter(name = "datos", description = "Componente que contiene los parametros necesarios para el proceso de solicitud.", in = ParameterIn.QUERY, required = true) @PostMapping("/generaraprobacion") public ResponseEntity<GenericResponseDto> guardarViatico(@RequestBody DatosGenerarAprobacion datos) throws ViaticosException { var result = viaticosServiceV2.aprobarSolicitud(datos); if(result.getCode() == -1L) return new ResponseEntity<>(result, HttpStatus.SERVICE_UNAVAILABLE); return new ResponseEntity<>(result, HttpStatus.CREATED); } }
Contiene sus clases para permitir conexiones multicliente y modelo multiempresa.
Un DTO es un patrón de diseño utilizado para transferir datos entre diferentes capas o componentes de una aplicación. El objetivo principal de un DTO es encapsular datos y proporcionar una estructura de datos simple que pueda transmitirse fácilmente por la aplicación. Los DTO se utilizan a menudo para transferir datos entre el front-end y el back-end de una aplicación web, entre microservicios o entre diferentes capas de una aplicación, como la capa de servicio y la capa de presentación.
/** * Copyright © 2023 ADA Corporation. All rights reserved. * * This component is protected by copyright. * * Use of this component is subject to the terms of the license agreement. */ package com.ada.viatico.proceso.sfcomercial.dto; import java.io.Serializable; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; /** * Instantiates a new afectacion presupuestal dto. */ @Schema(description = "Dto que contiene la configuración de afectación presupuestal de un rubro de una disponibilidad..") @Data public class AfectacionPresupuestalDto implements Serializable { /** The Constant serialVersionUID. */ private static final long serialVersionUID = -7167570415608375973L; /** The cod disponibilidad. */ @Schema(description = "Código de la disponibilidad.") Long codDisponibilidad; /** The codigo rubro. */ @Schema(description = "Código del rubro.") Long codigoRubro; /** The valor. */ @Schema(description = "Valor de afectación del rubro prespuestal.") Long valor; /** The cod C costos. */ @Schema(description = "Código del centro de costos.") Long codCCostos; }
La carpeta del modelo almacena modelos de datos o entidades que representan la estructura y el comportamiento del dominio de la aplicación. Estas clases se asignan a tablas, vistas o consultas de bases de datos y definen las propiedades y relaciones de los datos de la aplicación.
/** * Copyright © 2023 ADA Corporation. All rights reserved. * * This component is protected by copyright. * * Use of this component is subject to the terms of the license agreement. */ package com.ada.viatico.proceso.entity; import java.util.Date; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; import lombok.Data; /** * Instantiates a new comprobante de egreso. */ @Data @Entity @Table(schema = "TESORE01",name = "COMPROBANTE_DE_EGRESO") public class ComprobanteDeEgreso { /** The cod inter comprobante egreso. */ @Id @Column(name="COD_INTER_COMPROBANTE_EGRESO") private Long codInterComprobanteEgreso; /** The consecutivo. */ @Column(name="CONSECUTIVO") private Long consecutivo; /** The tipo. */ @Column(name="TIPO") private String tipo; /** The estado. */ @Column(name="ESTADO") private String estado; /** The fecha elaboracion. */ @Column(name="FECHA_ELABORACION") private Date fechaElaboracion; /** The fecha impresion. */ @Column(name="FECHA_IMPRESION") private Date fechaImpresion; }
Contiene clases de repositorio que se ocupan del acceso a datos. Estas clases suelen utilizar un marco ORM (Object-Relational Mapping) o JPA (Java Persistence API) para interactuar con la base de datos.
/** * Copyright © 2023 ADA Corporation. All rights reserved. * * This component is protected by copyright. * * Use of this component is subject to the terms of the license agreement. */ package co.ada.sicof.mobile.repository; import co.ada.core.db.config.repository.JpaAdaRepository; import co.ada.sicof.mobile.entity.Empresa; /** * The Interface EmpresaRepository. */ public interface EmpresaRepository extends JpaAdaRepository<Empresa, Long> { /** * Find by codigo empresa. * * @param codigoEmpresa the codigo empresa * @return the empresa */ public Empresa findByCodigoEmpresa(Long codigoEmpresa); /** * Find by codigo mempresa. * * @param codigoMempresa the codigo mempresa * @return the empresa */ public Empresa findByCodigoMempresa(String codigoMempresa); }
Contiene clases de servicio que implementan la lógica empresarial. Los controladores utilizan estos servicios para realizar operaciones con datos.
/** * Copyright © 2023 ADA Corporation. All rights reserved. * * This component is protected by copyright. * * Use of this component is subject to the terms of the license agreement. */ package com.ada.viatico.proceso.service.v2; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.ada.viatico.proceso.component.LegalizacionComponentV2; import com.ada.viatico.proceso.component.SolicitudComponentV2; import com.ada.viatico.proceso.dto.DatosGenerarAprobacion; import com.ada.viatico.proceso.dto.GenericResponseDto; import com.ada.viatico.proceso.repository.ITmpCompromisoRepository; import com.ada.viatico.proceso.service.Impl.TicketsImp; import com.ada.viatico.proceso.service.Impl.ViaticoSolicitudImpl; import com.ada.viatico.proceso.sfcomercial.dto.AprobacionLegalizacionDto; import com.ada.viatico.proceso.utiliti.ViaticosException; /** * The Class ViaticosServiceV2. * @version 2.0 */ @Service public class ViaticosServiceV2 { /** The log. */ Logger log = LoggerFactory.getLogger(ViaticosServiceV2.class); /** The Constant APROBRAR. */ private static final String APROBRAR = "A"; /** The ticketsimpl. */ @Autowired private TicketsImp ticketsimpl; /** The solicitud repo. */ @Autowired private ViaticoSolicitudImpl solicitudRepo; /** The compromiso tmp. */ @Autowired public ITmpCompromisoRepository compromisoTmp; /** The legalizacion component V 2. */ @Autowired private LegalizacionComponentV2 legalizacionComponentV2; /** The solicitud component. */ @Autowired private SolicitudComponentV2 solicitudComponentV2; /** * Aprobar solicitud. * * @param datos the datos * @return the generic response dto * @throws ViaticosException the viaticos exception */ @Transactional(rollbackFor = ViaticosException.class) public GenericResponseDto aprobarSolicitud(DatosGenerarAprobacion datos) throws ViaticosException { if (datos.getEstadoSolicitudTipo().equals(APROBRAR)) { return solicitudComponentV2.realizarAprobacion(datos); } else { return solicitudComponentV2.cancelarAprobacion(datos); } } /** * Aprobar legalizacion. * * @param aprobacionLegalizacionDto the aprobacion legalizacion dto * @param viaticoDetalleSolicitud the viatico detalle solicitud * @param viaticoDetalleAprobacion the viatico detalle aprobacion * @return the generic response dto * @throws ViaticosException the viaticos exception */ @Transactional(rollbackFor = ViaticosException.class) public GenericResponseDto aprobarLegalizacion(AprobacionLegalizacionDto aprobacionLegalizacionDto) throws ViaticosException { if(aprobacionLegalizacionDto.getEstadoTipo().equals("A")) { GenericResponseDto genericResponseDto = legalizacionComponentV2.generarProcesoInterfaz(aprobacionLegalizacionDto); if(genericResponseDto.getCode() == 1L) { solicitudRepo.generarUpdateEstadoTipo("A", aprobacionLegalizacionDto.getIdTicket()); ticketsimpl.generarUpdateEstadoTicket("A", aprobacionLegalizacionDto.getIdTicketLegalizado(), aprobacionLegalizacionDto.getDescripcion()); compromisoTmp.actualizarOrdenador(0L, aprobacionLegalizacionDto.getIdTicket()); } return genericResponseDto; }else { solicitudRepo.generarUpdateEstadoTipo("V", aprobacionLegalizacionDto.getIdTicket()); ticketsimpl.generarUpdateEstadoTicket("V", aprobacionLegalizacionDto.getIdTicketLegalizado(), aprobacionLegalizacionDto.getDescripcion()); compromisoTmp.actualizarOrdenador(aprobacionLegalizacionDto.getKaNlTerceroFuncionario(),aprobacionLegalizacionDto.getIdTicket()); } return new GenericResponseDto(1L, "El documento sigue pendiente por proceso de aprobación."); } }
Es un directorio general donde se registran clases de utilidad para mantener la base de código organizada y modular.
/** * Copyright © 2023 ADA Corporation. All rights reserved. * * This component is protected by copyright. * * Use of this component is subject to the terms of the license agreement. */ package com.ada.viatico.proceso.sfcomercial; import java.text.ParseException; import java.text.SimpleDateFormat; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Date; /** * The Class Utility. */ public class Utility { /** The instance. */ private static Utility instance; /** The Constant colombia. */ // Create a new Locale //public static final Locale colombia =new Locale("es", "CO"); /** The Constant pesoFormat. */ // Create a formatter given the Locale //public static final NumberFormat pesoFormat = NumberFormat.getCurrencyInstance(colombia); public static final Long PRESUPUESTO=1L; public static final Long TESORERIA=2L; public static final Long CONTABILIDAD=2L; public static final Long COMPROMISO=2L; public static final Long COMPROBANTE_EGRESO=1L; public static final Long COMPROBANTE_INGRESO=2L; /** * Instantiates a new utility. */ private Utility() { } /** * Gets the single instance of Utility. * * @return single instance of Utility */ public static Utility getInstance() { if (instance == null) instance = new Utility(); return instance; } /** * Convert from util date. * * @param utilDate the util date * @return the java.sql. date */ public static java.sql.Date convertFromUtilDate(java.util.Date utilDate) { if (utilDate == null) return null; return new java.sql.Date(utilDate.getTime()); } /** * Convert from sql date. * * @param sqlDate the sql date * @return the java.util. date */ public static java.util.Date convertFromSqlDate(java.sql.Date sqlDate) { if (sqlDate == null) return null; return new java.util.Date(sqlDate.getTime()); } /** * Gets the date without time using format. * * @return the date without time using format * @throws ParseException the parse exception */ public static Date getDateWithoutTimeUsingFormat() throws ParseException { SimpleDateFormat formatter = new SimpleDateFormat("dd/MM/yyyy"); // Formatear la fecha actual sin hora y convertirla a Date return formatter.parse(formatter.format(new Date())); } /** * Gets the numero. * * @return the numero */ public static String getNumero() { String formato = "yyyyMMddHHmmss"; DateTimeFormatter formateador = DateTimeFormatter.ofPattern(formato); LocalDateTime ahora = LocalDateTime.now(); return formateador.format(ahora); } }
Además de las carpetas src/main/java, hay otras carpetas que deben tenerse en una aplicación Springboot.
La estructura de carpetas puede variar de una empresa a otra. Pero esta es la estructura de carpetas básica de una aplicación Spring Boot que se recomienda se implemente en la mayoria de proyectos.
Contenido adaptado desde el sitio web: https://malshani-wijekoon.medium.com/spring-boot-folder-structure-best-practices-18ef78a81819