1. Interfaces como contrato: o fundamento da arquitetura
Em Java (e não só) uma interface não é apenas um conjunto de métodos. É um contrato: a promessa de que qualquer classe que implemente a interface suporta um determinado comportamento. A interface define o que deve ser implementado, e não como.
Por que isso é importante?
- Separação do código em camadas. Graças às interfaces, podemos separar “o que faz” de “como faz”. Por exemplo, se você tem a interface PaymentService, diferentes implementações podem processar pagamento com cartão de crédito, PayPal ou criptomoeda, mas o código que chama pay() não se preocupa com os detalhes.
- Flexibilidade e extensibilidade. Você pode adicionar uma nova implementação da interface sem alterar o restante do código. Isso é especialmente importante em equipes grandes e projetos de longa duração.
- Testabilidade. Graças às interfaces, é fácil substituir implementações por versões de teste (mocks), sem tocar no código principal.
Exemplo: camada de serviço e DAO
Vamos considerar um exemplo clássico de aplicações de negócio. Suponha que temos uma interface para trabalhar com usuários:
public interface UserRepository {
User findById(int id);
void save(User user);
}
Em diferentes situações podemos implementar essa interface de formas distintas:
- DatabaseUserRepository — armazena usuários em um banco de dados.
- InMemoryUserRepository — armazena usuários em memória (útil para testes).
- FileUserRepository — salva usuários em arquivo.
O código que trabalha com usuários depende apenas da interface:
public class UserService {
private final UserRepository userRepository;
// Injeção de dependência via construtor
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void registerUser(User user) {
userRepository.save(user);
}
}
Agora podemos substituir facilmente a implementação de UserRepository sem alterar o código do serviço.
2. Dependency Injection (injeção de dependências) e o papel das interfaces
Dependency Injection (DI, injeção de dependências) é uma técnica arquitetural em que as dependências (por exemplo, implementações de interfaces) são fornecidas ao objeto de fora, geralmente via construtor ou setter. Isso permite construir aplicações flexíveis, fáceis de testar e estender.
Por que as interfaces são importantes para DI?
Se codificássemos rigidamente a implementação no código, seria difícil substituí-la. Com interfaces, podemos fornecer qualquer implementação sem mudar o código principal.
Exemplo com injeção de dependência
public interface NotificationSender {
void send(String message);
}
public class EmailNotificationSender implements NotificationSender {
@Override
public void send(String message) {
System.out.println("Envio de e-mail: " + message);
}
}
public class SmsNotificationSender implements NotificationSender {
@Override
public void send(String message) {
System.out.println("Envio de SMS: " + message);
}
}
// Classe que usa NotificationSender
public class NotificationService {
private final NotificationSender sender;
public NotificationService(NotificationSender sender) {
this.sender = sender;
}
public void notifyUser(String message) {
sender.send(message);
}
}
Agora você pode testar facilmente o NotificationService, passando, por exemplo, um “stub” em vez de um remetente real de mensagens.
3. Padrões de projeto e interfaces
Interfaces não são apenas sobre arquitetura, mas também sobre padrões de projeto. Muitos padrões não podem ser implementados sem interfaces. Vejamos os mais populares.
Observer (Observador)
Observer — padrão que permite a um objeto (sujeito) notificar outros objetos (observadores) sobre mudanças no seu estado.
Diagrama UML (simplificado):
+------------------+ +------------------------+
| Subject |<------->| Observer |
+------------------+ +------------------------+
| +addObserver() | | +update() |
| +removeObserver()| +------------------------+
| +notifyObservers()|
+------------------+
Exemplo de código:
import java.util.ArrayList;
import java.util.List;
// Interface do observador
public interface Observer {
void update(String event);
}
// Interface do sujeito
public interface Subject {
void addObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers(String event);
}
// Implementação do sujeito
public class NewsAgency implements Subject {
private List<Observer> observers = new ArrayList<>();
@Override
public void addObserver(Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers(String event) {
for (Observer observer : observers) {
observer.update(event);
}
}
}
// Implementação do observador
public class NewsReader implements Observer {
private String name;
public NewsReader(String name) {
this.name = name;
}
@Override
public void update(String event) {
System.out.println(name + " recebeu a notícia: " + event);
}
}
// Classe principal para executar o exemplo
public class ObserverExample {
public static void main(String[] args) {
// Criamos a "agência de notícias" (sujeito)
NewsAgency agency = new NewsAgency();
// Criamos os observadores
Observer alice = new NewsReader("Alice");
Observer bob = new NewsReader("Bob");
// Inscrevemos os observadores nas notícias
agency.addObserver(alice);
agency.addObserver(bob);
// Enviamos uma notícia
agency.notifyObservers("Foi lançada uma nova versão do Java!");
// Removemos um observador e enviamos mais uma notícia
agency.removeObserver(bob);
agency.notifyObservers("Próxima notícia para assinantes");
}
}
Resultado:
Alice recebeu a notícia: Foi lançada uma nova versão do Java!
Bob recebeu a notícia: Foi lançada uma nova versão do Java!
Strategy (Estratégia)
Strategy — padrão que permite escolher o algoritmo de comportamento em tempo de execução, sem alterar o código cliente.
Diagrama UML (simplificado):
+------------------+
| Context |
+------------------+
| -strategy: Strat.|
| +setStrategy() |
| +execute() |
+------------------+
|
v
+------------------+
| Strategy |<-------------------------+
+------------------+ |
| +execute() | |
+------------------+ |
^ |
| |
+------------------+ +------------------+ |
| ConcreteA | | ConcreteB |---+
+------------------+ +------------------+
| +execute() | | +execute() |
+------------------+ +------------------+
Exemplo de código:
public interface PaymentStrategy {
void pay(int amount);
}
public class CreditCardPayment implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("Pagamento de " + amount + " RUB com cartão de crédito");
}
}
public class PaypalPayment implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("Pagamento de " + amount + " RUB via PayPal");
}
}
public class OnlineStore {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout(int amount) {
paymentStrategy.pay(amount);
}
}
// Uso:
OnlineStore store = new OnlineStore();
store.setPaymentStrategy(new CreditCardPayment());
store.checkout(1000);
store.setPaymentStrategy(new PaypalPayment());
store.checkout(500);
Resultado:
Pagamento de 1000 RUB com cartão de crédito
Pagamento de 500 RUB via PayPal
Command (Comando)
Command — padrão que encapsula um pedido como um objeto, permitindo passar ações como parâmetros.
Exemplo de código:
public interface Command {
void execute();
}
public class LightOnCommand implements Command {
@Override
public void execute() {
System.out.println("Luz ligada!");
}
}
public class LightOffCommand implements Command {
@Override
public void execute() {
System.out.println("Luz desligada!");
}
}
public class RemoteControl {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void pressButton() {
command.execute();
}
}
// Uso:
RemoteControl remote = new RemoteControl();
remote.setCommand(new LightOnCommand());
remote.pressButton(); // Luz ligada!
remote.setCommand(new LightOffCommand());
remote.pressButton(); // Luz desligada!
4. Vantagens do uso de interfaces na arquitetura
- Baixo acoplamento (Low Coupling). O código depende apenas da interface, não de uma implementação específica. Isso facilita substituição, teste e extensão.
- Testabilidade. É fácil substituir a implementação real por uma de teste (mock/stub) ao escrever testes de unidade.
- Extensibilidade. É possível adicionar novas implementações da interface sem alterar o código existente — princípio aberto/fechado (OCP).
- Desenvolvimento paralelo. Várias equipes podem implementar partes diferentes do sistema de forma independente, desde que tenham uma interface comum.
- Flexibilidade da arquitetura. É fácil incorporar novos padrões e abordagens.
5. Erros comuns ao usar interfaces na arquitetura
Erro nº 1: Forte acoplamento à implementação.
Se você usa classes concretas em todo lugar, e não interfaces, qualquer mudança de implementação exigirá reescrever o código em muitos pontos. Procure sempre programar “no nível de interfaces”.
Erro nº 2: Interfaces grandes demais (God Interface).
Uma interface deve ser compacta e responsável por uma única área de responsabilidade. Não coloque tudo em uma única interface — caso contrário, a implementação ficará pesada e confusa.
Erro nº 3: Ignorar as vantagens de testabilidade.
Se você não usa interfaces para substituir dependências em testes, seus testes podem se tornar lentos e pouco confiáveis, especialmente se trabalharem com bancos de dados reais ou rede.
Erro nº 4: Várias implementações, mas sem DI.
Se você criou várias implementações da interface, mas codificou rigidamente uma delas no código, perde toda a flexibilidade da arquitetura. Use injeção de dependências (DI)!
GO TO FULL VERSION