CodeGym /Cursos /C# SELF /Contratos de interface

Contratos de interface

C# SELF
Nível 23 , Lição 3
Disponível

1. Introdução

Então, já deu pra ver que interfaces são contratos poderosos. Mas o que é programar no nível das interfaces? É uma filosofia de design que diz: "Dependa de abstrações, não de implementações concretas."

Bora pra uma analogia. Tu quer fazer café. Tu compra uma cafeteira. Não importa se é o modelo específico "Super-Puper-Machine V3000" ou "Mega-Automático Barista 5000", né? O que importa é que o aparelho sabe "fazer café" – ou seja, implementa o "contrato" ICoffeeMaker. Tu só aperta o botão "Fazer café" e pronto. Como ela faz – com pó, cápsula ou sei lá o quê – tu, como usuário, nem liga.

No código, isso significa que ao invés de pedir um tipo específico tipo Document ou Image nos teus métodos, tu vai pedir IPrintable. Teu código vai funcionar com qualquer objeto que implemente essa interface, sem saber o tipo concreto dele.

Contrato de interface — é a garantia de que toda classe que implementa a interface vai obrigatoriamente fornecer as definições de todos os membros da interface (métodos, propriedades, eventos). Do ponto de vista do programa, qualquer objeto que implemente a interface oferece um "conjunto de funcionalidades", e tu pode confiar nisso, mesmo sem saber como tá implementado por dentro.

Tipo, tu combinou com teu colega de sempre se encontrar no sofá vermelho do hall. Como ele chega lá — de elevador, a pé ou correndo pela parede — tanto faz. O importante: ele vai estar lá. Interface é tipo esse "sofá vermelho" pro código.

Exemplo


public interface IPrintable
{
    void Print();
}

Contrato: qualquer um que implemente IPrintable tem que saber se imprimir na tela. Como faz isso — aí já é detalhe.

2. Contrato como ponto de interação entre partes do sistema

Como isso funciona na prática

Um sistema pode ter dezenas de classes, criadas por pessoas diferentes, em épocas diferentes, com objetivos diferentes. Mas se todas seguem o mesmo contrato (ou seja, implementam a mesma interface), dá pra usar tudo do mesmo jeito.

Exemplo de app real: imagina um banco de clientes. Classe Customer, classe Employee, classe Contractor. Cada uma guarda info do seu jeito, mas se todas implementam, por exemplo, a interface IContactInfo, que exige os métodos GetEmail() e GetPhoneNumber(), pro código de fora não importa o tipo do objeto — o importante é que dá pra pegar email e telefone.


public interface IContactInfo
{
    string GetEmail();
    string GetPhoneNumber();
}

public class Customer : IContactInfo
{
    public string Email { get; set; }
    public string Phone { get; set; }

    public string GetEmail() => Email;
    public string GetPhoneNumber() => Phone;
}

// Igualzinho pra Employee e Contractor...

Agora, se tu precisa imprimir os dados de todo mundo que tem contato com tua empresa (não importa quem seja), é só passar por uma lista de IContactInfo, chamar os métodos — e pronto.

3. Programando "no nível das interfaces"

Programar no nível das interfaces — é escrever código que depende só das interfaces (contratos), não das classes concretas. A classe que implementa a interface pode ser qualquer uma, desde que cumpra o contrato.

Por que isso é importante?

  • Escalabilidade: fácil adicionar novos tipos sem mexer no código que já existe.
  • Testabilidade: dá pra trocar objetos por mocks (fakes) nos testes, de boa.
  • Flexibilidade: a implementação pode mudar todo dia, a interface fica estável.
  • Arquitetura limpa: teus módulos ficam pouco acoplados, dá pra reaproveitar fácil.

Exemplo — Programa conhecido: "Conta bancária"

Nos exemplos anteriores, a gente tinha uma classe abstrata BankAccount com o método abstrato Withdraw(), e tipos concretos (SavingsAccount, CheckingAccount) faziam o resto.

Agora bora adicionar uma interface — tipo, pra mostrar info do saldo:


public interface IBalanceReporter
{
    void ReportBalance();
}

public abstract class BankAccount : IBalanceReporter
{
    public double Balance { get; set; }

    public abstract void Withdraw(double amount);

    public void ReportBalance()
    {
        Console.WriteLine($"Saldo atual: {Balance} euros");
    }
}

Agora todo mundo que trabalha com IBalanceReporter pode chamar ReportBalance() sem se importar com o tipo da conta.

4. Processamento geral e polimorfismo via contrato

Exemplo: processador geral

Quando a gente trabalha com interface, dá pra criar métodos genéricos que não dependem do tipo do objeto:


static void PrintAllBalances(IBalanceReporter[] accounts)
{
    foreach (var reporter in accounts)
    {
        reporter.ReportBalance();
    }
}

Nessa lista pode ter qualquer objeto que implemente IBalanceReporter: SavingsAccount, CheckingAccount, até um MockAccountForTesting. E não tem mágica, funciona de boa.

Esquemático: o que o contrato de interface traz


+-------------------+      implementa     +-------------------+
|   BankAccount     |  <---------------- |  IBalanceReporter |
|  (SavingsAccount) |                    |-------------------|
|  (CheckingAccount)|                    | + ReportBalance() |
+-------------------+                    +-------------------+
         |                                   ^
         |                                   |
         +-----------+-----------------------+
                     |
        Qualquer outra classe que implemente o contrato
Esquema: contrato de interface como ponto de interação

5. Contratos, lógica de negócio e arquitetura

O contrato de interface separa claramente o o que a classe tem que fazer do como ela faz. Por isso a galera de arquitetura de software curte desenhar a lógica dos sistemas por interfaces. Muitas vezes o contrato (interface) é feito primeiro, e a implementação vem depois.

Exemplo da vida real: sistemas de pagamento

Carteiras digitais, cartões, PayPal, cripto... Cada um tem seus detalhes, mas se tu abstrair e criar uma interface IPaymentProvider:


public interface IPaymentProvider
{
    void Pay(decimal amount);
    bool Refund(decimal amount);
}

O código que usa essa interface nem liga se tá pagando com cartão ou conta. Isso é bom pra arquitetura e pra vida: dá pra adicionar novos sistemas de pagamento sem mexer no resto do código.

6. Extraindo lógica de negócio pro contrato

O contrato permite jogar as regras principais de negócio pra interface, e deixar os detalhes (tipo checar limite, dar cashback, etc.) pra cada classe concreta.

Mais um exemplo


public interface ILogger
{
    void LogInfo(string message);
    void LogError(string message);
}

public class ConsoleLogger : ILogger
{
    public void LogInfo(string message)  => Console.WriteLine($"INFO: {message}");
    public void LogError(string message) => Console.WriteLine($"ERRO: {message}");
}

public class FileLogger : ILogger
{
    public void LogInfo(string message)  => /* Escrever no arquivo */;
    public void LogError(string message) => /* Escrever no arquivo */;
}

Depois o código do cliente fica assim (não importa qual logger tá usando):


void DoWork(ILogger logger)
{
    logger.LogInfo("Trabalho começou.");
    // ... algum trabalho ...
    logger.LogError("Deu ruim no trabalho.");
}

Isso aí é programar no nível das interfaces: o código do cliente depende só do contrato (interface), não da implementação concreta.

7. Erros e armadilhas comuns

Quem tá começando erra muito nisso: o código fica preso em classes concretas, não em interfaces. Isso dá vários problemas:

  • Difícil trocar ou testar algo — tem que mexer em muito código.
  • Adicionar novos tipos só mudando um monte de lugar.
  • Os módulos ficam muito "grudados" um no outro.

O contrário — se tu já começa o sistema usando interfaces e passando parâmetros por elas, tu consegue baixo acoplamento e flexibilidade.

Dica principal: tenta sempre pensar na interação entre módulos como interação entre contratos, não entre implementações concretas.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION