Categorias
Software Architecture Software Development

Criar ou não criar uma Interface?

Então você faz amplo uso das Interfaces, e mesmo assim o sistema continua amarrado, engessado. O que acontece? Veja os casos em que Interfaces são realmente úteis, e como aplicar.

É frequente ver por aí a recomendação genérica de usar Interfaces como uma forma de reduzir o acoplamento e possibilitar o uso da Inversão de Dependência, ou mesmo a Injeção de Dependência.

De repente, quase tudo no sistema é uma Interface. Mesmo assim, o sistema fica altamente acoplado e as dependências podem até ser injetadas, mas certamente não são invertidas.

O fato é que você pode acabar criando acoplamento mesmo com Interfaces, ex. ao injetar uma Interface de banco de dados numa classe de domínio. E este tipo de dependência não faz o menor sentido.

A verdade é que interfaces não são um pré-requisito para alcançar estes objetivos.

Então, quando é que vale a pena usar Interfaces? Veremos que há dois casos bem específicos:

1. Quando a Interface representa uma abstração de verdade

E quando eu digo “real” é porque você não precisa de uma Interface para ter uma abstração. Qualquer classe é uma abstração de algum conceito.

Uma classe Producté uma abstração do produto vendido num e-commerce, mapeando somente os atributos e operações relevantes para atendimento dos requisitos do projeto.

A pergunta, então, é quando uma abstração é do tipo certo para ser representada por uma Interface no software, de uma forma que traga benefícios reais.

“Smartphone” é uma abstração, e cada modelo é uma implementação.

Pistas de que uma Interface representa uma abstração real e relevante:

  • Ela tem mais de uma implementação;
  • Você consegue usar a Interface em composições, como um Decorator Pattern (mais sobre isso adiante);
  • O princípio LSP (Liskov Substitution Principle) é atendido por todas as implementações da Interface.

2. Nas fronteiras físicas de comunicação entre sistemas

Outro caso em que a Interface é relevante é quando ela aparece na fronteira física entre dois sistemas, separando completamente o sistema das preocupações de infraestrutura, tais como armazenamento (arquivos e banco de dados), e rede (https e outros). Em outras palavras, quando você tem IO.

Aqui nós aplicamos o princípio da Inversão de Dependência do SOLID: supondo um Controller, você faz com que ele dependa de uma Interface que abstrai o banco de dados ou repositório, ao invés de depender diretamente de implementações específicas destes recursos.

Novamente, isso traz a vantagem de poder envolver a implementação com um LoggingDecorator ou CacheDecorator.

Analogia com uma empresa

Pense numa empresa qualquer, na empresa onde você trabalha. O que torna essa empresa única são as pessoas, a cultura, a sua forma de fazer as coisas. Não o prédio, as cadeiras, ou o provedor de internet que ela contrata.

A primeira parte é intrínseca. A empresa pode evoluir, crescer, mas não pode simplesmente trocar uma cultura por outra da noite para o dia. Falando de software, o mesmo se aplica para entidades de domínio e regras de negócio, na maioria dos casos.

A segunda parte é um detalhe. É necessário, mas a empresa pode se mudar de um prédio para outro, ou trocar todas as suas cadeiras, pois mesmo assim ela permanece a mesma em sua essência. A propósito, isto é LSP (Liskov). Em software, a mesma ideia vale para um repositório ou serviço de envio de emails.

Quando o uso de Interfaces realmente se paga

Se uma Interface não tem um benefício claro e real, ela é somente um código inútil. Remova.

Dois exemplos de casos com benefícios reais:

Examplo 1: MediatR

Como informando em sua Github Page, esta é uma biblioteca feita para viabilizar a troca de mensagens entre partes de um mesmo sistema, como request/response, commands, queries, notifications e eventos.

Uma de suas aplicações seria despachar efeitos colaterais em resposta a eventos de domínio, tais como enviar um email de confirmação logo após a inclusão de um pedido.

Para fazer isso, você pode usar a Interface INotification para definir o evento:

public class OrderPlaced : INotification
{
    public Guid OrderId { get; set; }
}

E um handler para capturar este evento e enviar o email:

public class SendConfirmationEmailWhenOrderIsPlaced : INotificationHandler<OrderPlaced>
{
    public Task Handle(OrderPlaced notification, CancellationToken cancellationToken)
    {
        var message = BuildOrderConfirmationMessage(notification.OrderId);
        emailSender.Send(message);
    }
}

É importante notar que você pode ter quantos handlers quiser para este tipo de notificação. Aqui estão alguns exemplos de handlers que você poderia adicionar neste caso específico, dependendo (como sempre) das suas regras de negócio:

  • Atualizar o estoque do produto
  • Notificar a logística para que envie o produto
  • Verificar se o cliente é elegível para uma promoção com base no total que ele gastou nos últimos 30 dias

Além disso, note que cada um desses handlers seria uma classe separada, com lógica que é tecnicamente desacoplada do código que originou o evento, o que é lindo! Código assim é muito fácil de ler e entender, o que o torna também muito fácil de manter.

Mas, voltando ao tópico, uma peça está faltando, que é a ponte que conecta o evento OrderPlaced com todos estes handlers, certo?

Bom, você precisa injetar uma implementação da Interface IMediator no seu Controller ou Service, para que você possa despachar o evento pelo universo afora:

public class OrderService
{
	private readonly IDatabase database;
	private readonly IMediator mediator;
	public OrderService(IDatabase database, IMediator mediator)
	{
		this.database = database;
		this.mediator = mediator;
	}
	public async Task PlaceOrder(OrderDetails details)
	{
		var order = details.AsDomainModel();
		database.Orders.Add(order);
		database.SaveChanges();
		var event = new OrderPlaced { OrderId = order.Id };
		await mediator.Send(event);
	}
}

Por fim, como é que o mediator sabe que tem que enviar este evento a todos os handlers que estão esperando por ele?

Você só precisa de uma linha de código para escanear o seu código automaticamente e detectar todos os handlers disponíveis para cada tipo de evento (INotification). A implementação específica depende do tipo de Container de DI (Dependency Injection) que você está usando, e  aqui está uma lista de exemplos para todos o Containers mais utilizados.

Uma observação muito importante é que todas as implementações de INotificationHandler<OrderPlaced> são abstrações reais:

  • Várias implementações
  • Todas as implementações obedecem a LSP, já que você pode adicionar ou remover quantas implementações quiser sem alterar o correto funcionamento do sistema (desde que essas implementações representem requisitos correspondentes que tenham sido adicionados ou removidos também)

Examplo 2: Composição com o Decorator Pattern

Suponha que você tenha um ProductRepository e agora você quer adicionar cache a ele. Para simplificar, nossa Interface vai definir apenas um método.

public interface IProductRepository
{
    ReadOnlyCollection<ProductDto> ListProductsByCategory(int categoryId);
}

public class ProductRepository : IProductRepository
{
    public ReadOnlyCollection<ProductDto> ListProductsByCategory(int categoryId)
    {
        var sql = "SELECT * FROM Product WHERE CategoryId = @categoryId";
        var products = connection.Query<ProductDto>(sql, new { categoryId });
        return products.ToList().AsReadOnly();
    }
}

Você pode pensar em implementar o cache diretamente dentro da classe, mas isso violaria dois princípios do SOLID: o Single Responsibility e o Open/Closed.

Decorator Pattern ao resgate.

Nós só precisamos adicionar uma outra implementação de IProductRepository que será unicamente responsável pelo cache. Assim:

public class CachingProductRepository : IProductRepository
{
    private readonly IProductRepository repository;
    private readonly ICache cache;

    public CachingProductRepository(IProductRepository repository, ICache cache)
    {
        this.repository = repository;
        this.cache = cache;
    }

    public ReadOnlyCollection<ProductDto> ListProductsByCategory(int categoryId)
    {
        var key = $"{nameof(ListProductsByCategory)}_{categoryId}";
        if (!cache.ContainsKey(key))
        {
            var products = repository.ListProductsByCategory(categoryId);
            cache.Add(key, products);
        }
        return cache.Get<ReadOnlyCollection<ProductDto>>(key);
    }
}

E aí está: cache implementado sem tocar o código do repositório.

E você poderia continuar. Se agora você tem que implementar log de exceções, vou adicionar uma outra implementação que tomará conta especificamente disso.

Desde que essas implementações extras recebam outro IProductRepositoryem seus construtores, você pode sair criando composições com qualquer quantidade de implementações de IProductRepository. Claro, você precisa se lembrar de amarrar essas classes onde você faz a configuração da sua injeção de dependências.

Esta técnica fica melhor ainda quando você está aplicando o CQRS, já que você compõe diretamente com a Interface genérica IQuery e adiciona logging + caching para todas as queries de uma vez só na sua configuração de injeção de dependência.

Conclusão

Interfaces são muito poderosas e podem abrir espaço para técnicas igualmente poderosas, mas você não deveria sair criando Interfaces para toda e qualquer “abstração” sem pensar, só por fazer.

Use as Interfaces quando você tiver um objetivo claro em mente, pois do contrário você estará poluindo seu código e vai estar apenas usando sem sentido ferramentas valiosas.

Por Phillippe Santana

Apaixonado por escrever código que as pessoas possam entender. Sou um desenvolvedor de software, gerente de projetos, empreendedor e entusiasta de pessoas/cultura. Me adicione no [Linkedin](https://www.linkedin.com/in/phillippesantana/) e no [Medium](https://medium.com/@phillippesantana).