OLA · Práctica 02
Calidad de Software
1 / 12
Logo de la Universidad La Salle
Universidad La Salle Facultad de Ingenierías y Arquitectura · Escuela Profesional de Ingeniería de Software
Curso: Calidad de Software
Docente: Maribel Molina Barriga
Arequipa, octubre de 2026
PRÁCTICA DE LABORATORIO 02

Evaluación del diseño mediante atributos de calidad y evidencias

Proyecto OLA — Observatorio Litoral de Anomalías térmicas
Módulo «Alertas y avisos» del backend · commit d5c054f
¡Miau! Hoy evaluamos un diseño
Integrantes
Bloque 1 · Contexto

El caso: OLA y su módulo «Alertas y avisos»

Primero, el contexto

¿Qué hace OLA?

Clasifica la anomalía de la temperatura del mar en los laboratorios costeros de IMARPE y avisa a los usuarios suscritos.

Módulo evaluado

«Alertas y avisos» del backend (RF-01 y RF-03), rama main, commit d5c054f del 13/09/2026.

1

Evaluar alertas

alert_service.evaluate detecta los episodios de anomalía.

2

Registrar avisos

create_for_events guarda un aviso pendiente por suscrito y canal.

3

Enviar

Un administrador dispara send_pending: envía y reintenta los fallidos.

14unidades de diseño revisadas
13módulos medidos
1 011líneas de código
Python 3.13FastAPISQLAlchemy 2PostgreSQL 17
Bloque 1 · Contexto

¿Cómo lo evaluamos? Con evidencias

Medido, no opinado

Lista de revisión

8 criterios × 14 unidades = 112 comprobaciones.

Responsabilidad únicaCohesiónAbstraccionesAbierto/cerradoCapasSin duplicaciónComplejidad hasta 10Probar sin infraestructura

Métricas de diseño

LOC · NOM · Ce · Ca · inestabilidad · complejidad ciclomática por módulo.

Guion propio metricas.py (módulo ast), contrastado con ruff (C901).

Re-medición en Docker

Imagen del proyecto ola-api (Python 3.13.15) con PostgreSQL 17.11 desechable: 442 pruebas, ruff, mypy estricto y cobertura.

Escenario de cambio

El mismo cambio, un canal webhook, implementado en los dos diseños y medido con git diff --numstat.

Evidencias numeradas, citadas
y con su salida en bruto
Bloque 2 · Diagnóstico

Diseño actual (E-01): un servicio lo hace todo

¡Sospechoso!

Cuatro capas

Routers servicios dominio repositorios. En rosa, las unidades y dependencias con hallazgos.

notification_service

Reúne cinco responsabilidades y construye 4 sentencias SQL:

planificarpersistirredactarenviarmarcar lectura

clock: Ca = 0

Hay un reloj inyectable, pero nadie lo usa: el servicio llama a datetime.now(UTC) en 3 lugares (H-04).

Bloque 2 · Diagnóstico

Lo que dicen las métricas y el código

Los números no mienten
141líneasen notification_service
20complejidad acumulada: la mayor del módulo
4SQLsentencias construidas en el servicio
8de 9incumplimientos en solo 2 unidades

Complejidad baja (máx. 8): falla el reparto

  1. 1Solo conoce el correo (H-02)
  2. 2Guarda el estado en el bucle (H-01)
  3. 3Lee el reloj del sistema (H-04)
  4. 4Solo se prueba con PostgreSQL (H-05)
notification_service.py · send_pending · d5c054f (sin tipos)
def send_pending(session, mailer, *, limit=200):
    pendientes = notifications_repo.pending_emails(session, limit=limit)
    enviados = fallidos = 0

    for notificacion in pendientes:
        mensaje = _mensaje(notificacion)
        try:
            mailer.send(Email(to=notificacion.user.email,
                              subject=mensaje.subject, body=mensaje.body))
        except Exception as exc:
            notificacion.status = NotificationStatus.FAILED
            notificacion.error = str(exc)[:500]
            fallidos += 1
        else:
            notificacion.status = NotificationStatus.SENT
            notificacion.error = None
            notificacion.sent_at = datetime.now(UTC)
            enviados += 1

    session.commit()
    return SendSummary(attempted=len(pendientes),
                       sent=enviados, failed=fallidos)
Bloque 3 · Hallazgos

12 hallazgos, ordenados por prioridad

¡Doce! ¡Miau!

Regla de prioridad (E-04)

Impacto × 10 − Esfuerzo

Primero el impacto; a igual impacto, el menor esfuerzo.

Severidad según el impacto

Alta3
Media4
Baja5

Top 5 y estado

H-01H-02H-05H-03H-04
5 resueltos en el parche5 pendientes2 aceptados
Bloque 3 · Hallazgos

Los 5 priorizados comparten una causa

Una causa, dos soluciones
H-01Alta

Cinco responsabilidades

Planifica, persiste, redacta, envía y marca la lectura; además construye SQL.

Resuelto en el parchePrioridad 1
H-02Alta

Canales no extensibles

Un canal nuevo obliga a tocar el servicio, el repositorio, el router y las dependencias.

Resuelto en el parchePrioridad 1
H-05Alta

Envío sin pruebas unitarias

Reintentos y fallos solo se prueban con PostgreSQL: 0 unitarias, 34 de integración.

Resuelto en el parchePrioridad 3
H-03Media

Inserción duplicada

El mismo insert … on_conflict y su fila, dos veces en el servicio.

Resuelto en el parchePrioridad 4
H-04Media

Reloj sin usar

Clock existe, pero nadie lo importa: se usa el reloj del sistema.

Resuelto en el parchePrioridad 4
R1 · Canales de aviso resuelve H-02
R2 · Almacén, despachador y reloj resuelve H-01 · H-03 · H-04 · H-05
Los otros 7 quedan registrados con su decisión: 5 pendientes (H-06, H-07, H-09, H-10, H-12) y 2 aceptados (H-08, H-11).
Bloque 4 · Rediseño

R1 + R2: canales, despachador y reloj

¡Manos a la obra!
«coordinador» · modificadonotification_service
nuevo · recibe almacén, canales y relojNotificationDispatcher
«protocol» · nuevoNotificationStore
«protocol» · nuevoChannel
«protocol» · ya existíaClock
repositorioSqlNotificationStore
canalesEmailChannel InAppChannel
relojSystemClock
1 cada canal entrega su aviso · 2 el reloj llega inyectado
Un canal nuevo = una clase que cumple Channel + una línea en default_channels.
services/notification_dispatcher.py · nuevo
class NotificationDispatcher:
    def __init__(self, store: NotificationStore,
                 channels: ChannelRegistry, clock: Clock) -> None:
        self._store = store
        self._channels = channels
        self._clock = clock

    def send_pending(self, *, limit: int = 200) -> SendSummary:
        codigos = [canal.code for canal in self._channels.dispatchable()]
        pendientes = self._store.pending(codigos, limit=limit)
        enviados = fallidos = 0

        for aviso in pendientes:
            try:
                self._channels.get(aviso.channel).deliver(aviso)
            except Exception as exc:
                self._store.mark_failed(aviso.id, str(exc)[:MAX_ERROR_LENGTH])
                fallidos += 1
            else:
                self._store.mark_sent(aviso.id, self._clock.now())
                enviados += 1

        return SendSummary(attempted=len(pendientes),
                           sent=enviados, failed=fallidos)
Bloque 4 · Rediseño

Diseño propuesto (E-06) y su costo

Menos líneas, más pruebas
Verde: nuevoÁmbar: modificadoRosa: pendiente
14173
líneas de notification_service
2012
complejidad acumulada del servicio
40
sentencias SQL en el servicio
012
pruebas unitarias del envío

Probar la lógica de envío (E-09)

34 de integración, con PostgreSQL12.5–12.9 s
12 unitarias, en memoria0.02 s

El costo: +16 % de código (1 011 1 173 líneas) y más indirección: el servicio depende de 8 módulos (antes 5). Las 472 pruebas pasan.

Bloque 5 · Escenario y conclusiones

Escenario de cambio: un canal webhook

¡Que gane el mejor diseño!
Diseño actual
VS
Diseño propuesto
diseno_actual.patch: otra rama en el bucle
if notificacion.channel is NotificationChannel.WEBHOOK:
    webhook.send(WebhookMessage(...))
else:
    mailer.send(Email(...))
diseno_propuesto.patch: una clase nueva
class WebhookChannel:
    code = NotificationChannel.WEBHOOK
    requires_dispatch = True
    def deliver(self, notice: OutboundNotice) -> None: ...
Mejora real, pero moderada: el cambio queda localizado y el bucle no crece; las pruebas que cambian por especificación no disminuyen.
Bloque 5 · Escenario y conclusiones

Decisión (ADR-001) y conclusiones

¡Decisión tomada!

ADR-001 · Separar el envío en canales, almacén y despachador

Aceptado el 06/10/2026Resuelve H-01 a H-05
A · Puertos y canales4.25
B · Cambio mínimo3.55
C · Cola asíncrona3.20

Pesos: modificabilidad 30 %, comprobabilidad 25 %, esfuerzo 20 %, riesgo 15 %, comprensibilidad 10 %.

Gana A: la única que mejora a la vez modificabilidad y comprobabilidad.
1

El problema no era la complejidad (ninguna función supera 10), sino la cohesión y la dirección de las dependencias.

2

Dos refactorizaciones resolvieron los 5 hallazgos priorizados sin modificar ninguna de las 442 pruebas existentes.

3

La mejora se midió: el canal nuevo toca 2 módulos existentes menos y el bucle no crece, pero no reduce las pruebas que hay que actualizar.

4

Tiene un costo: +16 % de código y más indirección; 7 hallazgos quedan registrados con su decisión.

¡Gracias!

¿Preguntas?
Práctica de Laboratorio 02 · Proyecto OLA · Calidad de Software
Inicio
0 %
Calidad de Software · Universidad La Salle

Práctica 02 · Proyecto OLA

Evaluación del diseño mediante atributos de calidad y evidencias

→Espacioavanzar ←retroceder Fpantalla completa Msonido Cmodo calma