Interfaces

Objetivos da seção

    Apresentar o conceito de Tipo Abstrato de Dado (“interface”, em Java)

    Mostrar como interfaces permitem o desacoplamento entre interface e implementação

    Mostrar como interfaces criam uma hierarquia de tipos, e permite polimorfismo

 

MPj04392390000[1]

Saber quais métodos escrever, com que parâmetros e com que valor de retorno é uma arte que os alunos vão adquirir com experiência

Mas existem regras que devem ser aprendidas bem cedo!

Iremos aprender regras de ouro neste módulo!

 

    Levando o conceito de classes abstratas ao extremo, chegamos perto das interfaces

    Interfaces definem os métodos com seus parâmetros e tipos de retorno, mas sem implementação alguma

    Quando estamos trabalhando com uma dada interface sabemos quais são os métodos que podemos chamar, o que eles recebem como parâmetros de entrada e o que ele retorna

       Mas não sabemos como foi implementado

       Não há implementação associada

       É como se fosse um acordo de uso (um protocolo)

    Vejamos a implementação da interface FiguraGeometrica

public interface FiguraGeometricaIF extends Comparable{

 

   /**

    * Retorna a área da figura geométrica.

    */

   public double getArea();

 

   /**

    * Desenha a figura geométrica na tela.

    */

   public void desenha();

 

   /**

    * Compara a área desta figura geométrica com a de outra figura
    * geométrica.

    *

    * @param p

    *            A figura geométrica que será usada para a comparação.

    * @return 0 se as áreas forem iguais, -1 se esta figura tiver área menor

    *         que a área da figura p; e 1 caso contrário.

    */

   public int compareTo(FiguraGeometricaIF p);

 

   /**

    * Compara esta figura geométrica com outra para identificar se são ou não

    * iguais. Para que sejam iguais elas precisam ser do mesmo tipo e ter as

    * mesmas configurações.

    *

    * @param obj

    *            O objeto a ser comparado com este para igualdade.

    * @return true se eles forem iguais e false caso contrário.

    */

   public boolean equals(Object obj);

 

   /**

    * Indica a repreentação textual desta figura geométrica.

    * @return  A representação textual desta figura geométrica.

    */

   public String toString();

 

   /**

    * Retorna o perímetro desta figura geométrica.

    * @return  O perímetro desta figura geométrica.

    */

   public double getPerimetro();

 

}

    Note que temos uma palavra reservada nova: interface

    Como não existe implementação, não é possível fazer um new desta interface

    O que fizemos até agora foi apenas definir um tipo

       Isso só é útil se fizermos mais duas coisas

i)   Fornecer uma ou mais implementações desta interface

ii)          Usar o tipo definido (e não suas implementações ao definir as nossas referências a objetos)

    Vamos então criar uma classe que implementa esta interface?

package p2.exemplos;

 

 

public class Circunferencia implements FiguraGeometricaIF {

 

   private Ponto2D centro;

   private double raio;

 

   public Circunferencia() {

       centro = new Ponto2D(0, 0);

       raio = 1;

   }

 

   public Circunferencia(Ponto2D centro, double raio) throws Exception {

       if (raio <= 0.0)

          throw new Exception("O raio de um circulo nao pode ser nulo!");

       this.centro = centro;

       this.raio = raio;

   }

 

   public void desenha() {

       System.out.println("Desenhando um circulo.");

   }

 

   @Override

   public boolean equals(Object obj) {

       if (obj instanceof Circunferencia) {

          return false;

       }

       Circunferencia c = (Circunferencia) obj;

       return getCentro().equals(c) && getRaio() == c.getRaio();

   }

 

   public double getArea() {

       return Math.PI * raio * raio;

   }

 

   @Override

   public String toString() {

       return "Circulo de raio " + getRaio() + " e centro "

             + getCentro().toString() + ".";

   }

 

   public Ponto2D getCentro() {

       return centro;

   }

 

   public void setCentro(Ponto2D centro) {

       this.centro = centro;

   }

 

   public double getRaio() {

       return raio;

   }

 

   public void setRaio(double raio) {

       if (raio > 0.0)

          this.raio = raio;

   }

 

   public double getPerimetro() {

       return 2 * Math.PI * raio;

   }

 

   public int compareTo(Object obj) {

       if (obj instanceof Circunferencia) {

          throw new ClassCastException();

       }

       Circunferencia outra = (Circunferencia) obj;

       return ((int)this.getRaio() - (int)outra.getRaio());

   }

}

    Veja como é simples implementar uma interface (parece muito com estender uma classe)

       Só mudou a cláusula implements FiguraGeometricaIF que significa que esta classe implementa a interface FiguraGeometricaIF

       implements é uma palavra reservada

       Isso nos obriga a implementar cada método que pertence à interface

i)    O compilador ajudará você a garantir isso

    Qualquer objeto da classe Circulo pode ser tratado como se fosse do tipo FiguraGeometricaIF

    Observe também que há métodos implementados pela classe que não fazem parte da interface (quais, por exemplo?)

    Agora vamos criar um programa que desenha uma circunferência

package p2.exemplos;

 

public class DesenhaFiguras {

  

   public static void desenha( FiguraGeometricaIF figura ) {

      figura.desenha();

   }

 

   /**

    * @param args

    */

   public static void main(String[] args) {

      FiguraGeometricaIF figura = new Circunferencia();

      desenha(figura);

   }

 

}

Manutenção de programas

    Vamos fazer “manutenção” no nosso programa

       Manutenção é uma atividade extremamente comum feita por programadores

       Software não é uma coisa estática que não muda depois de feita

       sempre mudanças a fazer em programas que são utilizados

       Programas que não precisam mudar mais morreram: ninguém os está utilizando

    Nosso problema de manutenção: nosso usuário deseja manipular novos tipos de figuras geométricas

       Para simplificar nosso trabalho, vamos manipular retângulos (isso é simples!)

    A primeira coisa que fazemos é implementar a classe Retangulo:

package p2.exemplos;

 

public class Retangulo implements FiguraGeometricaIF {

   private double lado1;

   private double lado2;

 

   public Retangulo(double lado1, double lado2) {

       this.lado1 = lado1;

       this.lado2 = lado2;

   }

 

   public Retangulo() {

       lado1 = 1.0;

       lado2 = 2.0;

   }

 

   // este método é idêntico ao método compareTo de circunferencia!

   @Override

   public int compareTo(Object obj) {

       if (!(obj instanceof Retangulo)) {

          throw new ClassCastException();

       }

       Retangulo outro = (Retangulo) obj;

       if (getArea() == outro.getArea())

          return 0;

       if (getArea() > outro.getArea())

          return 1;

       return -1;

   }

 

   public void desenha() {

       System.out.println("Desenhando um retangulo.");

   }

 

   public double getArea() {

       return getLado1() * getLado2();

   }

 

   public double getPerimetro() {

       return 2 * getLado1() + 2 * getLado2();

   }

 

   @Override

   public boolean equals(Object obj) {

       if (obj instanceof Retangulo) {

          return false;

       }

 

       Retangulo r = (Retangulo) obj;

 

       if (getLado1() == r.getLado1() && getLado2() == r.getLado2())

          return true;

       if (getLado1() == r.getLado2() && getLado2() == r.getLado1())

          return true;

 

       return false;

   }

 

   @Override

   public String toString() {

       return "Retangulo de lados " + getLado1() + " e " + getLado2() + ".";

   }

 

   /**

    * @return the lado1

    */

   public double getLado1() {

       return lado1;

   }

 

   /**

    * @param lado1

    *            the lado1 to set

    */

   public void setLado1(double lado1) {

       this.lado1 = lado1;

   }

 

   /**

    * @return the lado2

    */

   public double getLado2() {

       return lado2;

   }

 

   /**

    * @param lado2

    *            the lado2 to set

    */

   public void setLado2(double lado2) {

       this.lado2 = lado2;

   }

}

    Agora vamos mudar o programa que desenha figuras! Agora queremos que ele desenhe um retângulo e não mais uma circunferência.

package p2.exemplos;

 

public class DesenhaFiguras {

  

   public static void desenha( FiguraGeometricaIF figura ) {

      figura.desenha();

   }

 

   /**

    * @param args

    */

   public static void main(String[] args) {

      FiguraGeometricaIF figura = new Retangulo();

      desenha(figura);

   }

 

}

    Mudamos completamente o comportamento do programa, mudando apenas a palavra Circunferencia pela palavra Retangulo!!!!

    Quero que vocês me expliquem por que essa mudança foi tão simples! (valendo chocolates!)

       As interfaces definem novos tipos

       As interfaces também participam de uma hierarquia de tipos

       Bolamos boas interfaces (definimos bem o vocabulário do problema)

       Implementamos a interface

       Usamos o tipo (interface) ao declarar objetos e não a classe que implementa a interface

       Escrevemos chamadas polimórficas

Um repositório de figuras geométricas

    Agora queremos criar uma abstração que é um repositório de figuras geométricas

    Deve ser possível adicionar e remover figuras neste repositório

    Deve ser possível desenhar todas as figuras de uma só vez

    Deve ser possível desenhar figuras de um certo tipo

       As figuras podem ser rotuladas pelo seu nome típico (quadrado, retângulo, etc.)

       Assim, pode-se desenhar um quadrado, retângulo, etc.

    Vamos ao código?

       Como esta classe deve ser chamada?

       Que atributos ela deve ter?

       Que métodos queremos escrever?

package p2.exemplos;

 

import java.util.HashMap;

import java.util.Iterator;

import java.util.Map;

 

/**

 * Um repositório de figuras geométricas.

 *

 * @author Raquel Lopes

 *

 */

public class RepositorioDeFiguras {

 

   private Map<String, FiguraGeometricaIF> repositorio;

 

   /**

    * Cria um repositório vazio de figuras geométricas.

    */

   public RepositorioDeFiguras() {

      repositorio = new HashMap<String, FiguraGeometricaIF>();

   }

 

   /**

    * Adiciona uma figura no repositório. Cada figura é identificada por seu

    * rótulo.

    *

    * @param rotulo

    *            A identificação da figura. Caso existe no repositório

    *            uma figura com este rotulo, esta figura será substituída

    *            pela nova figura.

    * @param figura

    *            A figura a ser adicionada.

    */

   public void adicionaFigura(String rotulo, FiguraGeometricaIF figura) {

      repositorio.put(rotulo, figura);

   }

 

   /**

    * Remove do repositório a figura geométrica que está associada ao rótulo

    * especificado.

    *

    * @param rotulo

    *            O rótulo da figura geométrica a ser removida do

    *            repositório.

    */

   public void removeFigura(String rotulo) {

      repositorio.remove(rotulo);

   }

 

   /**

    * Imprime na saída padrão todos os rótulos das figuras existentes no

    * repositório.

    */

   public void listaFigurasDoRepositorio() {

      Iterator<String> it = repositorio.keySet().iterator();

      while (it.hasNext()) {

         System.out.println(it.next());

      }

   }

 

   /**

    * Se existir, desenha a figura geométrica associada ao rótulo

    * especificado.

    *

    * @param rotulo

    *            O rótulo da figura geométrica a ser desenhada.

    */

   public void desenha(String rotulo) {

      if (repositorio.containsKey(rotulo))

         repositorio.get(rotulo).desenha();

   }

 

   /**

    * Desenha todas as figuras geométricas presentes no repositório.

    */

   public void desenhaTodasAsFiguras() {

      Iterator<FiguraGeometricaIF> it = repositorio.values().iterator();

      while (it.hasNext()) {

         it.next().desenha();

      }

   }

}

 

    Note que em nenhum momento acoplamos o repositório a implementação qualquer tipo específico de figura geométrica

       O repositório é capaz de gerenciar qualquer figura geométrica: as que já existem… e todas as que estão por vir! ;-) 

    Quer ver?

    Vamos agora escrever uma figura geométrica que é um quadrado

package p2.exemplos;

 

/**

 * Um quadrado é um quadrilátero que possui todos os lados igueis.

 *

 * @author Raquel Lopes

 *

 */

public class Quadrado extends Retangulo {

 

   /**

    * Cria um quadrado de lados 1.

    */

   public Quadrado() {

      super(1.0, 1.0);

   }

 

   @Override

   public String toString() {

      return "Quadrado de lados " + getLado1() + " e " + getLado2() + ".";

   }

 

   @Override

   public void setLado1(double lado) {

      setLado(lado);

   }

 

   @Override

   public void setLado2(double lado) {

      setLado(lado);

   }

 

   /**

    * Atribui novo valor aos lados do quadrado.

    *

    * @param lado

    *            O novo valor dos lados do quadrado.

    */

   public void setLado(double lado) {

      setLado1(lado);

      setLado2(lado);

   }

}

    O que precisa mudar na classe RepositorioDeFiguras para que a nova figura do tipo Quadrado também possa ser armazenada no repositório?

       Absolutamente NADA!

Representação em UML

    Em UML, interfaces são representadas de duas formas

       Com definição completa, como “classe” usando o estereótipo <<interface>>

       De forma resumida, usando uma bola

    Exemplo: nosso último programa

    Exemplo: nosso último programa mas usando a forma abreviada de interface

Qual é a diferença entre uma classe abstrata e uma interface?

    Uma classe abstrata também define um tipo abstrato de dados

    Uma classe abstrata pode ter implementação de alguns métodos

       Uma interface não

    Uma classe abstrata também define uma interface (seus métodos públicos)

    Para reusar o contrato de uma classe abstrata temos que estendê-la e implementar os métodos abstratos e/ou sobrescrever os métodos que devem apresentar comportamento diferente do herdado

       Isto causa uma relação “é um” entre a classe abstrata e a classe derivada dela

       Muitas vezes esta relação é muito forte para ser usada!

    Para reusar o contrato de uma interface temos que implementar os métodos que ela define

       Isto causa um relacionamento “é como um” entre a interface e a classe que a implementa

       A nova classe age como um …

i)    Raquel age como uma mãe…

ii)Raquel age como uma filha…

iii)        Raquel age como uma professora…

iv)        Raquel age como uma aluna… (na aula de francês!)

v)Raquel pode acumular vários papéis ao mesmo tempo

    Peculiaridade de Java:

       Em se tratando da hierarquia de classes apenas herança simples é permitida

       Em se tratando da hierarquia de tipos abstratos (interfaces) heran~ca múltipla é permitida

i)    Uma interface pode estender várias interfaces simultaneamente ou

ii)Uma interface pode herdar de várias interfaces simultaneamente

iii)        Isto é simples porque não há implementação. Assim, se dois métodos tiverem a mesma assinatura, apenas uma implementação será oferecida

       Como sugerido no exemplo dos papéis acumulados por Raquel, uma classe pode implementar várias interfaces simultaneamente

       Vejamos o código disso!

package p2.exemplos;

 

public interface MaeIF {

 

   public void manda();

 

}

 

package p2.exemplos;

 

public interface FilhaIF {

 

   public void obedece();

 

}

 

package p2.exemplos;

 

public interface ProfessoraIF {

 

   public void daAula();

   public void preparaAula();

  

}

 

package p2.exemplos;

 

public interface AlunaIF {

 

   public void estudaParaProva();

   public void participaDeAula();

   public void alunaFazProva();

 

}

 

package p2.exemplos;

 

public class MulherOcupada extends Pessoa implements MaeIF, FilhaIF, ProfessoraIF,

      AlunaIF {

 

   public MulherOcupada(String nome) {

      super(nome);

   }

 

   public void manda() {

      System.out.println("Mandando como mae!");

   }

 

   public void obedece() {

      System.out.println("Obedecendo como filha!");

   }

 

   public void daAula() {

      System.out.println("Professora dando aula.");

   }

 

   public void preparaAula() {

      System.out.println("Professora preparando aula.");

   }

  

   public void estudaParaProva() {

      System.out.println("Aluna estuda para a prova!");

   }

  

   public void participaDeAula() {

      System.out.println("Aluna participa de aula.");

   }

  

   public void alunaFazProva() {

      System.out.println("Aluna fazendo prova.");

   }

 

}

 

package p2.exemplos;

 

public class DiaDaMulherOcupada {

 

   private static void manda(MaeIF mae) {

      mae.manda();

   }

 

   private static void obedece(FilhaIF filha) {

      filha.obedece();

   }

 

   private static void preparaAula(ProfessoraIF professora) {

      professora.preparaAula();

   }

 

   private static void estudaParaProva(AlunaIF aluna) {

      aluna.estudaParaProva();

   }

 

   public static void main(String[] args) {

      MulherOcupada raquel = new MulherOcupada("Raquel");

      manda(raquel); // trata raquel como uma MaeIF

      obedece(raquel); // trata raquel como uma FilhaIF

      preparaAula(raquel); // trata raquel como uma ProfessoraIF

      estudaParaProva(raquel); // trata raquel como uma AlunaIF

   }

 

}

    Em situações como essa, uma flexibilidade que as interfaces nos oferecem é a de poder fazer o upcast para mais de um tipo base

       No exemplo acima fizemos o upcast do mesmo objeto para quatro diferentes tipos base!

    Como exemplo, podemos ter uma interface que estende várias outras

       No código acima, poderíamos ter criado a interface da mulher ocupada que estende MaeIF, FilhaIF, ProfessoraIF e AlunaIF

       Depois implementaríamos apenas esta interface. Também teria dado certo. Tente isto em casa!

Mas afinal, devemos usar interfaces ou classes abstratas?

    Depende! Se na sua classe base não existe implementação de método… Será uma interface!

    Se você acha que existe comportamento comum que deve estar na classe base… Use classe abstrata

Princípio importante de projeto orientado a objetos

MPj04393430000[1]

Program to an interface, not to an implementation!

                          Erich Gamma

Grande parte deste material  que segue foi retirado da entrevista concedida por Erich Gamma a Tim Bernners (aqui).

    O que você acha que isto significa? Vamos ver?

    Quando reusamos estabelecemos dependências

    Usar interfaces faz você depender de interfaces e não de implementações, o que é uma dependência saudável

    Mas o que significa usar uma interface?

       Significa declarar a variável como sendo do tipo da interface, mas ao fazer o new, obviamente você irá usar uma implementação da interface

    Uma interface define a colaboração entre objetos

       Uma interface não tem detalhes de execução

       Uma interface define o “vocabulário” da colaboração

       Uma vez que você entende as interfaces, você entende mais do sistema porque você compreende o vocabulário do problema

       Vocabulário = abstrações + métodos (mensagens)

    Como aprendemos a lidar com o pacote Collections?

       Entendemos como trabalhar com as interfaces…

    Toda classe faz parte de uma hierarquia de tipos

       A idéia é que quanto mais alto estivermos na hierarquia, mais abstrato é o tipo (até chegarmos nas interfaces!)

       Quanto mais descemos na hierarquia, mais concretas vão ficando as classes

       Esta regra diz que você deve declarar seus objetos (suas referências, na realidade!) como sendo do nível mais alto possível da hierarquia. Vá até o nível mais alto onde você possa chegar!!!

       Assim você estará desacoplado de implementações específicas. Novas implementações deste mesmo tipo abstrato podem ser facilmente incorporadas em seu programa

    Vejamos um exemplo

package p2.exemplos;

 

/**

 * Define a interface de um instrumento

 * @author Raquel Lopes

 *

 */

public interface Instrumento {

 

   public enum Nota {

      C, D, E, F, G, A, B; //muitas outras notas aqui!

   }

  

   public void toca(Nota n);

   public void afina();

}

 

package p2.exemplos;

 

/**

 * Classe que representa o instrumento musical saxofone.

 * @author Raquel Vigolvino Lopes

 *

 */

public class Sax implements Instrumento {

 

   @Override

   public void toca(Nota n) {

      System.out.println("Sax.toca() " + n);

   }

 

   @Override

   public void afina() {

      System.out.println("Afina sax.");

     

   }

 

   @Override

   public String toString() {

      return "Um Sax";

   }

}

 

package p2.exemplos;

 

/**

 * Classe que representa o instrumento musical Violao

 * @author Raquel Vigolvino Lopes

 *

 */

public class Violao implements Instrumento {

 

   @Override

   public void toca(Nota n) {

      System.out.println("Violao.toca() " + n);

   }

 

   @Override

   public void afina() {

      System.out.println("Afina violao.");

   }

 

   @Override

   public String toString() {

      return "Um Violao";

   }

}

 

package p2.exemplos;

 

/**

 * Classe que representa o instrumento musical flauta.

 * @author Raquel Lopes

 *

 */

public class Flauta implements Instrumento {

 

   @Override

   public void toca(Nota n) {

      System.out.println("Flauta.toca() " + n);

   }

 

   @Override

   public void afina() {

      System.out.println("Afina flauta.");

   }

 

   @Override

   public String toString() {

      return "Uma Flauta";

   }

}

 

package p2.exemplos;

 

/**

 * Classe que representa o instrumento musical baixo.

 * @author Raquel Lopes

 *

 */

public class Baixo implements Instrumento {

 

   @Override

   public void toca(Nota n) {

      System.out.println("Baixo.toca() " + n);

   }

 

   @Override

   public void afina() {

      System.out.println("Afina baixo.");

   }

  

   @Override

   public String toString() {

      return "Um Baixo";

   }

}

 

package p2.exemplos;

 

import p2.exemplos.Instrumento.Nota;

 

/**

 * Uma orquestra formada por 4 instrumentos e que que toca uma nota depois que

 * os instrumentos estão afinados.

 *

 * @author Raquel Lopes

 *

 */

public class Orquestra {

 

   private Instrumento[] orquestra = { new Baixo(), new Violao(), new Sax(),

         new Flauta() };

 

   public void afinaInstrumentos() {

      for (Instrumento instrumento : orquestra) {

         instrumento.afina();

      }

   }

 

   public void toca() {

      for (Instrumento instrumento : orquestra) {

         instrumento.toca(Nota.C);

      }

   }

 

   public void apresentacao() {

      for (Instrumento instrumento : orquestra) {

         System.out.println(instrumento);

      }

   }

 

   /**

    * @param args

    */

   public static void main(String[] args) {

      Orquestra orquestra = new Orquestra();

      orquestra.afinaInstrumentos();

      orquestra.apresentacao();

      orquestra.toca();

   }

 

}

    A saída deste programa é como segue

Afina baixo.

Afina violao.

Afina sax.

Afina flauta.

Um Baixo

Um Violao

Um Sax

Uma Flauta

Baixo.toca() C

Violao.toca() C

Sax.toca() C

Flauta.toca() C

    Note que independente de Instrumento ser uma classe regular, uma classe abstrata ou uma interface, o comportamento do programa Orquestra seria o mesmo

       Não existem evidencias sobre o tipo de definição de instrumento

    Note que não foi preciso declarar o método toString na interface, pois este método já aparece em Object é a raiz da hierarquia de classes

    Apesar de tudo que foi dito, cuidado para não sair criando interfaces sem necessidade!

       Refactoring!

 

    Design patterns interessantes de serem estudados neste momento: Strategy e Factory Method!

 

Voltar