DEV Community

Maxwell Anabil
Maxwell Anabil

Posted on

Adapter Pattern em Java: integrando novas transportadoras sem quebrar um sistema legado

Maxwell Anabil Macabeu da Silva

Bacharelado em Engenharia de Software — UniEvangélica

Anápolis — Goiás — Brasil

Resumo

Sistemas de e-commerce dependem de integrações com serviços externos, como gateways de pagamento, plataformas de logística e transportadoras. Um problema recorrente aparece quando cada fornecedor disponibiliza uma API diferente, com métodos, formatos de dados e estruturas de resposta incompatíveis com o sistema existente.

Neste artigo apresento a aplicação do Adapter Pattern, um padrão estrutural do catálogo GoF, em um cenário de integração de transportadoras dentro de um sistema de e-commerce desenvolvido em Java.

A proposta é criar uma interface padrão utilizada pelo sistema e implementar adaptadores responsáveis por traduzir a comunicação com diferentes APIs externas. Dessa forma, o sistema principal não precisa conhecer os detalhes específicos de cada transportadora.

Além da explicação do padrão, será apresentado um estudo de caso, um diagrama UML de classes, uma visão de arquitetura, uma implementação em Java e uma análise das vantagens e dos trade-offs envolvidos.

Palavras-chave: Adapter Pattern; Design Patterns; Java; Engenharia de Software; APIs; Integração de Sistemas; E-commerce.


1. Base Fundamental

1.1 O que são Design Patterns?

Durante o desenvolvimento de software, alguns problemas aparecem repetidamente em diferentes projetos.

Como organizar a criação de objetos? Como permitir que comportamentos sejam alterados sem modificar várias partes do sistema? Como integrar sistemas que possuem interfaces incompatíveis?

Os Design Patterns, ou padrões de projeto, representam soluções conhecidas e reutilizáveis para problemas recorrentes de design de software.

Eles não são códigos prontos que simplesmente copiamos para o projeto. Na realidade, são maneiras de estruturar classes e responsabilidades para resolver determinados tipos de problema.

Os padrões originalmente apresentados pelo chamado Gang of Four (GoF) são geralmente divididos em três grupos:

Categoria Objetivo Exemplos
Criacionais Organizam a criação de objetos Factory Method, Builder, Singleton
Estruturais Organizam como objetos e classes se relacionam Adapter, Decorator, Facade
Comportamentais Organizam responsabilidades e comportamentos Strategy, Observer, Command

O padrão abordado neste artigo é o Adapter Pattern, pertencente à categoria dos padrões estruturais.

1.2 O problema: quando cada API fala uma língua diferente

Imagine um sistema de e-commerce que possui um módulo responsável por calcular o valor do frete de um pedido.

Inicialmente, a empresa utilizava apenas uma transportadora. O código poderia ser semelhante a:

public class ShippingService {

    private LegacyCarrierClient client;

    public BigDecimal calculateShipping(
            String originZip,
            String destinationZip,
            int weightInGrams) {

        return client.calculate(
                originZip,
                destinationZip,
                weightInGrams
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Enquanto existe apenas uma integração, essa estrutura parece suficiente.

O problema aparece quando a empresa decide oferecer mais opções de entrega. Uma segunda transportadora pode possuir algo semelhante a:

fastShipClient.getRate(
    new FastShipPayload(
        origin,
        destination,
        weightInKg
    )
);
Enter fullscreen mode Exit fullscreen mode

Observe que as duas APIs não trabalham da mesma maneira.

Uma pode utilizar peso em gramas, parâmetros separados e valor retornado em centavos. Outra pode utilizar peso em quilogramas, um objeto de requisição e valor retornado como BigDecimal.

O sistema começa então a acumular verificações:

if (carrier.equals("LEGACY")) {

    // chama API da primeira transportadora

} else if (carrier.equals("FAST_SHIP")) {

    // converte peso
    // cria payload
    // chama outra API
    // converte resposta

} else if (carrier.equals("OUTRA_TRANSPORTADORA")) {

    // outra implementação
}
Enter fullscreen mode Exit fullscreen mode

Quanto mais integrações surgem, maior fica esse código.

Além disso, o ShippingService passa a conhecer detalhes que não deveriam fazer parte de sua responsabilidade, como formato de requisição de cada fornecedor, unidades de medida, nomes de métodos externos, estruturas de resposta e tratamento específico de cada API.

É exatamente nesse tipo de situação que o Adapter Pattern pode ajudar.

1.3 Como funciona o Adapter Pattern?

A palavra adapter significa adaptador.

Uma analogia simples é um adaptador de tomada. Você possui um dispositivo com um padrão de tomada, mas chega a um local onde a tomada possui outro formato. Em vez de modificar o aparelho ou a instalação elétrica, coloca um adaptador entre os dois.

No desenvolvimento de software, a ideia é semelhante:

Sistema existente
       ↓
Interface conhecida pelo sistema
       ↓
Adapter
       ↓
API externa incompatível
Enter fullscreen mode Exit fullscreen mode

O Adapter recebe uma chamada utilizando o formato esperado pelo sistema e realiza a tradução necessária para o formato exigido pelo serviço externo.

Normalmente temos três participantes principais:

  • Target: interface que o sistema gostaria de utilizar.
  • Adapter: implementa essa interface e converte a chamada.
  • Adaptee: classe ou API existente que possui uma interface incompatível.

No nosso exemplo:

Target
ShippingProvider

Adapters
LegacyCarrierAdapter
FastShipAdapter

Adaptees
LegacyCarrierClient
FastShipClient
Enter fullscreen mode Exit fullscreen mode

Assim, o restante do sistema trabalha apenas com ShippingProvider e não precisa saber qual API realmente está por trás daquele objeto.

1.4 Adapter não é Strategy

Os dois padrões podem parecer semelhantes porque ambos utilizam interfaces e composição. Porém, resolvem problemas diferentes.

O Strategy Pattern normalmente é utilizado quando existem diferentes algoritmos capazes de executar uma mesma tarefa.

Já o Adapter Pattern é utilizado principalmente quando queremos tornar interfaces incompatíveis compatíveis.

No nosso cenário, nossa aplicação espera:

quote(ShippingRequest request)
Enter fullscreen mode Exit fullscreen mode

mas uma API externa oferece:

calculate(originZip, destinationZip, weightGrams)
Enter fullscreen mode Exit fullscreen mode

e outra oferece:

getRate(FastShipPayload payload)
Enter fullscreen mode Exit fullscreen mode

O Adapter cria uma camada de tradução entre essas interfaces.


2. Desenvolvimento

2.1 Cenário do Mundo Real

Considere um e-commerce que vende produtos para diferentes regiões e precisa calcular o frete durante o checkout.

O fluxo é aproximadamente:

Cliente seleciona os produtos
          ↓
Informa o endereço
          ↓
Sistema solicita cotações
          ↓
Transportadoras calculam o frete
          ↓
Sistema apresenta preço e prazo
          ↓
Cliente escolhe a opção
Enter fullscreen mode Exit fullscreen mode

Inicialmente, o sistema possuía integração apenas com uma transportadora. Com o crescimento da operação, surgiu a necessidade de conectar novos provedores logísticos.

Porém, cada fornecedor possuía sua própria API.

Um deles trabalhava com peso em gramas, enquanto outro utilizava quilogramas. Um retornava valores em centavos, enquanto outro retornava BigDecimal. Um utilizava vários parâmetros no método, enquanto outro esperava um objeto de requisição.

Alterar o ShippingService sempre que uma nova integração fosse adicionada faria com que o serviço se tornasse cada vez mais acoplado às APIs externas.

A solução escolhida é definir uma interface comum chamada ShippingProvider. Cada API externa recebe então seu próprio Adapter.

2.2 Diagrama UML de Classes

Diagrama UML do Adapter Pattern aplicado ao serviço de fretes

Figura 1 — Diagrama UML do Adapter Pattern aplicado ao serviço de fretes.

O ponto mais importante do diagrama é observar que o ShippingService depende apenas de ShippingProvider.

Ele não conhece diretamente LegacyCarrierClient nem FastShipClient. Esses detalhes ficam escondidos dentro dos adapters.

2.3 Arquitetura de Software

Arquitetura de alto nível do sistema de cotação de fretes

Figura 2 — Arquitetura de alto nível do sistema de cotação de fretes.

O cliente não entra em contato diretamente com as transportadoras.

O Order Service solicita ao Shipping Service uma cotação. Dentro do Shipping Service, os adapters ficam responsáveis por conversar com cada API externa.

Essa separação é importante porque uma alteração realizada por uma transportadora afeta principalmente seu Adapter, em vez de espalhar modificações pelo restante da aplicação.

2.4 Implementação em Java

2.4.1 Representando a solicitação de frete

import java.math.BigDecimal;
import java.util.Objects;

public record ShippingRequest(
        String originZip,
        String destinationZip,
        BigDecimal weightKg
) {

    public ShippingRequest {

        Objects.requireNonNull(originZip);
        Objects.requireNonNull(destinationZip);
        Objects.requireNonNull(weightKg);

        if (weightKg.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException(
                    "O peso deve ser maior que zero."
            );
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Independentemente da transportadora utilizada, nosso sistema trabalhará internamente com peso em quilogramas.

2.4.2 Representando a resposta

import java.math.BigDecimal;

public record ShippingQuote(
        String provider,
        BigDecimal price,
        int deliveryDays
) {
}
Enter fullscreen mode Exit fullscreen mode

Agora qualquer transportadora utilizada pelo sistema deverá fornecer nome do provedor, preço e prazo de entrega.

2.4.3 Criando a interface padrão

Este é o nosso Target:

public interface ShippingProvider {

    ShippingQuote quote(ShippingRequest request);

}
Enter fullscreen mode Exit fullscreen mode

O ShippingService conhecerá somente essa interface.

Ele não precisa conhecer nenhuma API externa diretamente.

2.4.4 API da primeira transportadora

Imagine que uma integração antiga utiliza esta interface:

public interface LegacyCarrierClient {

    LegacyCarrierResponse calculate(
            String originZip,
            String destinationZip,
            int weightInGrams
    );

}
Enter fullscreen mode Exit fullscreen mode

E retorna:

public record LegacyCarrierResponse(
        long priceInCents,
        int estimatedDays
) {
}
Enter fullscreen mode Exit fullscreen mode

Nosso sistema trabalha com quilogramas e BigDecimal, enquanto essa integração utiliza gramas e centavos.

As interfaces são incompatíveis.

2.4.5 Criando o primeiro Adapter

É aqui que entra o padrão:

import java.math.BigDecimal;

public class LegacyCarrierAdapter
        implements ShippingProvider {

    private final LegacyCarrierClient client;

    public LegacyCarrierAdapter(
            LegacyCarrierClient client
    ) {
        this.client = client;
    }

    @Override
    public ShippingQuote quote(
            ShippingRequest request
    ) {

        int weightInGrams = request
                .weightKg()
                .multiply(BigDecimal.valueOf(1000))
                .intValueExact();

        LegacyCarrierResponse response =
                client.calculate(
                        request.originZip(),
                        request.destinationZip(),
                        weightInGrams
                );

        BigDecimal price = BigDecimal
                .valueOf(response.priceInCents())
                .movePointLeft(2);

        return new ShippingQuote(
                "Legacy Carrier",
                price,
                response.estimatedDays()
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Perceba que toda a incompatibilidade ficou dentro dessa classe.

O Adapter realiza as conversões necessárias e transforma a resposta externa em um ShippingQuote.

O restante do sistema não precisa conhecer nenhuma dessas conversões.

2.4.6 Integrando uma segunda transportadora

Agora imagine uma API mais moderna:

public interface FastShipClient {

    FastShipRate getRate(
            FastShipPayload payload
    );

}
Enter fullscreen mode Exit fullscreen mode

Seu payload:

import java.math.BigDecimal;

public record FastShipPayload(
        String from,
        String to,
        BigDecimal weight
) {
}
Enter fullscreen mode Exit fullscreen mode

E sua resposta:

import java.math.BigDecimal;

public record FastShipRate(
        BigDecimal amount,
        int businessDays
) {
}
Enter fullscreen mode Exit fullscreen mode

Novamente, podemos criar um Adapter:

public class FastShipAdapter
        implements ShippingProvider {

    private final FastShipClient client;

    public FastShipAdapter(
            FastShipClient client
    ) {
        this.client = client;
    }

    @Override
    public ShippingQuote quote(
            ShippingRequest request
    ) {

        FastShipPayload payload =
                new FastShipPayload(
                        request.originZip(),
                        request.destinationZip(),
                        request.weightKg()
                );

        FastShipRate response =
                client.getRate(payload);

        return new ShippingQuote(
                "Fast Ship",
                response.amount(),
                response.businessDays()
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Apesar de as duas APIs externas serem diferentes, para o restante do sistema elas agora possuem exatamente o mesmo contrato:

ShippingQuote quote(ShippingRequest request);
Enter fullscreen mode Exit fullscreen mode

Esse é o principal benefício do Adapter Pattern.

2.4.7 O ShippingService

Agora podemos criar um serviço que recebe diversas implementações de ShippingProvider:

import java.util.Comparator;
import java.util.List;

public class ShippingService {

    private final List<ShippingProvider> providers;

    public ShippingService(
            List<ShippingProvider> providers
    ) {
        this.providers = providers;
    }

    public ShippingQuote bestQuote(
            ShippingRequest request
    ) {

        return providers.stream()
                .map(provider ->
                        provider.quote(request)
                )
                .min(
                    Comparator.comparing(
                        ShippingQuote::price
                    )
                )
                .orElseThrow(() ->
                    new IllegalStateException(
                        "Nenhuma transportadora disponível."
                    )
                );
    }
}
Enter fullscreen mode Exit fullscreen mode

Observe que não existe um if ou switch para tratar cada transportadora.

O serviço simplesmente sabe que possui objetos capazes de executar quote(request).

A implementação específica deixa de ser responsabilidade dele.

2.4.8 Utilizando o serviço

Um exemplo simplificado seria:

import java.math.BigDecimal;
import java.util.List;

public class CheckoutDemo {

    public static void main(String[] args) {

        ShippingProvider legacyProvider =
                new LegacyCarrierAdapter(
                        new LegacyCarrierClientImpl()
                );

        ShippingProvider fastShipProvider =
                new FastShipAdapter(
                        new FastShipClientImpl()
                );

        ShippingService shippingService =
                new ShippingService(
                        List.of(
                                legacyProvider,
                                fastShipProvider
                        )
                );

        ShippingRequest request =
                new ShippingRequest(
                        "75000-000",
                        "74000-000",
                        new BigDecimal("2.5")
                );

        ShippingQuote best =
                shippingService.bestQuote(request);

        System.out.println(
                "Transportadora: " +
                best.provider()
        );

        System.out.println(
                "Frete: R$ " +
                best.price()
        );

        System.out.println(
                "Prazo: " +
                best.deliveryDays() +
                " dias"
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Se uma terceira transportadora for adicionada posteriormente, podemos criar outra implementação de ShippingProvider e registrá-la no serviço, sem reescrever a lógica de cálculo existente.

2.5 Testabilidade

Outra vantagem dessa estrutura aparece nos testes.

Podemos testar cada Adapter isoladamente:

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

import java.math.BigDecimal;

import org.junit.jupiter.api.Test;

class LegacyCarrierAdapterTest {

    @Test
    void deveConverterQuilogramasParaGramas() {

        LegacyCarrierClient client =
                mock(LegacyCarrierClient.class);

        when(
            client.calculate(
                "75000-000",
                "74000-000",
                2500
            )
        ).thenReturn(
            new LegacyCarrierResponse(
                1890,
                4
            )
        );

        LegacyCarrierAdapter adapter =
                new LegacyCarrierAdapter(client);

        ShippingRequest request =
                new ShippingRequest(
                    "75000-000",
                    "74000-000",
                    new BigDecimal("2.5")
                );

        ShippingQuote quote =
                adapter.quote(request);

        assertEquals(
                new BigDecimal("18.90"),
                quote.price()
        );

        assertEquals(
                4,
                quote.deliveryDays()
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

O teste consegue verificar a lógica do Adapter sem precisar executar todo o checkout.

Isso facilita encontrar erros em conversões e integrações.

2.6 Boas práticas aplicadas

Além do Adapter Pattern, algumas decisões ajudam a manter o projeto organizado.

A aplicação utiliza injeção de dependência, pois os clientes das transportadoras são recebidos pelo construtor dos Adapters em vez de serem criados internamente.

Também existe uma separação entre os objetos do domínio e os objetos utilizados pelas APIs externas.

Por exemplo, ShippingRequest pertence à aplicação, enquanto FastShipPayload pertence à integração externa.

Essa separação evita que alterações realizadas por uma transportadora contaminem diretamente os modelos internos do sistema.

Outro ponto importante é o uso de BigDecimal para representar valores monetários, evitando problemas comuns de precisão que podem ocorrer com tipos de ponto flutuante.

2.7 Prós e Contras do Adapter Pattern

Como qualquer decisão arquitetural, o Adapter Pattern possui benefícios e custos.

Vantagens

  • Reduz o acoplamento com bibliotecas e APIs externas.
  • Permite manter uma interface consistente dentro da aplicação.
  • Facilita a substituição de fornecedores.
  • Melhora a testabilidade das integrações.
  • Concentra conversões e adaptações em classes específicas.
  • Evita espalhar detalhes de APIs externas pelo sistema.
  • Facilita adicionar novas integrações.
  • Favorece o princípio de responsabilidade única.

Desvantagens e trade-offs

  • Aumenta o número de classes do projeto.
  • Cada API externa exige seu próprio Adapter.
  • Mudanças frequentes no fornecedor podem exigir manutenção constante do adaptador.
  • Em sistemas muito pequenos, a abstração pode ser desnecessária.
  • Uma má implementação pode transformar o Adapter em uma classe com responsabilidades demais.
  • O padrão não elimina a complexidade da integração: ele organiza e isola essa complexidade.

Portanto, utilizar Adapter para qualquer integração simples também pode gerar complexidade desnecessária.

O padrão faz mais sentido quando a interface externa realmente é incompatível com o modelo utilizado pelo restante da aplicação.


3. Conclusão

3.1 Resumo

O Adapter Pattern apresenta uma solução simples para um problema comum na engenharia de software: integrar componentes que possuem interfaces diferentes.

No cenário apresentado, cada transportadora possuía sua própria forma de calcular frete.

Sem uma camada de adaptação, o ShippingService precisaria conhecer os detalhes de cada API, criando alto acoplamento e dificultando a manutenção do código.

Com a criação da interface ShippingProvider, o sistema passou a trabalhar com um único contrato.

As classes LegacyCarrierAdapter e FastShipAdapter passaram a ser responsáveis por traduzir a comunicação entre o domínio da aplicação e os serviços externos.

O resultado é uma arquitetura mais desacoplada, extensível e testável.

3.2 Visão Pessoal

Ao estudar o Adapter Pattern, o que mais me chamou atenção foi perceber que um Design Pattern não precisa obrigatoriamente envolver uma solução extremamente complexa.

A ideia do Adapter é relativamente simples: criar uma camada de tradução entre duas interfaces incompatíveis.

Mesmo sendo simples, essa decisão pode ter um impacto grande na organização de um projeto.

Antes de estudar esse padrão, seria natural pensar em adaptar cada nova API diretamente dentro do serviço principal utilizando condicionais e conversões.

Porém, com o Adapter, fica mais claro que os detalhes de integração pertencem a uma classe específica.

Também percebi que padrões de projeto não devem ser aplicados apenas porque existem. Eles precisam resolver um problema real.

Se o sistema possuir apenas uma integração simples e que dificilmente será alterada, talvez criar vários níveis de abstração não seja necessário.

Por outro lado, quando diferentes APIs precisam conviver dentro de um mesmo sistema, o Adapter pode reduzir bastante o acoplamento e tornar futuras alterações mais seguras.

Para mim, esse foi um dos principais aprendizados: Design Patterns não servem apenas para deixar o código mais sofisticado, mas para tornar determinadas mudanças mais fáceis de realizar.

3.3 E você?

Você já precisou integrar uma API que possuía uma estrutura completamente diferente da utilizada pelo seu sistema?

Usaria o Adapter Pattern nesse cenário ou escolheria outra abordagem?

Compartilhe sua opinião nos comentários.


Referências

GAMMA, Erich; HELM, Richard; JOHNSON, Ralph; VLISSIDES, John. Design Patterns: Elements of Reusable Object-Oriented Software. Boston: Addison-Wesley, 1994.

FOWLER, Martin. Refactoring: Improving the Design of Existing Code. 2. ed. Boston: Addison-Wesley, 2018.

FREEMAN, Eric; ROBSON, Elisabeth. Head First Design Patterns. 2. ed. Sebastopol: O'Reilly Media, 2020.

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.