Neste artigo estudaremos sobre um assunto de grande utilidade na estruturação de projetos simples e complexos: a Interface.

Este assunto é de grande utilidade quando queremos construir um projeto bem estruturado, organizado, com baixo acoplamento e uma alta coesão. Mas para construir tal projeto é essencial conhecermos como utilizar de forma adequada as interfaces. O objetivo deste artigo é demonstrar de forma prática o uso de tal conceito, dando maior enfoque em código, tornando o aprendizado o mais prático possível.

Quer ficar por dentro de tudo que rola de novidade na linguagem Java? Não deixe de conferir o material que todo Programador Java deve acompanhar.

Interfaces

Não podemos começar a mostrar código sobre código sem entender pelo menos a ideologia por trás de uma Interface. Esta possui a definição sintática semelhante a definição de uma classe com alguns pontos a serem atentados:

  • Deve ser declarada com a palavra reservada interface;
  • Só pode conter assinatura dos métodos, sem corpo;
  • Só pode conter variáveis constantes, ou seja, aquelas declaradas para ser static e final;
  • Não podem ser instanciadas, apenas implementadas por classes com a palavra reservada implements;
  • A classe que implementa a interface deve implementar todos os seus métodos ou ser uma classe Abstrata;
  • Uma classe pode ter diversas interfaces, diferente da herança que só permite uma classe pai;
  • Uma interface pode estender diversas outras interfaces.

Dado os conceitos básicos acima podemos definir a interface como sendo um contrato que qualquer classe deve seguir para que possa usar tal interface. Vamos imaginar uma situação hipotética: você está criando a estrutura de um módulo eletrônico automotivo onde sua função é que este módulo seja o mais genérico possível a ponto de funcionar em qualquer tipo de carro (FORD, VW, FIAT, AUDI, BMW e etc.), lembrando que cada fabricante irá implementar sua funcionalidade específica.

A solução aqui é você criar uma interface chamada ModuloEletronico que possui a assinatura dos métodos ilustrando as ações que este deve realizar, por exemplo, subir vidro, descer vidro, Travar portas, ativar limpador de para-brisas e etc. Quando o fabricante Ford, por exemplo, for utilizar a sua estrutura, ele irá criar o ModuloEletronicoFord que irá dizer como um carro da Ford deve subir um vidro, descer um vidro e etc. Você não sabe como isso será feito, apenas sabe que deverá ser feito e precisa garantir que seja feito. Isso é uma interface.

Se ainda assim ficou um pouco confuso sobre o uso de interfaces, iremos ver um exemplo completo e prático de como usar tal recurso. Para isso, observe o diagrama de classes da Figura 1.

Diagrama de Classes

Figura 1. Diagrama de Classes

Todos os nossos códigos a seguir serão baseados no diagrama demonstrado. Em linhas gerais, nós temos uma classe Dispositivo com diversos atributos importantes, que serão explicados em detalhes mais à frente, e que poderá ser persistida na base de dados caso nossa lógica de negócios exija isso. Entenda que um dispositivo pode ser um Controle Remoto, mas também pode ser uma Impressora, como é nosso caso.

A classe impressora estende a classe Dispositivo, isso porque a nossa impressora tem um comportamento diferente dos outros dispositivos. A partir daqui começa a ficar interessante nosso diagrama, vamos pensar orientado a objetos. A nossa Impressora no caso acima é multifuncional pois tem as funções de scanner, fax, copiador e impressora. Poderíamos, por exemplo, implementar apenas Scanner e Printer para dizer que nossa impressora apenas realiza essas duas funções, mas o importante não é isso, o mais importante é como usamos toda essa modularização.

Para nosso programa não importa se a impressora é multifuncional ou se possui apenas função scanner ou apenas copia, isso realmente não importa. Desenvolveremos nosso programa de forma que ele chame o scanner como se fosse um dispositivo separado, a vantagem nisso é que independente se o scanner é na própria impressora ou não, o programa principal não mudará, é isso que chamamos de baixo acoplamento.

Você pode mudar o scanner para onde quer que seja, até colocar ele conectado remotamente em uma rede externa vinda do Japão, para o nosso programa o que importa é usar a interface Scanner para digitalizar o documento.

Vejamos agora a definição das nossas classes e interfaces definidas na Figura 1, começando pela classe Dispositivo, presente no código da Listagem 1.

Listagem 1. Dispositivo.java


  package devapp;
   
  import java.util.Date;
   
  public class Dispositivo {
         
          //1
          private String marca;
          private String modelo;
          private String cor;
          private String numeroSerie;
          private Date dataCompra;
          private Date dataCadastro;
         
          //2
          private boolean ativo;
         
          //3
          private String tipoDispositivo;
         
          //4
          public Dispositivo(){
                this.dataCadastro = new Date();
                this.ativo = true;
          }
      
         /*################################
          *      GETTERS e SETTERS
          * ################################
          * */
         
   
         public String getMarca() {
               return marca;
         }
   
         public void setMarca(String marca) {
               this.marca = marca;
         }
   
         public String getModelo() {
               return modelo;
         }
   
         public void setModelo(String modelo) {
               this.modelo = modelo;
         }
   
         public String getCor() {
               return cor;
         }
   
         public void setCor(String cor) {
               this.cor = cor;
         }
   
         public String getNumeroSerie() {
               return numeroSerie;
         }
   
         public void setNumeroSerie(String numeroSerie) {
               this.numeroSerie = numeroSerie;
         }
   
         public Date getDataCompra() {
               return dataCompra;
         }
   
         public void setDataCompra(Date dataCompra) {
               this.dataCompra = dataCompra;
         }
   
         public Date getDataCadastro() {
               return dataCadastro;
         }
   
         public void setDataCadastro(Date dataCadastro) {
               this.dataCadastro = dataCadastro;
         }
   
         public boolean isAtivo() {
               return ativo;
         }
   
         public void setAtivo(boolean ativo) {
               this.ativo = ativo;
         }
   
         public String getTipoDispositivo() {
               return tipoDispositivo;
         }
   
         public void setTipoDispositivo(String tipoDispositivo) {
               this.tipoDispositivo = tipoDispositivo;
         }
         
         /*
          * ##############################################
          * ##############################################
          * */
         
      
  }

Na classe dispositivo é importante entender algumas propriedades que estão numeradas com comentários:

  1. Temos os atributos que definem as características comuns a todos os dispositivos (controle remoto, impressora, mouse, televisão e etc.);
  2. O campo booleano 'ativo' identifica se este dispositivo ainda funciona. Este campo não é de suma importância para nosso estudo de caso, mas é importante modelar um caso no mundo real;
  3. O campo 'tipoDispositivo' identifica qual o tipo de dispositivo que estamos usando: IMPRESSORA, SCANNER (Caso seja um scanner separado), MOUSE e etc;
  4. Definimos um construtor onde colocamos a data de cadastro igual a data atual do sistema e o campo 'ativo' com o padrão TRUE.

É importante salientar que estamos abstraindo o uso de frameworks, o que deixaria o artigo bem mais complexo, pois o mais correto seriamos usar classes em atributos como tipoDispositivo, marca e modelo para tornar nosso projeto mais “Orientado a Objetos”.

Listagem 2. Impressora


  package devapp;
   
  import java.awt.image.BufferedImage;
   
  public class Impressora extends Dispositivo implements Multifuncional {      
     
         public Impressora(){
               setTipoDispositivo("IMPRESSORA");
         }
               
         /*
          * ####################################
          *  MÉTODOS DAS INTERFACES
          * ###################################
          * */
   
         @Override
         public void imprimir(byte[] dados, int quantidade) {
               // TODO Auto-generated method stub
               
         }
   
         @Override
         public void copiarDocumento(int quantidade) {
               // TODO Auto-generated method stub
               
         }
   
         @Override
         public void enviarFax(String texto, String numeroDestino) {
               // TODO Auto-generated method stub
               
         }
   
         @Override
         public String receberFax(String numeroOrigem) {
               // TODO Auto-generated method stub
               return null;
         }
   
         @Override
         public BufferedImage digitalizar() {
               // TODO Auto-generated method stub
               return null;
         }
         
         
   
  }

Nossa classe Impressora, presente no código da Listagem 2, implementa a interface Multifuncional e estende a classe Dispositivo. Perceba aqui que a palavra-chave implements exige o uso de uma interface logo depois. Ao colocar o trecho “implements Multifuncional” na nossa classe Impressora, somos obrigados a definir o comportamente de todos os métodos desta interface. Nesse caso estamos dizendo que nossa impressora é Multifuncional. Mas o que vem a ser uma impressora Multifuncional? Vejamos o que diz o código da Listagem 3 a seguir.

Listagem 3. Multifuncional


  package devapp;
   
  public interface Multifuncional extends Scanner, Fax, Printer, Copiadora {
   
  }

A interface Multifuncional estende Scanner, Fax, Printer e Copiadora. Isso significa que quem implementar a interface Multifuncional possui todas as funcionalidades possíveis de uma impressora. O interessante aqui é que se aparecer mais uma funcionalidade como “Filmadora”, poderíamos criar uma interface Filmadora e estender em Multifuncional, assim teremos mais uma funcionalidade adicionada sem mudar toda a estrutura do projeto.

Listagem 4. Scanner


  package devapp;
   
  import java.awt.image.BufferedImage;
   
  public interface Scanner {
         
         public BufferedImage digitalizar();
   
  }

Nossa interface Scanner da Listagem 4 define apenas o método digitalizar().

Listagem 5. Fax


  package devapp;
   
  public interface Fax {
   
         public void enviarFax(String texto, String numeroDestino);
         public String receberFax(String numeroOrigem); 
         
  }

A classe da Listagem 5 define dois métodos: enviarFax() e receberFax(), sendo que o primeiro recebe o texto a ser enviado e o número para onde deverá ser enviado. O segundo método recebe o número de origem e tenta buscar o valor enviado a partir deste número.

Listagem 6. Printer


  package devapp;
   
  public interface Printer {
         
         public void imprimir(byte[] dados, int quantidade);
   
  }

A primeira pergunta pode ser porque não chamamos de Impressora e sim de Printer na Listagem 6. Acontece que já temos uma classe chamada Impressora e não poderíamos criar uma interface de mesmo nome, sendo assim optamos por usar Printer. A nossa interface Printer define o método imprimir que recebe um array de byte onde este irá conter os dados que devem ser impressos (imagens, textos, mas tudo se resume a byte), e a quantidade de cópias que devem ser impressas.

Listagem 7. Copiadora


  package devapp;
   
  public interface Copiadora {
      
         public void copiarDocumento(int quantidade);
         
  }

A Copiadora, código da Listagem 7 define apenas o método copiarDocumento() que recebe a quantidade de cópias que devem ser impressas.

Enfim, depois de definimos toda nossa estrutura nós podemos começar a usufruir de tais recursos. Mas antes iremos criar mais uma classe chamada Util que explicaremos mais à frente. Observe a Listagem 8.

Listagem 8. Util


  ackage devapp;
   
  import java.util.Date;
   
  public class Util {
   
         // 1
         private static Impressora impressora;
   
         private static Impressora getImpressoraInstance() {
               if (impressora == null) {
                      impressora = new Impressora();
                      impressora.setCor("COR DA IMPRESSORA");
                      impressora.setDataCompra(new Date());
                      impressora.setMarca("MARCA DA IMPRESSORA");
                      impressora.setModelo("MODELO DA IMPRESSORA");
                      impressora.setNumeroSerie("12319iASAMASM");
               }
   
               return impressora;
         }
   
         // 2
         public static Scanner getScannerAtual() {
               return getImpressoraInstance();
         }
   
         // 3
         public static Fax getFaxAtual() {
               return getImpressoraInstance();
         }
   
         // 4
         public static Fax getCopiadora() {
               return getImpressoraInstance();
         }
   
         // 5
         public static Printer getPrinter() {
               return getImpressoraInstance();
         }
         
         //6
         public static Multifuncional getImpressora() {
               return getImpressoraInstance();
         }
   
  }

A classe acima irá auxiliar no uso da função correta. De qualquer lugar do nosso programa nós poderemos chamar esta classe e dizer: “Quero que você me retorne o Scanner atual”. Exatamente neste ponto não importará para nosso programa de onde vem esse Scanner, se é da própria impressora ou se uma conexão remota do Japão. Nosso programa irá usar a interface Scanner para chamar o método digitalizar(). Vejamos as explicações das linhas comentadas:

  1. Nosso objeto impressora armazena a instancia atual que foi criada da impressora, ou seja, teremos apenas uma instância por aplicação;
  2. O método getScannerAtual() retorna a impressora atual como um Scanner. Neste ponto estamos partindo do principio que o Scanner faz parte da impressora, mas se fosse aquele caso de possuir um Scanner separado? Simplesmente nós mudaríamos o comportamento do método getScannerAtual(), de alguma forma ele iria retornar o Scanner seja físico, remoto ou virtual, realmente não importa para o programa principal. Perceba que neste ponto a interface é de grande valia, pois no nosso programa principal nós usaremos apenas as interfaces, como a Scanner, sendo assim não importa de onde ela vem, apenas que ela pode ser usada para cumprir com as funções básicas de um Scanner.
    Quando chegamos neste ponto do artigo começa a ficar mais claro porque fizemos toda essa divisão e estrutura modularizada. Nosso programa tem uma estrutura pronta para funcionar de forma mais genérica possível, possibilitando a mudança radical de comportamento sem mudança de estrutura. Imagine que hoje nossa Copiadora pode ser acoplada a impressora mas amanhã nós compramos uma Copiadora profissional da XEROX e isso não fará a menor diferença para a estrutura do nosso projeto;
  3. O método getFaxAtual() retorna a impressora atual como um Fax, seguindo a mesma lógica do item 2;
  4. O método getCopiadora() retorna a impressora atual como uma Copiadora, seguindo a mesma lógica do item 2;
  5. O método getPrinter() retorna a impressora atual como uma Impressora simples (apenas função de imprimir), seguindo a mesma lógica do item 2. Não confunda com nossa classe Impressora, a nossa interface Printer possui apenas a função de imprimir, enquanto que a classe Impressora possui todas as nossas interfaces;
  6. O método getImpressora() retorna a impressora como sendo uma Impressora Multifuncional;

Listagem 9. PrincipalApp


  package devapp;
   
  import java.awt.image.BufferedImage;
   
   
  public class PrincipalApp {
   
         /**
          * @param args
          */
         public static void main(String[] args) {
   
               //1
               Scanner scanner = Util.getScannerAtual();
               BufferedImage imagemDigitalizada = scanner.digitalizar();
               
               //2
               Fax fax = Util.getFaxAtual();
               fax.enviarFax("ola mundo", "559188888888");
               String faxRecebido = fax.receberFax("559188888888");
               
               //3
               Copiadora cop = Util.getCopiadora();
               cop.copiarDocumento(10);
               
               //4
               Printer printer = Util.getPrinter();    
               String dados = "ola mundo";
               printer.imprimir(dados.getBytes(), 1);  
               
         }
   
  }

Para finalizar nós temos na Listagem 9 a classe que representa a execução do nosso programa. Vejamos os detalhes das linhas comentadas:

  1. Capturamos o Scanner atual e armazenamos em uma variável do tipo Scanner, depois chamamos o método digitalizar() na variável scanner que retorna um objeto BufferedImage. Lembre-se que interfaces não podem ser instanciadas, neste caso estamos usando ela apenas como uma “Referência” ao objeto em questão, que na verdade é uma instancia da classe Impressora mas pode ser referenciado por Scanner;
  2. Capturamos o Fax atual e armazenamos em um variável do tipo Fax, depois chamamos o método enviarFax() com os texto “ola mundo” e o número de destino no formato (+55 DDD e número). O método receberFax() nos retorna o texto lido através de uma String;
  3. Capturamos a Copiadora e executamos o método copiarDocumento() passando como parâmetro o valor 10, o que significa que serão impressas 10 cópias do documento.
  4. Capturamos o Printer, ou seja, a impressora que “apenas imprime” e executamos o método imprimir() passando os dados que queremos imprimir e a quantidade de folhas desejadas.

“Instanciando” uma interface

Citamos diversas vezes nas seções anteriores que uma interface não pode ser instanciada e naquele ponto isso era suficiente para não complicar/embaralhar os conceitos que estávamos aprendendo. E continue com a ideia que a interface não pode ser instanciada, pois não pode mesmo. Porém, existe uma forma de realizarmos tal feito, como mostra a Listagem 10.

Listagem 10. “Instanciando” interfaces


  Scanner scanner2 = new Scanner(){
   
                      @Override
                      public BufferedImage digitalizar() {
                             // TODO Auto-generated method stub
                             return null;
                      }
                      
               };

Bom, o que estamos fazendo acima parece a instanciação de uma interface, mas não é. Na verdade o que o compilador irá fazer é colocar criar a classe “PrincipalApp$1” e implementar a interface Scanner, e só depois instanciar a classe PrincipalApp$1. Assim, teremos a Listagem 11.

Listagem 11. Como o compilador “instancia” uma interface


  class PrincipalApp$1 implements Scanner{
   
                      @Override
                      public BufferedImage digitalizar() {
                             // TODO Auto-generated method stub
                             return null;
                      }
                      
                      
               }
               Scanner scanner2 = new PrincipalApp$1();

Obviamente que estamos considerando que o código da Listagem 10 está dentro da classe PrincipalApp mostrada na Listagem 9. O que fazemos na Listagem 10 é apenas um “atalho” proporcionado pelo Java, mas não muda a teoria de que uma interface não pode ser instanciada.

O objetivo deste artigo foi demonstrar o uso de interfaces na prática, usando exemplos de fácil entendimento e com base no mundo real. Vimos em detalhe desde a diagramação do projeto até a criação das classes e interfaces para definição do nosso projeto. Nosso projeto foi definido através de quatro interfaces de extrema importância: Scanner, Fax, Copiadora e Printer. Utilizamos cada uma dessas interfaces em nossa classe PrincipalApp que demonstra que não importa a origem do Scanner, o que importa é apenas conseguir chamar os métodos provenientes da sua funcionalidade, o mesmo acontece para o Fax, Copiadora e Printer.

Em muito se confunde quando usar ou não uma interface, e é por isso que um projeto de software deve gastar bastante tempo em sua fase de elaboração para que não haja dores de cabeça posteriormente, pois mudar estruturas complexas demandam tempo e acabam causando efeitos em outros módulos que não gostaríamos de mexer.

É importante salientar que estamos tratando de conceitos que se aplicam até o Java 7, pois no Java 8 há mudanças em relação a interfaces que não foi foco deste artigo.