Principios SOLID
Los principios SOLID son cinco reglas de diseño de software formuladas por Robert C. Martin que guían la escritura de código modular, extensible y testeable. No son dogmas absolutos: son herramientas de análisis que ayudan a detectar fricciones en el diseño antes de que se conviertan en deuda técnica.
En el contexto de GM Transport —con servicios en Go, NestJS/TypeScript y Flutter/Dart— los principios SOLID tienen expresiones concretas distintas en cada lenguaje. Go no tiene clases ni herencia; Dart tiene mixins; TypeScript tiene decoradores. Los principios son universales; la implementación es específica del lenguaje.
[S] Principio de Responsabilidad Única (SRP)
Una unidad de código —función, tipo, módulo— debe tener una sola razón para cambiar. Si una función cambia tanto cuando el negocio cambia sus reglas como cuando se reemplaza la base de datos, tiene dos responsabilidades y viola el SRP.
El indicador más claro de violación: un tipo o función que importa paquetes de infraestructura (GORM, HTTP, SQL) y también contiene lógica de negocio.
Ejemplo en Go
// Violación: la misma struct guarda la lógica de negocio Y los tags de GORMtype Solicitud struct { ID uint `gorm:"primaryKey"` RFC string `gorm:"column:rfc"` Estatus string `gorm:"column:estatus"`}
func (s *Solicitud) EsValida() bool { return s.RFC != "" }
// Correcto: dominio sin tags de infraestructura// src/domain/solicitud/solicitud.gotype Solicitud struct { UUID string RFC string Estatus EstatusSolicitud}
func (s *Solicitud) EsValida() bool { return s.RFC != "" }
// src/infrastructure/persistence/gorm/models/solicitud_model.gotype SolicitudModel struct { ID uint `gorm:"primaryKey"` RFC string `gorm:"column:rfc"` Estatus string `gorm:"column:estatus"`}Ejemplo en TypeScript/NestJS
// Violación: el servicio mezcla lógica de negocio con envío de email y persistencia@Injectable()class ConciliarService { async conciliar(uuid: string) { const solicitud = await this.db.query( `SELECT * FROM solicitudes WHERE uuid = ?`, [uuid], ); if (solicitud.estatus !== "ListaParaConciliar") throw new Error("Estatus inválido"); await this.db.query( `UPDATE solicitudes SET estatus = 'EnProceso' WHERE uuid = ?`, [uuid], ); await this.mailer.send({ to: solicitud.email, subject: "En proceso" }); }}
// Correcto: tres responsabilidades → tres clasesclass ConciliarSolicitudUseCase { async execute(cmd: ConciliarCommand): Promise<void> { const solicitud = await this.solicitudRepo.findByUUID(cmd.uuid); solicitud.iniciarProcesamiento(); // lógica de negocio en la entidad await this.solicitudRepo.save(solicitud); this.eventBus.publish(new SolicitudEnProceso(solicitud.uuid)); }}Cuándo aplicar SRP
Cuando una clase tiene más de una razón para cambiar y ese cambio implicaría modificar código ajeno a la responsabilidad. No dividir a la primera señal: el SRP se aplica cuando la separación reduce el acoplamiento real, no solo para aumentar el número de archivos.
[O] Principio de Abierto/Cerrado (OCP)
Un módulo debe estar abierto para extensión y cerrado para modificación. La nueva funcionalidad se agrega definiendo nuevas implementaciones, no modificando el código existente.
En la práctica, OCP se implementa mediante interfaces y composition: el código que consume define qué necesita (interfaz); el código que provee implementa esa interfaz. Agregar un nuevo proveedor no toca el consumidor.
Ejemplo en Go
// Interfaz — definida por quien consumetype NotificadorRepository interface { Send(destinatario string, mensaje string) error}
// Implementación 1 — sin modificar la interfaz ni el caso de usotype EmailNotificador struct { client *smtp.Client }
func (e *EmailNotificador) Send(dest, msg string) error { return e.client.Send(dest, msg)}
// Implementación 2 — nueva funcionalidad sin tocar NotificadorRepositorytype SlackNotificador struct { webhook string }
func (s *SlackNotificador) Send(dest, msg string) error { return postToSlack(s.webhook, dest, msg)}
// El caso de uso no cambia cuando se agrega Slacktype NotificarUseCase struct { notificador NotificadorRepository}Ejemplo en TypeScript/NestJS
// Con decorador y módulo NestJS — agregar un nuevo generador de reporte// no modifica los existentes ni el use caseexport interface ReporteGenerator { generar(conciliacionUUID: string): Promise<Buffer>;}
@Injectable()export class ReporteR21Generator implements ReporteGenerator { async generar(uuid: string): Promise<Buffer> { /* lógica R21 */ }}
@Injectable()export class ReportePDFGenerator implements ReporteGenerator { async generar(uuid: string): Promise<Buffer> { /* lógica PDF */ }}[L] Principio de Sustitución de Liskov (LSP)
Si B extiende A, cualquier código que funcione con A debe funcionar igual de bien con B. El subtipo no puede romper los contratos del supertipo: precondiciones no más estrictas, postcondiciones no más débiles.
En Go (sin herencia), LSP se aplica a las implementaciones de interfaces: toda implementación de una interfaz debe cumplir el contrato implícito, no solo la firma.
Ejemplo en Go
// Contrato implícito: FindByUUID retorna ErrNotFound si no existe, nunca nil, niltype SolicitudRepository interface { FindByUUID(uuid string) (Solicitud, error)}
// Violación de LSP — retorna nil, nil en vez de ErrNotFoundtype MemorySolicitudRepo struct { data map[string]Solicitud }
func (r *MemorySolicitudRepo) FindByUUID(uuid string) (Solicitud, error) { s, ok := r.data[uuid] if !ok { return Solicitud{}, nil // violación: el consumidor no puede distinguir "no encontrado" de error } return s, nil}
// Correctofunc (r *MemorySolicitudRepo) FindByUUID(uuid string) (Solicitud, error) { s, ok := r.data[uuid] if !ok { return Solicitud{}, ErrSolicitudNotFound(uuid) } return s, nil}Ejemplo en Dart/Flutter
// La subclase no puede restringir las precondiciones del supertipoabstract class AuthRepository { Future<Either<Failure, User>> login(String rfc, String password);}
// Violación: AuthHttpRepository lanza excepción en vez de Either<Failure>class AuthHttpRepository implements AuthRepository { @override Future<Either<Failure, User>> login(String rfc, String password) async { // Violación: lanza en vez de retornar Left(Failure) final response = await dio.post('/login', data: {'rfc': rfc}); if (response.statusCode != 200) throw Exception('Login failed'); return Right(UserMapper.fromJson(response.data)); }}
// Correcto: respeta el contrato Either<Failure, User>class AuthHttpRepository implements AuthRepository { @override Future<Either<Failure, User>> login(String rfc, String password) async { try { final response = await dio.post('/login', data: {'rfc': rfc}); return Right(UserMapper.fromJson(response.data)); } on DioException catch (e) { return Left(ServerFailure(e.message)); } }}[I] Principio de Segregación de Interfaces (ISP)
Una implementación no debe depender de métodos que no usa. Las interfaces gordas —con muchos métodos— obligan a implementaciones que solo necesitan dos métodos a proporcionar implementaciones vacías o que lanzan excepciones para los métodos que no necesitan.
Ejemplo en Go
// Violación: interface gorda — los reportes no necesitan Save ni Deletetype SolicitudRepository interface { FindByUUID(uuid string) (Solicitud, error) FindAll() ([]Solicitud, error) Save(s Solicitud) error Delete(uuid string) error}
// Correcto: interfaces segregadas por caso de usotype SolicitudReader interface { FindByUUID(uuid string) (Solicitud, error) FindByRFC(rfc string) ([]Solicitud, error)}
type SolicitudWriter interface { Save(s Solicitud) error}
// El reporte solo necesita leertype GenerarReporteUseCase struct { solicitudes SolicitudReader // no conoce Save ni Delete}Ejemplo en TypeScript/NestJS
// Interface segregada por propósitoexport interface ConciliacionReader { findByUUID(uuid: string): Promise<Conciliacion | null>; findByRFC(rfc: string): Promise<Conciliacion[]>;}
export interface ConciliacionWriter { save(c: Conciliacion): Promise<void>;}
// El módulo de reportes implementa solo la lectura@Injectable()export class ConciliacionReporteService implements ConciliacionReader { async findByUUID(uuid: string) { /* ... */ } async findByRFC(rfc: string) { /* ... */ } // No necesita implementar save}[D] Principio de Inversión de Dependencia (DIP)
Los módulos de alto nivel no deben depender de los módulos de bajo nivel. Ambos deben depender de abstracciones. Las abstracciones no deben depender de los detalles; los detalles deben depender de las abstracciones.
En términos prácticos: el dominio (alto nivel) define interfaces; la infraestructura (bajo nivel) las implementa. El dominio nunca importa la infraestructura.
Ejemplo en Go
// Violación: el caso de uso importa directamente GORMimport "gorm.io/gorm"
type ConciliarUseCase struct { db *gorm.DB // dependencia de bajo nivel directa}
func (uc *ConciliarUseCase) Execute(cmd ConciliarCommand) error { var s models.Solicitud uc.db.Where("uuid = ?", cmd.UUID).First(&s) // infraestructura en el dominio}
// Correcto: el caso de uso depende de la interfaz, no de GORMtype ConciliarUseCase struct { solicitudes SolicitudRepository // interfaz concils ConciliacionRepository}
func (uc *ConciliarUseCase) Execute(cmd ConciliarCommand) error { s, err := uc.solicitudes.FindByUUID(cmd.UUID) if err != nil { return err } return s.IniciarProcesamiento()}Ejemplo en NestJS con inyección de dependencias
// El módulo registra la implementación concreta bajo el token de la interfaz@Module({ providers: [ { provide: SOLICITUD_REPOSITORY, // token = abstracción useClass: GormSolicitudRepository, // implementación concreta }, ConciliarSolicitudUseCase, ],})export class SolicitudModule {}
// El caso de uso recibe la abstracción — no sabe qué hay detrás@Injectable()export class ConciliarSolicitudUseCase { constructor( @Inject(SOLICITUD_REPOSITORY) private readonly solicitudes: SolicitudRepository, // interfaz ) {}}Ejemplo en Dart/Flutter
// El caso de uso recibe la abstracción en el constructorclass LoginUseCase { final AuthRepository _authRepository; // abstracción
LoginUseCase(this._authRepository);
Future<Either<Failure, User>> execute(String rfc, String password) { return _authRepository.login(rfc, password); }}
// La inyección se hace en el composition root (provider)final loginUseCaseProvider = Provider((ref) => LoginUseCase( ref.read(authRepositoryProvider), // implementación concreta inyectada aquí));Resumen
| Principio | Regla central | Señal de violación |
|---|---|---|
| SRP | Una razón para cambiar | La clase importa infraestructura y contiene lógica de negocio |
| OCP | Extensión sin modificación | Agregar un proveedor obliga a editar el consumidor |
| LSP | El subtipo cumple el contrato | Un mock retorna nil, nil donde el contrato dice ErrNotFound |
| ISP | Interfaces mínimas | Una implementación tiene métodos vacíos o con panic("not implemented") |
| DIP | Depender de abstracciones | El dominio importa GORM, TypeORM o cualquier paquete de infraestructura |