Buenas prácticas de la capa de servicio
La capa de servicios encapsula las operaciones de negocio y orquesta el acceso a datos para los controladores, las tareas programadas y otros componentes. Los servicios se ejecutan en el mismo ámbito de inyección de dependencias que los controladores y las tareas, trabajan con entidades de dominio y dejan las preocupaciones de presentación a las capas superiores.
Mantén los servicios con propósito
- Introduce un servicio solo cuando la lógica abarque varias consultas/repositorios o requiera preocupaciones transversales como caché o mensajería.
- No envuelvas una única consulta en un método de servicio; en ese caso, expón la consulta directamente.
Diseña interfaces limpias
- Empareja interfaz e implementación (por ejemplo,
IPrestamoServicio/PrestamoServicio) para poder sustituir o simular la implementación en pruebas. - Mantén los métodos cohesivos y asíncronos: sufija los métodos asíncronos con
Asyncy acepta unCancellationTokenen las operaciones de E/S. - Cuando una interfaz crece demasiado, segrégala por responsabilidad (Crono lo hace, por ejemplo, con
IPrestamoServicio, dividido en interfaces específicas comoIPrestamoConsultaServicio,IPrestamoLiquidacionServicio, etc.).
Minimiza dependencias
- Inyecta por el constructor solo los colaboradores que realmente necesitas; evita el patrón service locator.
- Nunca inyectes controladores, ayudantes de Razor u otros tipos de presentación.
- Trabaja con modelos de dominio y con el
CronoDbContexto.
Evita cadenas de servicios
- Los servicios deben ser sin estado e independientes. Cuando uno necesita funcionalidad de otro, extrae un ayudante compartido o un método de dominio.
- Trae los datos por lotes en vez de iterar elemento por elemento llamando a otro servicio en cada iteración.
Ejemplo
public interface IPrecioServicio
{
Task<decimal> ObtenerPrecioAsync(int prestamoId, CancellationToken cancelToken = default);
}
public class PrecioServicio : IPrecioServicio
{
private readonly CronoDbContexto _db;
private readonly ICacheAdministrador _cache;
// Registrador opcional inyectado por propiedad
public ILogger Registrador { get; set; } = NullLogger.Instance;
public PrecioServicio(CronoDbContexto db, ICacheAdministrador cache)
{
_db = db;
_cache = cache;
}
public async Task<decimal> ObtenerPrecioAsync(int prestamoId, CancellationToken cancelToken = default)
{
var clave = $"prestamo-precio-{prestamoId}";
return await _cache.ObtenerAsync(clave, async () =>
{
var prestamo = await _db.Prestamos.FindByIdAsync(prestamoId, cancelToken);
return prestamo.Monto;
});
}
}
Registro y caché
- Expón un registrador opcional para que el contenedor inyecte una instancia contextual:
public ILogger Registrador { get; set; } = NullLogger.Instance;(ver Registro). - Usa
ICacheAdministradorpara consultas costosas e invalida las entradas cuando cambien los datos (ver Caché).
Pruebas
- Mantén las interfaces pequeñas para simplificar el mocking y las pruebas unitarias.
- Evita el estado estático salvo que sea thread-safe y compartido de forma intencional.