Com o avanço do uso de sistemas de informação em praticamente todas as empresas atuais, sejam elas pequenas, médias ou grandes, faz-se necessário cada vez mais adotar uma política de organização e estruturação adequada de um projeto de software, visto que devido à concorrência atual do mercado, somos cada vez mais forçados a oferecer um serviço de qualidade e isso é ótimo para o avanço cada maior de nossos conhecimentos. Assim como a indústria automobilística avançou consideravelmente durante as décadas, a indústria de fabricação de softwares também segue a mesma linha de crescimento.

O objetivo deste artigo é apresentar, praticar e exercitar o uso do padrão de projetoconhecimento como Adapter, que também possui o sinônimo Wrapper. Não apresentaremos apenas conceitos e diagramas sobre tal padrão, nosso foco vai muito além, iremos demonstrar usos práticos deste padrão em Java e tentar explicar um conceito que a tanto confunde os profissionais da área de TI.

Adapter de Composição

Também conhecido pelo nome Wrapper, como citamos anteriormente, o padrão de projeto Adapter converte a interface de uma classe em uma outra interface que o cliente está esperando, ele possibilita que classes com comportamentos diferentes possam trabalhar juntas em prol do mesmo objetivo. Não se preocupe se não ficou claro o conceito acima, iremos detalhar mais abaixo de uma forma ainda mais didática.

Vamos por partes. Citamos mais acima que o Adapter “converte a interface de uma classe em uma outra interface que o cliente está esperando”, mas o que isso significa exatamente?

Para tentar entender como o Adapter age para transformar uma interface qualquer na interface que estamos esperando, vamos a um exemplo bem didático:

Imagine que você tem uma interface que possui um método capaz de tocar uma música, ou seja, em qualquer ponto do seu sistema você poderá usar esta interface para acionar a ação de tocar uma música, instanciando uma classe que implemente tal comportamento, chamaremos de MediaPlayer, como mostra a Listagem 1.

Listagem 1. Interface MediaPlayer


   
  public interface MediaPlayer {
         public void play(String audioType, String fileName);
  }

A interface acima é o que o cliente espera, ou seja, em qualquer ponto do sistema o cliente chama a mediaPlayer.play(formato da musica, nome da musica) para tocar a música desejada. Quem realizou a chamada ao método play() não faz ideia de como a música será reproduzida, apenas deseja que a mesma seja reproduzida. Vejamos na Listagem 2 a implementação da nossa interface.

Listagem 2. Implementando interface MediaPlayer


  public class AudioPlayer implements MediaPlayer {
   
         @Override
         public void play(String audioType, String fileName) {
               if (audioType.equalsIgnoreCase("mp3")) {
                      System.out.println("Reproduzindo música " + fileName + " ...");
               } else {
                      System.err.println("Formato " + audioType + " não suportado");
               }
         }
   
  }

O que temos acima é a implementação do método play() onde só será executada a reprodução da música se o formato for MP3. Até este momento não aplicamos o padrão Adapter, estamos apenas situando, você leitor, no contexto que estamos trabalhando.

Porém, nossa classe AudioPlayer apenas consegue reproduzir músicas no formato MP3, mas agora é necessário que sejam reproduzidos os formatos MP4 e VLC.

Baixamos uma biblioteca que possui as implementações necessárias para reproduzir MP4 e VLC. Vejamos as Listagens 3 a 5.

Listagem 3. Interface AdvancedMediaPlayer


   
  public interface AdvancedMediaPlayer {
         public void playVlc(String fileName);
         public void playMp4(String fileName);
  }

Perceba que a interface da Listagem 3 possui métodos diferentes da nossa interface MediaPlayer.

Listagem 4. Classe VlcPlayer


  public class VlcPlayer implements AdvancedMediaPlayer{
     @Override
     public void playVlc(String fileName) {
        System.out.println("Playing vlc file. Name: "+ fileName);           
     }
   
     @Override
     public void playMp4(String fileName) {
        //do nothing
     }
  }

Listagem 5. Classe Mp4Player


  public class Mp4Player implements AdvancedMediaPlayer{
   
     @Override
     public void playVlc(String fileName) {
        //do nothing
     }
   
     @Override
     public void playMp4(String fileName) {
        System.out.println("Playing mp4 file. Name: "+ fileName);           
     }
  }

Pronto, temos a definição da nossa biblioteca terceirizada. Aqui entrará o nosso Adapter ou Adaptador, ele servirá como uma ponte de ligação entre essa nova interface com a antiga interface MediaPlayer, vejamos nosso Adapter na Listagem 6.

Listagem 6. MediaAdapter


  public class MediaAdapter implements MediaPlayer {
   
     AdvancedMediaPlayer advancedMusicPlayer;
   
     public MediaAdapter(String audioType){
        if(audioType.equalsIgnoreCase("vlc") ){
           advancedMusicPlayer = new VlcPlayer();                    
        } else if (audioType.equalsIgnoreCase("mp4")){
           advancedMusicPlayer = new Mp4Player();
        }      
     }
   
     @Override
     public void play(String audioType, String fileName) {
        if(audioType.equalsIgnoreCase("vlc")){
           advancedMusicPlayer.playVlc(fileName);
        }else if(audioType.equalsIgnoreCase("mp4")){
           advancedMusicPlayer.playMp4(fileName);
        }
     }
  }

O nosso adapter implementa o MediaPlayer pois é o que o cliente espera, mas usa a nova interface AdvancedMediaPlayer. O papel dele é realizar a conversão desta nova interface para a antiga interface de forma que ambos funcionem sem alterar o comportamento atual do sistema.

Agora nosso AudioPlayer que antes só podia reproduzir músicas em MP3 irá receber um Adapter capaz de ajudá-lo a reproduzir uma maior quantidade de músicas, vejamos a Listagem 7.

Listagem 7. AudioPlayer com adapter


  public class AudioPlayer implements MediaPlayer {
     MediaAdapter mediaAdapter;
   
     @Override
     public void play(String audioType, String fileName) {          
   
        //inbuilt support to play mp3 music files
        if(audioType.equalsIgnoreCase("mp3")){
           System.out.println("Playing mp3 file. Name: "+ fileName);               
        }
        //mediaAdapter is providing support to play other file formats
        else if(audioType.equalsIgnoreCase("vlc")
           || audioType.equalsIgnoreCase("mp4")){
           mediaAdapter = new MediaAdapter(audioType);
           mediaAdapter.play(audioType, fileName);
        }
        else{
           System.out.println("Invalid media. "+
              audioType + " format not supported");
        }
     }   
  }

Perceba na listagem acima que além das funcionalidades padrões do nosso player, que é tocar música em MP3, foi adicionado/plugado um adaptador capaz de acrescentar funcionalidades ao mesmo sem alterar o comportamento do sistema. Isso significa que na classe principal (Listagem 8), responsável por executar o play() nada mudará.

Listagem 8. Classe principal


  public class AdapterPatternDemo {
     public static void main(String[] args) {
        AudioPlayer audioPlayer = new AudioPlayer();
   
        audioPlayer.play("mp3", "beyond the horizon.mp3");
        audioPlayer.play("mp4", "alone.mp4");
        audioPlayer.play("vlc", "far far away.vlc");
        audioPlayer.play("avi", "mind me.avi");
     }
  }

Se amanhã surgir a necessidade de acrescentar mais formatos, a classe AdapterPatternDemo pode continuar inalterada, apenas sendo necessário acrescentar funcionalidades ao nosso Adapter.

Adapter de Herança

O adapter que vimos acima trabalha especificamente com o conceito de composição, ou seja, a nossa classe AudioPlayer que antes só podia tocar músicas em MP3 foi adicionada de um adapter, como atributo, para adicionar um maior número de funcionalidades. Uma outra forma de trabalhar com Adapter é através da herança, muito útil em implementação de Listeners, por exemplo.

Vamos imaginar um outro exemplo para entender o uso do Adapter usando Herança. O Java provê uma uma interface chamada List, muito conhecida até pelas iniciantes em Java. Esta interface define vários métodos para manipular elementos em uma lista tais como: isEmpty, contains, size, add, remove, addAll e etc. A maioria dos sistemas, se não todos, em algum momento fazem uso desta interface para manipular uma lista de objetos, vejamos a Listagem 9.

Listagem 9. Manipulando uma lista com List


  import java.util.ArrayList;
  import java.util.List;
   
  public class Main {
     public static void main(String[] args) {
        List minhaList = new ArrayList();
        minhaList.add("ola mundo");
     }
  }

Veja que estamos fazendo uso da interface List na listagem 9, e chamando o método add() que está definido nela, sendo o ArrayList a classe que implementa List, já que está não pode ser instanciada diretamente, serve apenas como um “contrato” a ser seguido. Agora imagine que estamos usando o método add() na interface List em 450 pontos diferentes do sistema (isso ainda é pouco, normalmente são muito mais para este tipo de interface tão comum), e agora a Oracle atualiza o pacote “java.util” removendo o método add() da interface List. Você teria que mudar todos os 450 pontos do sistema o que levaria muito tempo e dinheiro, além de ter que arquitetar uma nova lógica para cada um desses pontos já que o método add() já não existe mais.

A ideia aqui é trabalhar com um Adapter nosso, já que não podemos alterar ou mexer no List, sendo assim chamaremos sempre nosso Adapter e não o List. Vejamos a Listagem 10.

Listagem 10. Usando Adapter para o List


  import java.util.Collection;
  import java.util.Iterator;
  import java.util.List;
  import java.util.ListIterator;
   
   
  public class ListAdapter implements List {
   
           @Override
           public int size() {
                     // TODO Auto-generated method stub
                     return 0;
           }
   
           @Override
           public boolean isEmpty() {
                     // TODO Auto-generated method stub
                     return false;
           }
   
           @Override
           public boolean contains(Object o) {
                     // TODO Auto-generated method stub
                     return false;
           }
   
           @Override
           public Iterator iterator() {
                     // TODO Auto-generated method stub
                     return null;
           }
   
           @Override
           public Object[] toArray() {
                     // TODO Auto-generated method stub
                     return null;
           }
   
           @Override
           public Object[] toArray(Object[] a) {
                     // TODO Auto-generated method stub
                     return null;
           }
   
           /*
            * MÉTODO NOVO, que não tem no List (Só um exemplo)
            * */
           public boolean add(Object e) {
                     // TODO: criar lógica
                     return false;
           }
   
           @Override
           public boolean remove(Object o) {
                     // TODO Auto-generated method stub
                     return false;
           }
   
           @Override
           public boolean containsAll(Collection c) {
                     // TODO Auto-generated method stub
                     return false;
           }
   
           @Override
           public boolean addAll(Collection c) {
                     // TODO Auto-generated method stub
                     return false;
           }
   
           @Override
           public boolean addAll(int index, Collection c) {
                     // TODO Auto-generated method stub
                     return false;
           }
   
           @Override
           public boolean removeAll(Collection c) {
                     // TODO Auto-generated method stub
                     return false;
           }
   
           @Override
           public boolean retainAll(Collection c) {
                     // TODO Auto-generated method stub
                     return false;
           }
   
           @Override
           public void clear() {
                     // TODO Auto-generated method stub
                     
           }
   
           @Override
           public Object get(int index) {
                     // TODO Auto-generated method stub
                     return null;
           }
   
           @Override
           public Object set(int index, Object element) {
                     // TODO Auto-generated method stub
                     return null;
           }
   
           @Override
           public void add(int index, Object element) {
                     // TODO Auto-generated method stub
                    
           }
   
           @Override
           public Object remove(int index) {
                     // TODO Auto-generated method stub
                     return null;
           }
   
           @Override
           public int indexOf(Object o) {
                     // TODO Auto-generated method stub
                     return 0;
           }
   
           @Override
           public int lastIndexOf(Object o) {
                     // TODO Auto-generated method stub
                     return 0;
           }
   
           @Override
           public ListIterator listIterator() {
                     // TODO Auto-generated method stub
                     return null;
           }
   
           @Override
           public ListIterator listIterator(int index) {
                     // TODO Auto-generated method stub
                     return null;
           }
   
           @Override
           public List subList(int fromIndex, int toIndex) {
                     // TODO Auto-generated method stub
                     return null;
           }
   
  }

Veja bem, nosso Adapter implementa todos os métodos da interface List, e adiciona um novo método chamado add() que não existe mais no List (claro que isto é apenas um exemplo, a Oracle jamais retiraria o add() sem um plano de contingência muito bem estruturado, isso poderia causar um grande abalo mundial, imagine quantos sistemas no mundo usam este recurso que é vital). Já que agora não existe mais o add() no List, teremos que pensar em uma nova maneira de adicionar objetos a lista, mas todo esse trabalho será feito em apenas um ponto, no nosso ListAdapter. Veja a Listagem 11.

Listagem 11. Implementando o novo método add()


  /*
          * MÉTODO NOVO, que não tem no List (Só um exemplo)
          * */
         public boolean add(Object e) {
               this.set(this.size(), e);
               return true;
         }

Se estivermos usando o ListAdapter em vez do List em todos os 450 pontos do nosso sistema, quando o método add() for retirado do List nós apenas criamos uma lógica interna no Adapter para que a adição de elementos possa continuar, veja acima que criamos uma alternativa para o add(), usando o método set() que ainda continua no List.

Listagem 12. Usando o ListAdapter


   
  public class Main {
     public static void main(String[] args) {
            ListAdapter minhaList = new ListAdapter();
         minhaList.add("ola mundo");
     }
  }
  

Diferente da listagem 9, não nos importa se o método add() existe ou não na interface List, nós continuamos usando ele.

Isso também nos traz outros benefícios e não apenas o baixo acoplamento entre classes, que traz grandes pontos positivos para o projeto. Quando usamos Adapters em Listeners, por exemplo, evitamos de sobrescrever todos os métodos da interface mesmo que não necessitemos deles.

Listagem 13. Exemplo com Listener


  import javax.swing.event.CellEditorListener;
  import javax.swing.event.ChangeEvent;
   
  public class Main {
         public static void main(String[] args) {
          new CellEditorListener() {
                      
                      @Override
                      public void editingStopped(ChangeEvent e) {
                             // TODO Auto-generated method stub
                             
                      }
                      
                      @Override
                      public void editingCanceled(ChangeEvent e) {
                             // TODO Auto-generated method stub
                             
                      }
               };
         }
  }
  

No exemplo acima nós precisamos implementar todos os métodos da interface CellEditorListener, mesmo que só fossemos utilizar o método editingCanceled. Com o Adapter nós evitamos isso.

Listagem 14. CellAdapterListener


  import javax.swing.event.CellEditorListener;
  import javax.swing.event.ChangeEvent;
   
   
  public class CellAdapterListener implements CellEditorListener {
   
         @Override
         public void editingStopped(ChangeEvent e) {
               // TODO Auto-generated method stub
               
         }
   
         @Override
         public void editingCanceled(ChangeEvent e) {
               // TODO Auto-generated method stub
               
         }
   
         
   
  }
  

E agora na nossa classe main na Listagem 15.

Listagem 15. Usando o CellAdapterListener


  import javax.swing.event.ChangeEvent;
   
   
  public class Main {
         public static void main(String[] args) {
          new CellAdapterListener(){
               @Override
               public void editingCanceled(ChangeEvent e) {
                 System.out.println("ola mundo, mudei apenas aqui");
                      
               }
          };
          
         }
  }

Veja como é comodo trabalhar desta forma. Além de diminuirmos o acoplamento entre as classes nós ainda reduzimos a quantidade de código inútil em nosso projeto. Esse é um exemplo bem simples, mas existem listeners com muito mais métodos que podem facilmente poluir toda sua classe se não tratados da forma correta.

Para aqueles que finalizaram o item 2 (Adapters de Composição) com a seguinte dúvida: Porque eu não posso simplesmente alterar o comportamento da classe? A resposta está no item 3 (Adapters de Herança), pois nem sempre você tem acesso a estas classes, como é o caso do List, CellEditorListener e etc, que são exemplos onde você não pode alterar o comportamento delas, para isso existe o Adapter. A pergunta é simples: Se amanhã a Oracle resolvesse retirar o método add() do List, o que seria menos custoso ?

1 – Parar de usar a linguagem Java e optar por uma outro linguagem que tenha o método add();

2 – Criar um Adapter que implemente a interface List adicionando o método add() como uma alternativa ao código;

Com certeza a segunda opção é sem dúvida milhões de vezes menos custosa. Que fique claro que estamos tratando de uma questão quase improvável (a retirada do método add() da interface List), mas se tratássemos de bibliotecas mais específicas, de autores mais específicos, é muito importante que o projeto do seu sistema não baseie-se complementante nela, ou seja, tenha um alto acoplamento nela, pois se amanhã este autor resolver remover um método você terá uma grande dor de cabeça e mudar toda a lógica do seu sistema. O Adapter lhe ajudará nesta tarefa.

Para finalizar vamos demonstrar um último exemplo sobre adapter que pode concluir o seu raciocínio, se ainda houver alguma dúvida. Vamos esquecer o código e pensar no nosso dia a dia, onde podemos ver um exemplo de Adapter? Em uma extensão elétrica, isso mesmo.

Em uma extensão elétrica nós temos diversas entrada, vamos imaginar que a nossa tenha apenas 3 entradas para tomadas. Agora compramos mais um aparelho e precisamos de 4 entradas, mas a 3 já estão cheias e não temos mais onde por. O que seria mais viável:

1 – Comprar uma nova extensão com 4 entrada e jogar essa com 3 entrada fora;

2 – Compra um Adaptador (nosso Adapter) que possua 2 entradas, assim somando mais 1 entrada a nossa extensão;

Com certeza vocês responderam a opção 2 novamente. Ninguém em sã consciência compraria uma nova extensão só porque precisa de mais 1 entrada, o normal e mais viável é comprar adaptadores para tal situação.

Esperamos que após toda a explicação realizada, com exemplos e conceitos, tenha ficado bem nítido o uso correto deste padrão de projeto. Vale ressaltar que estamos mostrando o seu funcionamento em Java, mas ele pode ser aplicado a qualquer outra linguagem de programação. O nosso último exemplo, sobre a extensão, foi o ponto final para que aqueles que ainda ficaram com alguma dúvida pudessem sana lá definitivamente, pois nada melhor que trazer o problema para nosso cotidiano para tentar desmistificá-lo.

Além disso, vimos a teoria e uso do Adapters de Composição, ou seja, aqueles que são “acoplados” junto a classe necessária, e ao final vimos também a teoria e uso dos Adapters de Herança, que diferente dos de Composição, são usados em forma de herança para adicionar funcionalidades a uma classe e baixar o nível de acoplamento. Possivelmente, você leitor, já deve ter se deparado com algum Adapter de Herança, que é muito mais cupom que o de composição e provavelmente agora deverá entender o porquê dele existir.