Refatoração de Código: Extrair Adapter

Receba em primeira mão o conteúdo exclusivo do Blog, além de promoções de livros e cursos de programação. Você receberá um email de confirmação. Somente depois de confirmar é que poderei lhe enviar o conteúdo exclusivo por email.

Email inválido.
Blog /Android /Refatoração de Código: Extrair Adapter

Refatoração de Código: Extrair Adapter

Vinícius Thiengo23/06/2016
(547) (8) (13)
Go-ahead
"Dar o seu melhor neste exato momento vai colocá-lo no melhor lugar possível no momento seguinte."
Oprah Winfrey
Código limpo
Capa do livro Refatorando Para Programas Limpos
TítuloRefatorando Para Programas Limpos
CategoriaEngenharia de Software
AutorVinícius Thiengo
Edição
Ano2017
Capítulos46
Páginas598
Comprar Livro
Conteúdo Exclusivo
Receba em primeira mão o conteúdo exclusivo do Blog, além de promoções de livros e cursos de programação.
Email inválido

Opa, blz?

Nesse artigo continuamos com a série Refatoração de Código para obter maior performance de nossos projetos de software. Dessa vez abordando o método de refatoração Extrair Adapter.

Lembrando que os artigos dessa série de refatoração de código, incluindo os artigos sobre padrões de projetos, todos esses podem ser utilizados em qualquer projeto de software que trabalhe com orientação a objetos.

Antes de prosseguir é importante que você tenha conhecimento do padrão de projeto Adapter. Caso ainda não o conheça, tem um artigo no blog somente sobre ele:

Assumindo que a partir desse ponto você sabe o que é um Adapter, vamos ao método de refatoração proposto.

Tópicos presentes no artigo:

Motivação

Em seu projeto há classes que têm vários condicionais para permitir que códigos cliente possam trabalhar com diferentes versões de instâncias de outras classes do domínio do problema. Isso para a execução de algumas tarefas no código cliente sem que ele tenha de verificar versões de código em uso.

Essas classes adaptadoras também têm variáveis de instâncias e alguns métodos específicos somente para trabalhar corretamente com as diferentes versões de instâncias suportadas do domínio do problema.

O modelo de código atual do projeto é, na verdade, um problema, pois qualquer alteração nas classes suportadas no código da classe adaptadora pode implicar na também alteração dessa classe adaptadora. Além do mais, devido a utilização de inúmeros condicionais o código fica pouco legível a outros programadores do projeto podem perder um bom tempo para entendê-lo.

Note que esse problema de classe adaptadora suportando mais de uma versão de código é conhecido como "classe adaptadora sobrecarregada”. Uma maneira de corrigir esse problema é aplicar o padrão de projeto Adapter por meio do método de refatoração Extrair Adapter.

Código de exemplo

Abaixo o código que vamos utilizar como exemplo. Esse é um código real de acesso a banco de dados onde uma classe cliente trabalha com condicionais para saber qual versão utilizar de um API que permite acesso ao banco de dados. Segue código da classe adaptadora sobrecarregada, Query:

public class Query {
private Login login;
private Session session;
private LoginSession loginSession;
private boolean version52;
private CoreQuery query;

/* Login para a versão 5.1 da API */
public void login( String server, String user, String password ) {
version52 = false;
session = login.loginSession( server, user, password );
}

/* Login para a versão 5.2 da API */
public void login( String server, String user, String password, String configFileName ) {
version52 = true;
loginSession = new LoginSession( configFileName, false );
loginSession.loginSession( server, user, password );
}

/* Método único para ambas as versões */
public void doQuery(){
if( query != null ){
query.clearResultSet();
}

if( version52 ){
query = loginSession.createQuery( Query.OPEN_FOR_QUERY );
}
else{
query = session.createQuery( Query.OPEN_FOR_QUERY );
}
executeQuery();
}
}

 

Veja que as versões de API suportadas são as 5.1 e 5.2.

Note que aqui somente as partes importantes para entendimento do método de refatoração é que foram apresentadas.

Mecânica

Nosso primeiro passo é identificar em nosso projeto a classe (ou classes) que trabalha como uma classe adaptadora sobrecarregada. Em nosso caso é a classe Query que tem código para trabalhar com múltiplas versões da API de banco de dados:

public class Query {
...
}

 

Seguindo para o passo dois, agora temos de criar uma subclasse (ou classe) adaptadora para somente uma versão de todas as versões suportadas em nossa classe adaptadora sobrecarregada, Query.

Com isso vamos estar respeitando a lógica mais comum do padrão Adapter: um adapter para cada versão e não um para múltiplas.

Vamos começar criando uma classe adaptadora para a versão 5.1 da API de banco de dados. Vamos chamá-la Query51:

public class Query51 extends Query {
/* TODO */
}

 

Ainda dentro do passo dois temos de mover ou copiar todos os métodos e variáveis de instâncias que estão em Query e que são utilizados pela versão 5.1 da API.

Antes vamos a uma possível dúvida: por que mover ou copiar?

Esse trecho é crítico, pois não há uma regra para seguir, é literalmente no “olhômetro".

No caso, copie quando forem entidades que também são utilizadas por outras versões suportadas pela classe adaptadora sobrecarregada. Dessa forma não há chances de quebrar o funcionamento do projeto, não foi necessário remover nada.

Mova somente quando são entidades utilizadas apenas pela versão que está sendo adaptada e que não afeta os códigos clientes que acessam diretamente instâncias da classe adaptadora sobrecarregada.

O que quer dizer com: "...não afeta os códigos clientes que acessam diretamente instâncias da classe adaptadora sobrecarregada"?

Simples. Em nosso exemplo, quando houver instanciações para Query em que é possível alterar para Query51 sem muitas modificações, então os métodos e atributos em Query exclusivos para a versão Query51 podem ser movidos para essa classe.

Esse passo é realmente confuso no início. Caso na aplicação dele você ainda esteja com receio dos efeitos, apenas copie os métodos e atributos que a versão sendo adaptada também utiliza. Pois há um passo dessa refatoração somente de remoção de código duplicado.

Seguindo com a atualização do código de Query51, temos:

public class Query51 extends Query {
private Login login;
private Session session;

public void login( String server, String user, String password ) {
session = login.loginSession( server, user, password );
}

public void doQuery(){
if( query != null ){
query.clearResultSet();
}
query = session.createQuery( Query.OPEN_FOR_QUERY );
executeQuery();
}
}

 

Depois dessa atualização temos o seguinte código em Query:

public class Query {
private LoginSession loginSession;
private CoreQuery query;

/* Login para a versão 5.1 da API */
public void login( String server, String user, String password ) {}

/* Login para a versão 5.2 da API */
public void login( String server, String user, String password, String configFileName ) {
loginSession = new LoginSession( configFileName, false );
loginSession.loginSession( server, user, password );
}

/* Método único para ambas as versões */
public void doQuery(){
if( query != null ){
query.clearResultSet();
}
query = loginSession.createQuery( Query.OPEN_FOR_QUERY );
executeQuery();
}
}

 

Note que optamos por deixar o método login() da versão 5.1 da API para termos certeza que códigos clientes não tivessem problemas quanto ao funcionamento.

Ainda no passo dois, devemos alterar os códigos clientes que instanciam Query e poderiam estar instanciando Query51. Abaixo o código cliente ainda não modificado, loginToDatabase():

...
public void loginToDatabase( String db, String user, String password ){
query = new Query();

if( usingVersion52() ){
query.login( db, user, password, get52ConfigFileName() ); /* versão 5.2 */
}
else{
query.login( db, user, password ); /* versão 5.1 */
}
}
...

 

Atualizando ele temos:

...
public void loginToDatabase( String db, String user, String password ){
if( usingVersion52() ){
query = new Query();
query.login( db, user, password, get52ConfigFileName() ); /* versão 5.2 */
}
else{
query = new Query51();
query.login( db, user, password ); /* versão 5.1 */
}
}
...

 

Com isso podemos seguir ao passo três. Nesse passo devemos apenas realizar o passo anterior nas outras versões ainda suportadas pela classe adaptadora sobrecarregada, Query. Em nosso caso somente falta a versão 5.2 da API. Segue código da nova classe, Query52:

public class Query52 extends Query {
private LoginSession loginSession;

public void login( String server, String user, String password, String configFileName ) {
loginSession = new LoginSession( configFileName, false );
loginSession.loginSession( server, user, password );
}

public void doQuery(){
if( query != null ){
query.clearResultSet();
}
query = loginSession.createQuery( Query.OPEN_FOR_QUERY );
executeQuery();
}
}

 

Com essa atualização temos o seguinte novo código na classe Query:

abstract public class Query {
private CoreQuery query;

/* Login para a versão 5.1 da API */
public void login( String server, String user, String password ) {}

/* Login para a versão 5.2 da API */
public void login( String server, String user, String password, String configFileName ) {}

abstract public void doQuery();
}

 

Conseguimos com isso remover de Query, implementações de versões específicas de API.

Note que ainda no passo três podemos escolher modificar ainda mais a classe Query do que apenas mover alguns métodos e atributos para novas classes.

Em nosso caso a tornamos em uma classe abstrata. Para esse projeto essa escolha foi ideal, pois ou se trabalha com uma versão ou com a outra, não há uma versão padrão caso nenhuma seja especificada.

Alem do mais, as subclasses criadas são obrigadas a implementarem alguns métodos herdados, característica que de forma implícita associa a utilização de classe abstrata ou Interface para se aplicar o padrão Adapter.

No quatro e último passo temos a tarefa de remover todos os códigos duplicados que restaram.

Se voltarmos um pouco no artigo vamos notar que os métodos doQuery() das classes Query51 e Query52 são muito similares, somente o método de criação de CoreQuery é que muda. Veja esse método em Query51:

public class Query51 extends Query {
...

public void doQuery(){
if( query != null ){
query.clearResultSet();
}
query = session.createQuery( Query.OPEN_FOR_QUERY ); /* TRECHO ÚNICO */
executeQuery();
}
}

 

E agora em Query52:

public class Query52 extends Query {
...

public void doQuery(){
if( query != null ){
query.clearResultSet();
}
query = loginSession.createQuery( Query.OPEN_FOR_QUERY ); /* TRECHO ÚNICO */
executeQuery();
}
}

 

Notou algo?

Se conhece o padrão Factory Method notou que podemos aplicar ele em nossa classe abstrata Query forçando as classes Query51 e Query52 implementarem um método de criação createQuery(), ou seja, implementariam somente a única parte do código que varia em doQuery().

Para aplicar esse padrão utilizando as três classes envolvidas no projeto, podemos utilizar os métodos de refatoração: Introduzir Criação Polimórfica com Factory e Formar Template Method.

Segue classe Query com trecho adicionado depois das refatorações:

abstract public class Query {
...

abstract protected Query createQuery(); /* NOVO MÉTODO ABSTRATO IMPLICANDO NO FACTORY METHOD */

/* ESSE MÉTODO SOMENTE TEM O TRECHO createQuery() SENDO IMPLEMENTADO DE FORMA ÚNICA, AINDA EFEITO DO PADRÃO FACTORY METHOD */
public void doQuery(){
if( query != null ){
query.clearResultSet();
}
query = createQuery();
executeQuery();
}
}

 

Então a classe Query51:

public class Query51 extends Query {
...

@Override
protected Query createQuery() {
return session.createQuery( Query.OPEN_FOR_QUERY );
}
}

 

E enfim a classe Query52:

public class Query52 extends Query {
...

@Override
protected Query createQuery() {
return loginSession.createQuery( Query.OPEN_FOR_QUERY );
}
}

 

Ainda temos os métodos sobrecarregados login() em Query, porém eles não têm mais utilidade, tendo em mente que cada uma das subclasses implementa o próprio.

Ambos se diferenciam por causa de um parâmetro. Mais precisamente o parâmetro configFileName que é utilizado na versão 5.2 da API. Segue código atual de Query:

abstract public class Query {
...

/* Login para a versão 5.1 da API */
public void login( String server, String user, String password ) {}

/* Login para a versão 5.2 API */
public void login( String server, String user, String password, String configFileName ) {}

...
}

 

Nesse caso podemos criar um construtor em Query52 que aceite um configFileName como parâmetro de entrada eliminando assim a necessidade de ter esse parâmetro no método login() dessa classe. Segue código atualizado de Query52:

public class Query52 extends Query {
...
private String configFileName;

public Query52( String configFileName ){
super();
this.configFileName = configFileName;
}
...
}

 

Consequentemente podemos tornar o método login(), em Query, um método abstrato, pois agora ele tem exatamente a mesma assinatura em ambas as subclasses.

abstract public class Query {
...

abstract public void login( String server, String user, String password );
...
}

 

Podemos voltar ao código cliente loginToDatabase() e atualizá-lo:

...
public void loginToDatabase( String db, String user, String password ){
if( usingVersion52() ){
query = new Query52( get52ConfigFileName() );
}
else{
query = new Query51();
}
query.login( db, user, password );
}
...

 

Para finalizar vamos colocar ainda mais legibilidade ao código. Como? Renomeando a classe Query para AbstractQuery, pois isso é o que ela realmente é, dessa forma outros developers saberão que estão trabalhando com uma classe abstrata.

É bom saber

Esse passo final sobre renomeação é na verdade um passo extra. Os passos com utilização dos métodos de refatoração Introduzir Criação Polimórfica com Factory e Formar Template Method, esses não são extras, na verdade, o passo que requisita essas refatorações (passo quatro) tem como definição: remover qualquer códigos duplicados nos novos adaptadores aplicando outras refatorações.

As refatorações mais comumente aplicadas nesse passo quatro são as duas utilizadas aqui.

Essa alteração de nome pode ser um problema, pois podemos ter códigos que trabalham de forma polimórfica utilizando o tipo Query. Alterando para AbstractQuery teríamos de percorrer o código do projeto modificando o tipo sendo utilizado.

Mas o IDE faz isso para nós!

No way! Acredite, dependendo da linguagem, um IDE não realizaria a modificação por completo no projeto. O PHP, por exemplo, trabalha com includes e requiries via String, algo que alguns IDEs ainda não conseguem mapear 100%.

Logo nossa solução é criar uma Interface Query e renomear nossa classe Query para AbstractQuery. Segue nova Interface com a assinatura dos métodos abstratos:

public interface Query {
abstract void login( String server, String user, String password );
abstract AbstractQuery createQuery();
}

 

Então a classe AbstractQuery passa a implementar Query. Na verdade as subclasses de AbstractQuery (Query51 e Query52) é que vão ter de implementar os métodos abstratos de Query, alias ambas já implementam. Segue código de AbstractQuery:

abstract public class AbstractQuery implements Query {
private AbstractQuery query;

public void doQuery(){
if( query != null ){
query.clearResultSet();
}
query = createQuery();
executeQuery();
}
}

 

Com isso finalizamos a aplicação do padrão Adapter em nosso código por meio do método de refatoração Extrair Adapter.

Conclusão

Com a aplicação do Adapter via método de refatoração conseguimos um código mais modularizado e ainda mais legível devido a essa modularização. Adicionar suporte a novas instâncias de classes diferentes as já utilizadas também fica mais fácil. Somente fique atento para não cair na tentação de colocar uma classe adaptadora atendendo a mais de uma versão de instância de classe que deve ser adaptada, achando com isso que estará economizando código, quando na verdade poderá estar complicando ele.

Outros artigos da série

Segue lista de todos os artigos liberados dessa série sobre refatoração de códigos:

Internalizar Singleton

Mover Embelezamento Para Decorator

Substituir Condicionais que Alteram Estado por State

Introduzir Objeto Nulo

Unificar Interfaces Com Adapter

Mover Conhecimento de Criação Para Factory

Substituir Notificações Hard-Coded Por Observer

Substituir Código de Tipo Por Classe

Extrair Parâmetro

Unificar Interfaces

Limitar Instanciação Com Singleton

Mover Acumulação Para Parâmetro Coletor

Compor Method

Formar Template Method

Substituir Lógica Condicional Por Strategy

Introduzir Criação Polimórfica com Factory Method

Encapsular Classes Com Factory

Encadear Construtores

Substituir Construtores Por Métodos de Criação

Fontes

Refatoração para Padrões

Vlw.

Receba em primeira mão o conteúdo exclusivo do Blog, além de promoções de livros e cursos de programação.
Email inválido

Relacionado

Refatoração de Código: Formar Template MethodRefatoração de Código: Formar Template MethodAndroid
Crash Reporting, Firebase Android - Parte 11Crash Reporting, Firebase Android - Parte 11Android
Padrão de Projeto: Factory MethodPadrão de Projeto: Factory MethodAndroid
11º Sorteio Novatec e Blog Thiengo [Calopsita], livro Pense em Python11º Sorteio Novatec e Blog Thiengo [Calopsita], livro Pense em PythonSorteios

Compartilhar

Comentários Facebook

Comentários Blog

Para código / script, coloque entre [code] e [/code] para receber marcação especifica.
Forneça seu nome válido.
Forneça seu email válido.
Forneça o comentário.
Enviando, aguarde...