Desvendando o Poder do Padrão de Design Adapter: Integrando Sistemas com Interfaces Incompatíveis de Forma Elegante

O design pattern Adapter, é um padrão do tipo estrutural, a solução que ele nos provê é bem interessante, poder utilizar uma classe como uma interface na qual essa classe não implementa, esse tipo de abordagem é bem útil em evoluções ou adaptações que são necessárias no sistema.

Esse pattern vai atuar como um intermediador entre duas interfaces que são incompatíveis, fazendo com que o utilizador dessas interfaces consiga utilizar uma classe que não foi projetada originalmente para aquela interface.

Esse tipo de abordagem traz uma grande vantagem quando estamos trabalhando em melhorias no sistema e precisamos adicionar novas funcionalidades ou trocar componentes, com um sistema que já está em operação, pois podemos ir trocando de forma gradual sem afetar de fato o funcionamento do sistema como um todo.

Implementação

Para exemplificar como seria esse implementação, vamos utilizar um exemplo de queries em banco de dados, sabemos que o mais comum é utilizar um ORM que fará o trabalho pesado para nós, mas essa é uma boa maneira de exemplificar esse tipo de implementação.

Como primeiro passo vamos imaginar que temos uma interface que define o método na qual utilizamos para chamar nossa query.

public interface QueryBancoDados {

	Object executarQuery();

}
QueryBancoDados.java

E que o nosso sistema já utiliza um banco de dados relacional, e para isso temos uma classe concreta que implementa essa interface fazendo a chamada ao banco.

public class SQLQuery implements QueryBancoDados {

	@Override
	public Object executarQuery() {
		System.out.println("SELECT * FROM pessoas");
		return null;
	}

}
SQLQuery.java

Não vamos fazer a implementação detalhada de como seria essa chamada e ler os resultados e transformar em objetos para não fugir do nosso foco que é a implementação do adapter em si.

Vamos imaginar que nosso sistema tem um controlador que utiliza-se dessa implementação para buscar as pessoas no banco de dados e exibir.

public class PessoasController {

	public Object buscarPessoas() {
		QueryBancoDados query = new SQLQuery();
		return query.executarQuery();
	}

}
PessoaController.java

Temos um exemplo de uma implementação já funcionando, e vamos utilizar nosso pattern para atualizar essa implementação adicionando uma nova ferramenta.

Imagine que essa tabela pessoa será migrada para uma “collection” do MongoDB e que agora para consumir esse recurso vamos precisar buscar os dados de lá, como se trata de um outro banco de dados e que se comunica de uma forma diferente vamos precisar implementar uma nova forma de leitura, veja o exemplo a seguir.

public class MongoQuery {

	public Object executar() {
		System.out.println("db.collection('pessoas').find({})");
		return null;
	}

}
MongoQuery.java

Observe que nossa implementação é completamente diferente da anterior e precisamos de uma forma para substituir a chamada para esse novo banco, para isso vamos criar um Adapter.

public class MongoQueryAdapter implements QueryBancoDados {

	private MongoQuery mongoQuery;
	
	public MongoQueryAdapter(MongoQuery mongoQuery) {
		super();
		this.mongoQuery = mongoQuery;
	}

	@Override
	public Object executarQuery() {
		return mongoQuery.executar();
	}

}
MongoQueryAdapter.java

A criação do nosso adapter é relativamente simples, precisamos apenas criar uma nova classe que encapsule nossa implementação atual, mas que implemente a interface alvo, para que no método a ser invocado, possamos substituir o comportamento chamando assim nossa nova classe que irá ler os dados do outro banco.

public class PessoasController {

	public Object buscarPessoas() {
		//Implementação antiga
		// QueryBancoDados query = new SQLQuery();
		
		// Nova implementação utlizando o adapter
		QueryBancoDados query = new MongoQueryAdapter(new MongoQuery());
		
		return query.executarQuery();
	}

}
PessoaController.java

Observe como a substituição acaba ficando simples, claro que nesse contexto que estamos seria bem simples fazer a substituição direta, mas em sistemas mais complexos pode não ser tão fácil assim trocar componentes, então essa acaba sendo uma abordagem que resolve bastante problemas.

Observações sobre a utilização do Adapter

Como todo design pattern esse não é uma exceção e tem lá suas desvantagens que devem ser consideradas quando você escolhe utilizá-lo, vamos citar algumas a seguir.

Esse padrão por ser ótimo em fazer conversar duas assinaturas diferentes pode trazer mais complexidade para o seu código, dependendo de como está estruturado e a quantidade de adapters que você precisa criar o código pode ficar bem mais complexo de ser entendido e ainda mais difícil de manter.

Esse tipo de implementação também pode causar um overhead de desempenho, pois está acontecendo um redirecionamento de chamadas de métodos, em sistemas que o desempenho é muito considerado e o ambiente onde ele está sendo executado é limitado, a utilização desse padrão pode trazer problemas.

Essa abordagem também faz o nosso sistema ficar mais acoplado, pois utiliza diferentes partes recursos do sistema, e no futuro remover um componente pode ser algo desafiador.

Identificar a forma correta de construir o adapter nem sempre é tão simples, a depender da complexidade do sistema, pode ser muito difícil conseguir construir um adapter que não seja necessário alterar muitos pontos do sistema, isso pode trazer mais complexidade e transformar o desenho do seu sistema em algo bem confuso.

Muitas das vezes esses adapters são construídos para servir de forma temporária, para uma evolução do sistema, o problema é que às vezes o temporário acaba se tornando permanente e depois se transformando em uma legado muito difícil de se lidar e o pior de tudo muito difícil de ser removido, então tenha bastante cuidado com esses adaptadores temporários.

Conclusão

Como abordado aqui esse é uma excelente padrão quando utilizado da forma correta, sua utilização não serve apenas para criar código temporários em uma possível evolução do sistema, mas serve como um padrão a ser considerado para construção de interfaces desacopladas, é bastante utilizado em arquiteturas modernas como a hexagonal.

Como todo padrão tem seus prós e contras e o importante é analisar onde será implementado esse padrão e tentar prever os futuros problemas que pode ocasionar.

Importante lembrar que toda implementação sempre pode trazer problemas futuros, e sempre devemos pensar em construir um código limpo e de fácil entendimento para outros desenvolvedores, excelentes programadores sempre tentam implementar funcionalidades que seja de fácil entendimento para outros, e que outros consigam dar continuidade ao seu trabalho.

Links uteis

Mauricio Lima
Mauricio Lima

Bacharel em Ciência da Computação, profissional dedicado ao desenvolvimento de software e entusiasta da tecnologia.

Artigos: 65