1. Adapter (Adaptador)
1.1. Definição
📘"O padrão adapter converte a interface de uma classe em outra interface que os clientes esperam. Um adaptador permite que classes anteriormente incompatíveis possam trabalhar em conjunto". [UCPP]
1.2. Aplicabilidade
Um dos casos onde o adapter é amplamente aplicado é quando utiliza-se um determinado serviço web, framework ou biblioteca (que chamaremos de componente) onde existem diferentes implementações destes, cada uma funcionando de uma forma diferente. Logo, o código necessário para usar um determinado componente seria diferente de outro. Normalmente vão existir vários métodos com nomes e parâmetros diferentes (assinaturas). Assim, trocar tal componente por outro pode ser um trabalho complexo.
Um adapter neste caso pode fornecer uma interface padrão a ser utilizada publicamente, encapsulando as diferenças entre as diferentes implementações dos componentes e permitindo a substituição de um componente por outro.
Você também pode precisar fornecer um componente existente para ser utilizado por uma outra empresa, mas seu componente tem uma interface pública diferente do que tal empresa exige. Isto ocorre por exemplo, em sistemas de pagamento online disponibilizados por diversas empresas. Para que o seu sistema seja notificado se um pagamento foi confirmado, ele precisa fornecer um serviço com uma determinada interface. Assim, o sistema de pagamento interage com tal interface para notificar o seu sistema da confirmação do pagamento de um cliente. Alguns exemplos incluem o serviço do PagSeguro e PayPal.
1.3. Modelagem do Padrão
Podemos ter 1 ou mais classes que precisam ser adaptadas, como as classes AdaptadaAbc
e AdaptadaXyz
.
Cada uma dessas classes fornece uma implementação diferente de uma determinada funcionalidade.
Por exemplo, cada classe pode representar o acesso a um determinado serviço na web,
como obtenção da previsão do tempo. Cada uma obtém a previsão de um serviço (site) diferente.
Podemos ter vários métodos diferentes que fornecem as mesmas funcionalidades, como obtenção da previsão para uma determinada data, média de temperatura para um determinado mês do ano, umidade relativa do ar, etc. No entanto, os métodos, parâmetros e tipos em cada uma dessas classes (como possivelmente foram implementadas por pessoas/equipes distintas), serão diferentes.
Para padronizarmos tais métodos e assim tornar idêntica a forma de usar qualquer uma dessas classes, devemos implementar um adaptador para cada classe a ser adaptada. No diagrama de exemplo, a classe AdaptadaAbc
possui os métodos metodoA()
e metodoB()
. Já a classe AdaptadaXyz
possui os métodos metodoX()
e metodoY()
. A padronização pode ser feita pela criação de uma interface comum (Alvo
) que será implementada por cada adaptador. O nome de cada classe Adapter normalmente é formado pelo nome da classe a ser adaptada, seguido da palavra Adapter
. Assim, temos as classes AbcAdapter
e XyzAdapter
para o modelo apresentado. Uma classe AbcAdapter
que implementa a interface Alvo
é um adaptador para a classe AdaptadaAbc
. Em outras palavras, ela adapta objetos AdaptadaAbc
para funcionarem como se fossem do tipo Alvo
.
Cliente
é uma classe qualquer que vai usar esta interface para se comunicar com os objetos adaptados por meio de seus adaptadores.
Assim, se a classe Cliente
precisar trocar um objeto por outro, como o objeto será acessado por meio da interface Alvo
, não precisaremos alterar a forma de utilizar um novo objeto.
Observe que, por exemplo, o metodo1()
na classe AbcAdapter
vai apenas delegar a chamada ao metodoA()
de um objeto AbcAdapter
.
Um projeto de exemplo para o diagrama apresentado está disponível aqui. Ele deve ser alterado para incluir as mudanças necessárias para o problema específico que você estiver resolvendo com o padrão.
Você pode entender melhor o diagrama da Figura 1 fazendo uma analogia com o adaptador de tomada como a Figura 2 abaixo.
1.4. Exemplos Práticos
A geração de números aleatórios utilizando serviços web como http://random.org e http://random.irb.hr também é outro exemplo onde adapters podem ser criados. Tais serviços geram números verdadeiramente aleatórios usando, por exemplo, dados meteorológicos. No entanto, cada serviço implementa uma API diferente, com parâmetros e tipos de retorno distintos. Para trocar um serviço por outro em uma aplicação, é preciso alterar o código da aplicação por causa das diferenças na API.
1.5. Princípios utilizados
-
Single Responsibility Principle (SRP) pois cada adaptador criado possui uma responsabilidade única: tornar uma classe a ser adaptada compatível com a interface alvo.
-
Open/Closed Principle (OCP), pois se uma nova classe precisar ser adaptada, basta criar um novo adaptador implementando a interface alvo. Não precisamos alterar nenhum código existente.
-
Liskov Substitution Principle (LSP) pois em qualquer lugar que a interface alvo puder ser usada, podemos usar um adaptador pra qualquer uma das classes adaptadas. A Figura 4 mostra como o princípio se aplica.
-
Dependency Inversion Principle (DIP): ao usar somente a interface alvo para declarar variáveis e não as classes adaptadas, passamos a depender de uma abstração e não de implementações. Com isto, os relacionamentos de dependência são invertidos: no lugar de o cliente depender de cada uma das implementações, ele depende apenas do tipo abstrato (a interface alvo). O cliente nem precisa saber da existência das classes concretas. Reveja a Figura 1 para entender o relacionamento de dependência.
-
Programar para uma interface não uma implementação (GoF): como o código do cliente vai depender apenas da interface alvo, estaremos "programando para uma interface, não uma implementação".
2. Padrões Relacionados
-
Decorator
3. Onde o padrão é usado no JDK e outros lugares
No JDK, apesar de não estar explícito pelo nome, temos adapters implementados como métodos. Exemplos incluem:
-
java.util.Arrays#asList()
: a partir de um array, retorna uma nova instância de um adaptador para List. -
java.util.Collections#list()
: a partir de uma coleção qualquer como conjuntos (Set), filas (Queue), -
um pacote inteiro de adapters para aplicações Desktop com JavaFX
Bibliotecas para geração de logs em aplicações são um exemplo onde o Adapter e outros padrões (como o Facade) são usados. Você pode nunca ter usado uma biblioteca destas em Java ou qualquer linguagem, mas pode ter certeza que precisará. Log é um recurso essencial em qualquer aplicação rodando em produção.
A java.util.logging (JUL) é um dos casos onde uma biblioteca fornecida pelo JDK possui recursos limitados e outras surgiram justamente para resolver tais problemas, como a Apache Log4J. Existem algumas outras bibliotecas de log para Java, mas incompatíveis entre si. Por esta causa, o adaptador Apache Log4J JUL Adapter permite utilizar a Log4J no lugar da JUL, sem precisar alterar o código da aplicação.
3.1. Pensando em interfaces como adaptadores
Em um classe que implementa múltiplas interfaces, podemos pensar na classe como sendo um adaptador para todas estas interfaces. Apesar de nem sempre o padrão Adapter está sendo de fato implementado nestes casos, ao pensar assim, podemos ter alguns benefícios.
Na Java Collections Framework (JCF), classes como ArrayList e LinkedList funcionam como adaptadores para a interface List. Assim, no lugar de declarar os tipos concretos, usamos a interface List no lugar. Assim, em qualquer lugar que for exigido uma List, podemos passar um "adaptador" como ArrayList ou LinkedList. Internamente, estas classes podem ter métodos com nomes e assinaturas diferentes, mas como elas implementam os métodos de List, as diferenças internas são encapsuladas.
Por exemplo, na classe ArrayList utiliza-se elementData(index)
para acessar um elemento
em uma determinada posição. Na classe LinkedList utiliza-se node(index).item
.
Mesmo as duas classes fazerem parte da mesma framework (a JCF), elas representam estruturas
de dados muito diferentes. Para tornar seu uso uniforme para nós desenvolvedores,
os métodos citados não são públicos. Temos o public E get(int index)
em tais classes,
que é herdado da interface List
. Tal método padroniza o acesso aos elementos.
Classes como ArrayList e LinkedList implementam múltiplas interfaces em uma hierarquia como List → Collection → Iterable. Tal hierarquia nos permite usar um ArrayList como se fosse um objeto List, Collection ou Iterable, de acordo com suas necessidades.
Como exemplo, veja o seguinte método com uma implementação não ideal:
private void imprimir(ArrayList<Double> elementos){
for (Double e : elementos) {
System.out.println(e);
}
elementos.clear();
}
Como pode ver, o método recebe um ArrayList, imprime todos os valores e apaga seus elementos. Pense em como podemos criar 3 diferentes versões deste método para:
-
imprimir qualquer tipo de lista (ArrayList, LinkedList, etc)
-
impedir que a lista seja modificada (por exemplo, pela remoção de elementos)
-
imprimir qualquer tipo de coleção (ArrayList, LinkedList, HashSet, TreeSet, etc)
Analise a árvore hierarquica dos tipos mencionados e quais métodos públicos eles fornecem para resolver os 3 problemas acima. Você começar analisando a hierarquia da classe ArrayList em sua documentação. |