BackDev Patterns & Practices
Solid Principles//

Principio de Segregación de Interfaces

Te guía a crear interfaces enfocadas para que los clientes dependan solo de lo que usan, reduciendo el acoplamiento y mejorando la flexibilidad.

Introducción

El Principio de Segregación de Interfaces (ISP) establece: ningún cliente debería verse forzado a depender de métodos que no utiliza. En términos prácticos, esto significa preferir varias interfaces pequeñas y enfocadas sobre una sola interfaz grande y de propósito general.

Las violaciones de ISP son sutiles. Una interfaz “gorda” puede parecer conveniente: un solo contrato que cubre todo. Pero cuando una clase implementa esa interfaz y la mitad de los métodos lanzan NotImplementedException o retornan valores ficticios, algo está mal. La interfaz está forzando a los implementadores a prometer capacidades que no tienen.

Este principio importa porque las interfaces gordas crean acoplamiento. Cuando dependes de una interfaz con 15 métodos pero solo usas 3, los cambios en los otros 12 métodos aún pueden forzarte a recompilar, re-probar y re-desplegar. Las interfaces enfocadas minimizan este acoplamiento.

En este artículo, exploramos ISP a través de un escenario real: construir un sistema de gestión de Comunidad de Práctica. Verás cómo el diseño de repositorios de Gathering, sus abstracciones CQRS e interfaces de almacenamiento demuestran una adecuada segregación de interfaces, y qué sucede cuando la ignoras.

El Problema: Interfaces Gordas

Un Ejemplo Simple: Impresora Multifunción

Antes de sumergirnos en el código de Gathering, veamos la violación clásica de ISP: una interfaz de dispositivo multifunción. Esto ilustra el principio central sin necesitar contexto de aplicación.

Sin ISP: Una Interfaz para Gobernarlos a Todos

1public interface IMultiFunctionDevice
2{
3    void Print(Document doc);
4    void Scan(Document doc);
5    void Fax(Document doc);
6    void Staple(Document doc);
7    void PhotoCopy(Document doc);
8}
9
10// Una impresora básica es forzada a implementar todo
11public class BasicInkjetPrinter : IMultiFunctionDevice
12{
13    public void Print(Document doc)
14    {
15        // Esto funciona bien
16        Console.WriteLine("Imprimiendo: " + doc.Name);
17    }
18
19    public void Scan(Document doc)
20    {
21        throw new NotSupportedException("Esta impresora no puede escanear");
22    }
23
24    public void Fax(Document doc)
25    {
26        throw new NotSupportedException("Esta impresora no puede enviar fax");
27    }
28
29    public void Staple(Document doc)
30    {
31        throw new NotSupportedException("Esta impresora no puede engrapar");
32    }
33
34    public void PhotoCopy(Document doc)
35    {
36        throw new NotSupportedException("Esta impresora no puede fotocopiar");
37    }
38}

El problema: BasicInkjetPrinter es forzada a implementar 5 métodos pero solo soporta 1. Los otros 4 son trampas: compilan pero lanzan excepciones en tiempo de ejecución. Cualquier cliente que reciba un IMultiFunctionDevice no puede confiar en que todos los métodos funcionen.

Con ISP: Interfaces Enfocadas

1// Cada capacidad es su propia interfaz
2public interface IPrinter
3{
4    void Print(Document doc);
5}
6
7public interface IScanner
8{
9    void Scan(Document doc);
10}
11
12public interface IFax
13{
14    void Fax(Document doc);
15}
16
17// La impresora básica implementa solo lo que soporta
18public class BasicInkjetPrinter : IPrinter
19{
20    public void Print(Document doc)
21    {
22        Console.WriteLine("Imprimiendo: " + doc.Name);
23    }
24}
25
26// El dispositivo multifunción implementa múltiples interfaces
27public class OfficePrinter : IPrinter, IScanner, IFax
28{
29    public void Print(Document doc) { /* ... */ }
30    public void Scan(Document doc) { /* ... */ }
31    public void Fax(Document doc) { /* ... */ }
32}
33
34// Los clientes dependen SOLO de lo que necesitan
35public class DocumentService
36{
37    private readonly IPrinter _printer;
38
39    public DocumentService(IPrinter printer)
40    {
41        // Esto funciona con BasicInkjetPrinter Y OfficePrinter
42        // Sin riesgo de NotSupportedException
43        _printer = printer;
44    }
45
46    public void PrintDocument(Document doc)
47    {
48        _printer.Print(doc); // Siempre seguro
49    }
50}

Beneficios de ISP Aquí:

  • ✓ Sin métodos muertos: Cada clase implementa solo lo que realmente soporta.
  • ✓ Seguro para clientes: DocumentService sabe que IPrinter.Print() siempre funciona, sin sorpresas en tiempo de ejecución.
  • ✓ Composición flexible: Nuevos dispositivos pueden implementar cualquier combinación de interfaces.

Este patrón simple (dividir interfaces gordas en interfaces enfocadas) es la base de ISP. Ahora apliquemoslo a un sistema más grande y del mundo real.

Caso de Estudio: Interfaces de Repositorio en Gathering

La aplicación Gathering necesita repositorios para Sessions y Communities. Un enfoque ingenuo crearía una interfaz masiva para todo el acceso a datos. Veamos cómo luce eso, y cómo Gathering lo evita.

La Violación: Una Interfaz de Repositorio Gorda

1// ✗ VIOLACIÓN: Una interfaz para todo
2public interface IDataRepository
3{
4    // Operaciones de Session
5    Task<Session?> GetSessionByIdAsync(Guid id, CancellationToken ct = default);
6    Task<IReadOnlyList<Session>> GetAllSessionsAsync(CancellationToken ct = default);
7    Task<IReadOnlyList<Session>> GetSessionsByCommunityIdAsync(
8        Guid communityId, CancellationToken ct = default);
9    Task<IReadOnlyList<Session>> GetActiveSessionsAsync(CancellationToken ct = default);
10    void AddSession(Session session);
11    void UpdateSession(Session session);
12    void RemoveSession(Session session);
13
14    // Operaciones de Community
15    Task<Community?> GetCommunityByIdAsync(Guid id, CancellationToken ct = default);
16    Task<IReadOnlyList<Community>> GetAllCommunitiesAsync(CancellationToken ct = default);
17    void AddCommunity(Community community);
18    void UpdateCommunity(Community community);
19    void RemoveCommunity(Community community);
20
21    // Operaciones de Resource
22    Task<IReadOnlyList<SessionResource>> GetResourcesBySessionIdAsync(
23        Guid sessionId, CancellationToken ct = default);
24    void AddResource(SessionResource resource);
25    void RemoveResource(SessionResource resource);
26
27    // Persistencia
28    Task<int> SaveChangesAsync(CancellationToken ct = default);
29}
30
31// CreateCommunityCommandHandler solo necesita operaciones de community,
32// pero depende de la interfaz COMPLETA
33public class CreateCommunityCommandHandler
34{
35    private readonly IDataRepository _repository;
36
37    public CreateCommunityCommandHandler(IDataRepository repository)
38    {
39        _repository = repository;
40        // Este handler depende de 17+ métodos pero usa solo 2:
41        // AddCommunity() y SaveChangesAsync()
42    }
43
44    public async Task<Result<Guid>> HandleAsync(CreateCommunityCommand command,
45        CancellationToken ct)
46    {
47        var result = Community.Create(command.Name, command.Description);
48        if (result.IsFailure) return Result.Failure<Guid>(result.Error);
49
50        _repository.AddCommunity(result.Value);       // Usa 1 método
51        await _repository.SaveChangesAsync(ct);         // Usa 1 método
52        return Result.Success(result.Value.Id);
53        // ¿Los otros 15 métodos? Acoplamiento innecesario.
54    }
55}

Problemas con esta Interfaz Gorda:

  • Acoplamiento innecesario: CreateCommunityCommandHandler está acoplado a operaciones de session, resource y community, solo necesita operaciones de community.
  • Difícil de probar: Hacer mock de IDataRepository requiere implementar 17+ métodos aunque la prueba solo ejercite 2.
  • Amplificación de cambios: Agregar un nuevo método de consulta de session fuerza la recompilación de cada clase que depende de IDataRepository, incluyendo handlers de community que no tienen nada que ver con sessions.
  • SRP violado: La interfaz misma tiene múltiples responsabilidades (sessions, communities, resources, persistencia).

El Enfoque Correcto: Interfaces Segregadas

Gathering resuelve esto elegantemente dividiendo responsabilidades en interfaces enfocadas. Cada interfaz sirve a un solo rol, y los clientes dependen solo de lo que necesitan.

Paso 1: Repositorio Base Genérico

1// De: Gathering.Domain/Abstractions/IRepository.cs
2public interface IRepository<T> where T : Entity
3{
4    // Consultas
5    Task<T?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
6    Task<IReadOnlyList<T>> GetAllAsync(CancellationToken cancellationToken = default);
7    Task<IReadOnlyList<T>> FindAsync(
8        Expression<Func<T, bool>> predicate,
9        CancellationToken cancellationToken = default);
10    Task<T?> FirstOrDefaultAsync(
11        Expression<Func<T, bool>> predicate,
12        CancellationToken cancellationToken = default);
13
14    // Verificaciones de existencia
15    Task<bool> ExistsAsync(Guid id, CancellationToken cancellationToken = default);
16    Task<bool> AnyAsync(
17        Expression<Func<T, bool>> predicate,
18        CancellationToken cancellationToken = default);
19
20    // Conteo
21    Task<int> CountAsync(CancellationToken cancellationToken = default);
22    Task<int> CountAsync(
23        Expression<Func<T, bool>> predicate,
24        CancellationToken cancellationToken = default);
25
26    // Comandos
27    void Add(T entity);
28    void AddRange(IEnumerable<T> entities);
29    void Update(T entity);
30    void UpdateRange(IEnumerable<T> entities);
31    void Remove(T entity);
32    void RemoveRange(IEnumerable<T> entities);
33}

Esta es una interfaz enfocada y cohesiva. Cada método trata sobre operaciones CRUD en un solo tipo de entidad. Cualquier repositorio para cualquier entidad necesita estas operaciones.

Paso 2: Interfaces de Repositorio Especializadas

1// De: Gathering.Domain/Sessions/ISessionRepository.cs
2public interface ISessionRepository : IRepository<Session>
3{
4    // Solo consultas específicas de Session que van más allá del CRUD genérico
5    Task<IReadOnlyList<Session>> GetByCommunityIdAsync(
6        Guid communityId,
7        CancellationToken cancellationToken = default);
8
9    Task<IReadOnlyList<Session>> GetActiveSessionsAsync(
10        CancellationToken cancellationToken = default);
11
12    Task<IReadOnlyList<SessionResource>> GetResourcesBySessionIdAsync(
13        Guid sessionId,
14        CancellationToken cancellationToken = default);
15
16    void AddResource(SessionResource resource);
17}
18
19// De: Gathering.Domain/Communities/ICommunityRepository.cs
20public interface ICommunityRepository : IRepository<Community>
21{
22    // ¡No se necesitan métodos adicionales!
23    // El IRepository<Community> base es suficiente
24}

Observa los Patrones Clave de ISP:

  • ICommunityRepository no agrega métodos. La interfaz genérica es suficiente. Ningún cliente se ve forzado a depender de métodos que solo las sessions necesitan.
  • ISessionRepository agrega solo consultas específicas de session. Métodos como GetByCommunityIdAsync no tienen sentido para communities.
  • IUnitOfWork está separado de los repositorios. La persistencia (SaveChanges) es una preocupación diferente al acceso a datos.

Paso 3: Preocupación de Persistencia Separada

1// De: Gathering.Domain/Abstractions/IUnitOfWork.cs
2public interface IUnitOfWork : IDisposable
3{
4    Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
5}

IUnitOfWork es una interfaz de un solo método. Es el ejemplo definitivo de ISP: una interfaz, una responsabilidad. Cualquier handler que necesite persistencia depende de esto, nada más.

Paso 4: Los Handlers Dependen Solo de lo que Necesitan

1// De: Gathering.Application/Communities/Create/CreateCommunityCommandHandler.cs
2public sealed class CreateCommunityCommandHandler 
3    : ICommandHandler<CreateCommunityCommand, Guid>
4{
5    private readonly ICommunityRepository _communityRepository;
6    private readonly IUnitOfWork _unitOfWork;
7    private readonly IValidator<CreateCommunityCommand> _validator;
8    private readonly IImageStorageService _imageStorageService;
9
10    public CreateCommunityCommandHandler(
11        ICommunityRepository communityRepository,
12        IUnitOfWork unitOfWork,
13        IValidator<CreateCommunityCommand> validator,
14        IImageStorageService imageStorageService)
15    {
16        _communityRepository = communityRepository;
17        _unitOfWork = unitOfWork;
18        _validator = validator;
19        _imageStorageService = imageStorageService;
20    }
21
22    public async Task<Result<Guid>> HandleAsync(
23        CreateCommunityCommand command, 
24        CancellationToken cancellationToken = default)
25    {
26        // Validación...
27        var result = Community.Create(command.Name, command.Description);
28        if (result.IsFailure) return Result.Failure<Guid>(result.Error);
29
30        _communityRepository.Add(result.Value);
31        await _unitOfWork.SaveChangesAsync(cancellationToken);
32
33        return Result.Success(result.Value.Id);
34    }
35}

ISP en Acción:

  • ICommunityRepository: Solo CRUD de community. Sin métodos de session contaminando la dependencia.
  • IUnitOfWork: Solo SaveChangesAsync. No mezclado con consultas de repositorio.
  • IImageStorageService: Solo Upload y Delete. No un IFileService gordo con 20 métodos.
  • IValidator: Solo valida CreateCommunityCommand. No un validador genérico para todos los comandos.

Caso de Estudio: Segregación de Interfaces CQRS

El patrón CQRS (Command Query Responsibility Segregation) de Gathering es otro poderoso ejemplo de ISP. En lugar de un solo IRequestHandler que maneja tanto lecturas como escrituras, los comandos y consultas tienen interfaces separadas.

La Violación: Un Handler para Todo

1// ✗ VIOLACIÓN: Una interfaz gorda para todas las operaciones
2public interface IHandler<TRequest, TResponse>
3{
4    Task<TResponse> HandleAsync(TRequest request, CancellationToken ct);
5    
6    // Los comandos necesitan validación, las consultas no
7    Task<ValidationResult> ValidateAsync(TRequest request);
8    
9    // Los comandos necesitan unit of work, las consultas no
10    Task SaveChangesAsync(CancellationToken ct);
11    
12    // Las consultas necesitan paginación, los comandos no
13    Task<PagedResult<TResponse>> HandlePagedAsync(
14        TRequest request, int page, int pageSize);
15}
16
17// Un handler de consulta es forzado a implementar métodos específicos de comandos
18public class GetSessionByIdHandler : IHandler<GetSessionQuery, SessionDto>
19{
20    public async Task<SessionDto> HandleAsync(
21        GetSessionQuery request, CancellationToken ct)
22    {
23        // Esto funciona bien
24        return await _repository.GetByIdAsync(request.Id, ct);
25    }
26
27    public Task<ValidationResult> ValidateAsync(GetSessionQuery request)
28    {
29        // Las consultas no necesitan FluentValidation - forzado a implementar
30        throw new NotImplementedException();
31    }
32
33    public Task SaveChangesAsync(CancellationToken ct)
34    {
35        // Las consultas no escriben datos - forzado a implementar
36        throw new NotImplementedException();
37    }
38
39    public Task<PagedResult<SessionDto>> HandlePagedAsync(
40        GetSessionQuery request, int page, int pageSize)
41    {
42        // No todas las consultas necesitan paginación - forzado a implementar
43        throw new NotImplementedException();
44    }
45}

El Enfoque Correcto: Interfaces Segregadas de Comando y Consulta

1// De: Gathering.Application/Abstractions/ICommand.cs
2public interface ICommand : IRequest<Result>, IBaseCommand { }
3
4public interface ICommand<TResponse> : IRequest<Result<TResponse>>, IBaseCommand { }
5
6// De: Gathering.Application/Abstractions/ICommandHandler.cs
7public interface ICommandHandler<TCommand> : IRequestHandler<TCommand, Result>
8    where TCommand : ICommand { }
9
10public interface ICommandHandler<TCommand, TResponse> 
11    : IRequestHandler<TCommand, Result<TResponse>>
12    where TCommand : ICommand<TResponse> { }
13
14// De: Gathering.Application/Abstractions/IQuery.cs
15public interface IQuery<TResponse> : IRequest<Result<TResponse>> { }
16
17// De: Gathering.Application/Abstractions/IQueryHandler.cs
18public interface IQueryHandler<TQuery, TResponse> 
19    : IRequestHandler<TQuery, Result<TResponse>>
20    where TQuery : IQuery<TResponse> { }

Cada interfaz tiene exactamente un método (heredado de IRequestHandler): el método Handle. Pero la segregación está en las restricciones de tipo:

  • ICommandHandler maneja solo tipos ICommand. Los comandos modifican estado y retornan Result.
  • IQueryHandler maneja solo tipos IQuery. Las consultas leen estado y retornan datos.
  • Ningún handler es forzado a implementar métodos de la otra categoría.
1// Un handler de comando - solo implementa manejo de comandos
2public sealed class CreateSessionCommandHandler 
3    : ICommandHandler<CreateSessionCommand, Guid>
4{
5    private readonly ISessionRepository _sessionRepository;
6    private readonly ICommunityRepository _communityRepository;
7    private readonly IUnitOfWork _unitOfWork;
8    private readonly IValidator<CreateSessionCommand> _validator;
9    private readonly IImageStorageService _imageStorageService;
10
11    public CreateSessionCommandHandler(
12        ISessionRepository sessionRepository,
13        ICommunityRepository communityRepository,
14        IUnitOfWork unitOfWork,
15        IValidator<CreateSessionCommand> validator,
16        IImageStorageService imageStorageService)
17    {
18        _sessionRepository = sessionRepository;
19        _communityRepository = communityRepository;
20        _unitOfWork = unitOfWork;
21        _validator = validator;
22        _imageStorageService = imageStorageService;
23    }
24
25    public async Task<Result<Guid>> HandleAsync(
26        CreateSessionCommand request, 
27        CancellationToken cancellationToken = default)
28    {
29        // Validar, verificar que community existe, subir imagen, crear session, guardar
30        // ...
31        _sessionRepository.Add(sessionResult.Value);
32        await _unitOfWork.SaveChangesAsync(cancellationToken);
33        return Result.Success(sessionResult.Value.Id);
34    }
35}

Caso de Estudio: Interfaz de Almacenamiento Enfocada

Otro patrón ISP en Gathering es la abstracción de almacenamiento de imágenes. En lugar de un IFileService gordo que maneje todas las operaciones de archivo imaginables, Gathering define una interfaz enfocada para exactamente lo que la aplicación necesita.

La Violación: Un Servicio de Archivos Navaja Suiza

1// ✗ VIOLACIÓN: Demasiadas capacidades en una sola interfaz
2public interface IFileService
3{
4    // Operaciones de imagen (necesarias para Gathering)
5    Task<Result<string>> UploadImageAsync(Stream stream, string fileName,
6        string contentType, string entityType, CancellationToken ct);
7    Task<Result> DeleteImageAsync(string url, CancellationToken ct);
8
9    // Operaciones de documento (no necesarias para Gathering)
10    Task<Result<string>> UploadDocumentAsync(Stream stream, string fileName,
11        CancellationToken ct);
12    Task<Result<byte[]>> DownloadDocumentAsync(string url, CancellationToken ct);
13    Task<Result<string>> ConvertToPdfAsync(string documentUrl, CancellationToken ct);
14
15    // Operaciones de video (no necesarias para Gathering)
16    Task<Result<string>> UploadVideoAsync(Stream stream, string fileName,
17        CancellationToken ct);
18    Task<Result<string>> GenerateThumbnailAsync(string videoUrl, CancellationToken ct);
19    Task<Result<TimeSpan>> GetVideoDurationAsync(string videoUrl, CancellationToken ct);
20
21    // Operaciones generales de archivo (no necesarias para handlers)
22    Task<bool> FileExistsAsync(string url, CancellationToken ct);
23    Task<Result<long>> GetFileSizeAsync(string url, CancellationToken ct);
24    Task<Result<IReadOnlyList<string>>> ListFilesAsync(string prefix, CancellationToken ct);
25}
26
27// CreateCommunityCommandHandler depende de 12 métodos pero usa solo 2
28public class CreateCommunityCommandHandler
29{
30    private readonly IFileService _fileService;
31    // Los 12 métodos son visibles y "disponibles" pero irrelevantes
32}

El Enfoque Correcto: Solo lo que Necesitas

1// De: Gathering.Application/Abstractions/IImageStorageService.cs
2public interface IImageStorageService
3{
4    Task<Result<string>> UploadImageAsync(
5        Stream imageStream,
6        string fileName,
7        string contentType,
8        string entityType,
9        CancellationToken cancellationToken = default);
10
11    Task<Result> DeleteImageAsync(
12        string imageUrl, 
13        CancellationToken cancellationToken = default);
14}

Dos métodos. Esa es la interfaz completa. Cada handler que necesita operaciones de imagen depende de exactamente lo que necesita: subir y eliminar, nada más.

1// De: Gathering.Infrastructure/Storage/AzureBlobStorageService.cs
2internal sealed class AzureBlobStorageService(
3    BlobServiceClient blobServiceClient) : IImageStorageService
4{
5    private const string ContainerName = "images";
6
7    public async Task<Result<string>> UploadImageAsync(
8        Stream imageStream, string fileName, string contentType,
9        string entityType, CancellationToken cancellationToken = default)
10    {
11        try
12        {
13            var containerClient = blobServiceClient
14                .GetBlobContainerClient(ContainerName);
15            await containerClient.CreateIfNotExistsAsync(
16                cancellationToken: cancellationToken);
17
18            var extension = Path.GetExtension(fileName).ToLower();
19            var blobName = $"{entityType}/{Guid.NewGuid()}{extension}";
20            var blobClient = containerClient.GetBlobClient(blobName);
21
22            imageStream.Seek(0, SeekOrigin.Begin);
23
24            var uploadOptions = new BlobUploadOptions
25            {
26                HttpHeaders = new BlobHttpHeaders { ContentType = contentType }
27            };
28
29            await blobClient.UploadAsync(
30                imageStream, uploadOptions, cancellationToken);
31
32            return Result.Success(blobClient.Uri.AbsoluteUri);
33        }
34        catch (Exception ex)
35        {
36            return Result.Failure<string>(
37                ImageStorageError.UploadFailed(ex.Message));
38        }
39    }
40
41    public async Task<Result> DeleteImageAsync(
42        string imageUrl, CancellationToken cancellationToken = default)
43    {
44        try
45        {
46            var uri = new Uri(imageUrl);
47            var blobClient = new BlobClient(uri);
48            await blobClient.DeleteIfExistsAsync(
49                cancellationToken: cancellationToken);
50
51            return Result.Success();
52        }
53        catch (Exception ex)
54        {
55            return Result.Failure(
56                ImageStorageError.DeleteFailed(ex.Message));
57        }
58    }
59}

Beneficios de ISP en la Práctica:

  • ✓ Fácil de implementar: AzureBlobStorageService implementa 2 métodos, no 12. La implementación es enfocada y directa.
  • ✓ Fácil de mockear: Las pruebas solo necesitan configurar 2 métodos. La preparación de pruebas es mínima.
  • ✓ Fácil de intercambiar: Cambiar de Azure a AWS significa implementar 2 métodos en una nueva clase, no 12.
  • ✓ Interfaz estable: Agregar características de procesamiento de video no afecta a IImageStorageService. Esas irían en un IVideoStorageService separado.

ISP en TypeScript

Los mismos principios aplican en TypeScript. Aquí está el patrón de repositorio y almacenamiento traducido a un contexto Node.js:

1// Interfaz de repositorio genérico - enfocada en CRUD
2interface IRepository<T extends Entity> {
3  getById(id: string): Promise<T | null>;
4  getAll(): Promise<ReadonlyArray<T>>;
5  find(predicate: (entity: T) => boolean): Promise<ReadonlyArray<T>>;
6  exists(id: string): Promise<boolean>;
7
8  add(entity: T): void;
9  update(entity: T): void;
10  remove(entity: T): void;
11}
12
13// Interfaz de persistencia segregada
14interface IUnitOfWork {
15  saveChanges(): Promise<number>;
16}
17
18// Extensión específica de Session
19interface ISessionRepository extends IRepository<Session> {
20  getByCommunityId(communityId: string): Promise<ReadonlyArray<Session>>;
21  getActiveSessions(): Promise<ReadonlyArray<Session>>;
22}
23
24// Repositorio de Community - no necesita métodos extra
25interface ICommunityRepository extends IRepository<Community> {
26  // El IRepository base es suficiente
27}
28
29// Almacenamiento de imagen enfocado - solo lo que la app necesita
30interface IImageStorageService {
31  uploadImage(
32    stream: NodeJS.ReadableStream,
33    fileName: string,
34    contentType: string,
35    entityType: string
36  ): Promise<Result<string>>;
37
38  deleteImage(imageUrl: string): Promise<Result<void>>;
39}
40
41// ✗ Violación de interfaz GORDA
42interface IFileService {
43  uploadImage(stream: NodeJS.ReadableStream, fileName: string): Promise<Result<string>>;
44  deleteImage(url: string): Promise<Result<void>>;
45  uploadDocument(stream: NodeJS.ReadableStream, fileName: string): Promise<Result<string>>;
46  downloadDocument(url: string): Promise<Result<Buffer>>;
47  convertToPdf(documentUrl: string): Promise<Result<string>>;
48  uploadVideo(stream: NodeJS.ReadableStream, fileName: string): Promise<Result<string>>;
49  generateThumbnail(videoUrl: string): Promise<Result<string>>;
50  // 7 métodos... y creciendo
51}
52
53  // ✓ Handler depende solo de interfaces enfocadas
54class CreateCommunityHandler {
55  constructor(
56    private communityRepository: ICommunityRepository,
57    private unitOfWork: IUnitOfWork,
58    private imageStorage: IImageStorageService
59  ) {}
60
61  async handle(command: CreateCommunityCommand): Promise<Result<string>> {
62    const result = Community.create(command.name, command.description);
63    if (!result.isSuccess) return failure(result.error);
64
65    if (command.imageStream) {
66      const uploadResult = await this.imageStorage.uploadImage(
67        command.imageStream,
68        command.imageFileName,
69        command.imageContentType,
70        "communities"
71      );
72
73      if (!uploadResult.isSuccess) {
74        return failure(uploadResult.error);
75      }
76    }
77
78    this.communityRepository.add(result.value);
79    await this.unitOfWork.saveChanges();
80
81    return success(result.value.id);
82  }
83}

Beneficios de Aplicar ISP

1. Acoplamiento Reducido

Cada handler depende de exactamente las interfaces que necesita. Los cambios en ISessionRepository no afectan a CreateCommunityCommandHandler. Los cambios en IImageStorageService no afectan a handlers de consulta que nunca suben imágenes.

2. Pruebas Más Simples

Hacer mock de IUnitOfWork significa implementar un método. Hacer mock de IImageStorageService significa implementar dos. Compara eso con hacer mock de un IDataRepository de 17 métodos para cada prueba.

3. Intención Más Clara

Un constructor que recibe ICommunityRepository, IUnitOfWork e IImageStorageService te dice exactamente qué hace el handler: gestiona communities, persiste cambios y maneja imágenes. Un constructor que recibe IDataRepository no te dice nada.

4. Evolución Independiente

Puedes agregar nuevos métodos a ISessionRepository (por ejemplo, GetUpcomingSessionsAsync) sin tocar ICommunityRepository ni sus implementaciones. Cada interfaz evoluciona independientemente.

5. Mejor Registro de DI

Con interfaces enfocadas, la inyección de dependencias es precisa. Registras cada interfaz con su implementación. Con una interfaz gorda, un solo registro maneja todo, haciendo más difícil intercambiar componentes individuales.

Trampas del Mundo Real

Trampa 1: Explosión de Interfaces

Dividir demasiado agresivamente crea docenas de interfaces de un solo método que son difíciles de navegar. El objetivo no es minimizar métodos por interfaz sino agrupar operaciones cohesivas. IRepository agrupa operaciones CRUD porque naturalmente pertenecen juntas.

Guía: Si los métodos siempre cambian juntos y sirven al mismo cliente, mantenlos en una sola interfaz.

Trampa 2: Dividir Basándose en la Implementación, No en las Necesidades del Cliente

ISP se trata de la perspectiva del cliente, no del implementador. No dividas una interfaz porque la implementación es compleja, divívela porque diferentes clientes necesitan diferentes subconjuntos.

Trampa 3: Interfaces Marcadoras sin Propósito

ICommunityRepository no agrega métodos a IRepository. Eso está bien, existe como un tipo distinto para inyección de dependencias y extensión futura. Pero crear interfaces vacías “por si acaso” sin un propósito claro de DI o extensión agrega ruido.

Trampa 4: Ignorar la Herencia de Interfaces

C# soporta herencia de interfaces (ISessionRepository extiende IRepository). Usa esto para componer interfaces enfocadas en lugar de duplicar firmas de métodos a través de interfaces separadas.

Lista de Verificación: Detectando Violaciones de ISP

Si respondes “sí” a cualquiera de estas, probablemente se esté violando ISP.

Conclusión

El Principio de Segregación de Interfaces transforma cómo diseñas contratos. En lugar de interfaces de talla única que fuerzan a los clientes a depender de métodos que nunca usan, creas interfaces enfocadas que coinciden con lo que cada cliente realmente necesita.

Gathering demuestra esto a través de su jerarquía de repositorios (IRepository → ISessionRepository / ICommunityRepository), sus abstracciones CQRS (ICommandHandler vs IQueryHandler), y su interfaz de almacenamiento (IImageStorageService con solo 2 métodos). Cada interfaz es cohesiva, enfocada y fácil de implementar y probar.

La idea clave: diseña interfaces desde la perspectiva del cliente, no del implementador. Pregunta “¿qué necesita este consumidor?”, no “¿qué puede hacer esta implementación?”

En el próximo artículo, exploramos el Principio de Inversión de Dependencias, que asegura que los módulos de alto nivel dependen de abstracciones en lugar de implementaciones concretas, la pieza final que hace que todos los principios SOLID funcionen juntos.

About

Dev Patterns & Practices is a space for long-form thinking on design, technology, and craft. Every piece is written with care and the belief that the best ideas deserve room to breathe.