Interfaces
• 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
|
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 * * @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); } } |
• 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
› Há 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
• 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 já 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!
• 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
• 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!
• 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
|
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!