The Automatización Backend leverages several classic design patterns to create a maintainable, extensible system. Each pattern was chosen to solve specific architectural challenges.
The Chain of Responsibility pattern allows multiple restriction handlers to validate scheduling constraints in sequence. If any handler fails, the chain stops and returns an error.
Instead of stopping at the first failure, collect all errors:
# src/core/restrictions/restriction_handler.py:139def validate_all_and_collect_errors(self, context: Dict[str, Any]) -> List[str]: """ Execute all validations and collect ALL errors. Useful for showing users all problems at once. """ errors = [] # Validate this handler result = self.validate(context) if result is not None: errors.append(result) # Continue with next handler regardless if self._next_handler: next_errors = self._next_handler.validate_all_and_collect_errors(context) errors.extend(next_errors) return errors
The Builder pattern constructs complex schedule objects step-by-step, allowing different types of schedules (normal, lab, virtual, block) to be built with the same construction process.
# src/core/schedules/builders/horario_normal_builder.py:6class HorarioNormalBuilder(HorarioBuilderBase): """Builder for regular lecture classes""" def validate_build_data(self) -> str | None: """Validate all required fields are present""" required_fields = { 'docente': self._docente, 'aula': self._aula, 'asignatura': self._asignatura, 'start_time': self._start_time, 'end_time': self._end_time, 'dia': self._dia, 'sede': self._sede, 'id': self._id } missing_fields = [field for field, value in required_fields.items() if value is None] if missing_fields: return f"Missing required fields: {missing_fields}" return None def build(self) -> HorarioBase: validation_error = self.validate_build_data() if validation_error: raise ValueError(f"Error building normal schedule: {validation_error}") self._validate_normal_requirements() horario = HorarioClaseNormal( docente=self._docente, aula=self._aula, asignatura=self._asignatura, start_time=self._start_time, end_time=self._end_time, dia=self._dia, sede=self._sede ) return horario def _validate_normal_requirements(self) -> None: """Validate business rules for normal classes""" # Classroom must be lecture-type aula_tipo = self._aula.get('tipo', '').lower() if aula_tipo not in ['aula', 'teorica']: raise ValueError("Classroom must be 'aula' or 'teorica' type for normal class") # Duration constraints start_hour, start_min = map(int, self._start_time.split(':')) end_hour, end_min = map(int, self._end_time.split(':')) duration_minutes = (end_hour * 60 + end_min) - (start_hour * 60 + start_min) if duration_minutes < 50: raise ValueError("Normal classes must be at least 50 minutes") if duration_minutes > 180: raise ValueError("Normal classes cannot exceed 3 hours")
Normal Builder
Lab Builder
Virtual Builder
Block Builder
class HorarioNormalBuilder(HorarioBuilderBase): # Requires: all fields # Validates: duration 50-180 min, classroom type 'teorica' def get_tipo(self) -> str: return 'normal'
class HorarioLaboratorioBuilder(HorarioBuilderBase): # Requires: all fields # Validates: duration 100-240 min, classroom type 'laboratorio' def get_tipo(self) -> str: return 'laboratorio'
class HorarioVirtualBuilder(HorarioBuilderBase): # Requires: docente, asignatura, tiempo (no aula or sede) # Validates: has meeting URL def get_tipo(self) -> str: return 'virtual'
class HorarioBloqueoBuilder(HorarioBuilderBase): # Requires: aula, tiempo, razon (no docente or asignatura) # Used for: maintenance, events, closures def get_tipo(self) -> str: return 'bloqueo'
# src/core/schedules/builders/horario_director.py:6class HorarioDirector: """Orchestrates schedule construction using a builder""" def __init__(self): self._builder = None def set_builder(self, builder: IHorarioBuilder) -> None: self._builder = builder def construct_horario_completo(self, datos: Dict[str, Any]) -> HorarioBase: """Build a complete schedule from data dictionary""" if not self._builder: raise ValueError("No builder configured") self._builder.reset() # Set data only if present if datos.get('docente') is not None: self._builder.set_docente(datos['docente']) if datos.get('aula') is not None: self._builder.set_aula(datos['aula']) if datos.get('asignatura') is not None: self._builder.set_asignatura(datos['asignatura']) if datos.get('start_time') is not None and datos.get('end_time') is not None: self._builder.set_tiempo(datos['start_time'], datos['end_time']) # ... set other fields return self._builder.build()