Regras básicas de projeto
Material cedido por Jacques Saúvé (poucas alterações no
original)
• Aprender algumas das regras básicas nas quais o programador deve se apoiar ao projetar software
› Buscamos princípios de um bom projeto OO
• Acompanhar um exemplo de refatoramento de software, transformando um software de qualidade pobre em um de melhor qualidade
• O que é Design?
› É uma das partes mais difíceis da programação
• Consiste em criar abstrações
› Isto significa três coisas:
i) Quais classes devem ser criadas?
ii)Quais responsabilidades (métodos) devem ser assumidas por cada classe?
iii) Quais são os relacionamentos entre tais classes e objetos dessas classes?
• Criar boas abstrações é difícil e vem com experiência
› Porém, algumas regras básicas ajudarão a adquirir a experiência mais rapidamente
• Responsabilidades são obrigações de um tipo ou de uma classe
› Obrigações de fazer algo
i) Fazer algo a si mesmo
ii)Iniciar ações em outros objetos
iii) Controlar ou coordenar atividades em outros objetos
› Obrigações de conhecer algo
i) Conhecer dados encapsulados
ii)Conhecer objetos relacionados
iii) Conhecer coisas que ele pode calcular
• Exemplos
› Uma Conta bancária tem a responsabilidade de “logar” as transações (fazer algo)
› Uma Conta bancária tem a responsabilidade de saber sua data de criação (conhecer algo)
• Lembre de Saint-Exupéry:
› “Atingimos a perfeição não quando nada pode acrescentar-se a um projeto mas quando nada pode retirar-se”
• Esta é a MegaRegra
• Qual é o princípio mais fundamental para atribuir responsabilidades?
› É este: Atribuir uma responsabilidade ao expert de informação - a classe que possui a informação necessária para preencher a responsabilidade
› Exemplo: Entre as seguintes classes do mundo bancário, (Agencia, Conta, ContaCaixa, ContaSimples, Extrato, ExtratoHTML, Moeda, Movimento, Real, Transacao), quem deve ser responsável pela responsabilidade “Localizar a conta com certo número”?
i) Perguntamos: onde estão
guardadas as Contas? (onde estão os dados)
ii)Estão na Agencia
iii)
Portanto,
a classe Agencia deve ter a responsabilidade (através do método
localizarConta(int número))
› Consequências
i) A encapsulação é mantida, já que objetos usam sua própria informação para cumprir suas responsabilidades
ii)Leva a fraco acoplamento entre objetos e sistemas mais robustos e fáceis de manter
› Leva a alta coesão, já que os objetos fazem tudo que é relacionado à sua própria informação
• Também conhecido como:
› "Quem sabe, faz"
› "Expert"
› "Animação" (objetos são vivos e podem assumir qualquer responsabilidade, mesmo que sejam passivos no mundo real)
i) Exemplo: No mundo bancário real, uma agência é algo passivo e não "localiza contas"
› "Eu mesmo faço"
› "Colocar os serviços junto aos atributos que eles manipulam"
• O problema:
› Como minimizar dependências e, conseqüentemente, maximizar o reuso?
› O acoplamento
é uma medida de quão fortemente uma classe está conectada, possui conhecimento
ou depende de outra classe
› Com fraco acoplamento, uma classe não é dependente de muitas outras classes
› Com uma classe possuindo forte acoplamento, temos os seguintes problemas:
i) Mudanças a uma classe relacionada força mudanças locais à classe
ii)A classe é mais difícil de entender isoladamente
iii) A classe é mais difícil de ser reusada, já que depende da presença de outras classes
•
A
solução: Atribuir responsabilidades de forma a
minimizar o acoplamento
• Discussão
› Minimizar acoplamento é um dos princípios de ouro do projeto OO
› Acoplamento de manifesta de várias formas:
i) X tem um atributo que referencia uma instância de Y
ii)X tem um método que referencia uma instância de Y
(1) Pode ser parâmetro, variável local, objeto retornado pelo método
iii) X é uma subclasse direta ou indireta de Y
iv) X implementa a interface Y
› A herança é um tipo de acoplamento particularmente forte
› Não se deve minimizar acoplamento criando alguns poucos objetos monstruosos (God classes)
i) Exemplo: todo o comportamento numa classe e outras classes usadas como depósitos passivos de informação
• Exemplo: Ordenação de registros de alunos por matrícula e nome
class Aluno {
String nome;
long matrícula;
public String
getNome() { return nome; }
public long
getMatrícula() { return matrícula; }
// etc. } ListaOrdenada
listaDeAlunos = new
ListaOrdenada(); Aluno
novoAluno = new Aluno(...); //etc. listaDeAlunos.add(novoAluno); |
• Agora vejamos os problemas
class ListaOrdenada { Object[] elementosOrdenados = new
Object[tamanhoAdequado]; public void add(Aluno x)
{
// código
não mostrado aqui //
... long matrícula1 = x.getMatrícula(); long matrícula2 = elementosOrdenados[k].getMatrícula(); if(matrícula1 < matrícula2) { //
faça algo } else { //
faça outra coisa } } } |
• O problema da solução anterior é que há forte acoplamento
› ListaOrdenada sabe muita coisa de Aluno
i) O fato de que a comparação de alunos é feito com a matrícula
ii)O fato de que a matrícula é obtida com getMatrícula()
iii) O fato de que matrículas são long (representação de dados)
iv) Como comparar matrículas (com <)
› O que ocorre se mudarmos qualquer uma dessas coisas?
• Solução 2: mande uma mensagem para o próprio objeto se comparar com outro
class ListaOrdenada { Object[] elementosOrdenados = new
Object[tamanhoAdequado]; public void add(Aluno x)
{
// código
não mostrado //
... if(x.compareTo(elementosOrdenados[K]) < 0) { //
faça algo } else { //
faça outra coisa } } } |
• Reduzimos o acoplamento escondendo informação atrás de um método
• Problema: ListaOrdenada só funciona com Aluno
• Solução 3: use interfaces para desacoplar mais ainda
interface Comparable {
public int compareTo(Object outro); } class Aluno implements Comparable {
public int compareTo(Object outro) {
// compare
registro de aluno com outro return ... } } class ListaOrdenada { Object[] elementosOrdenados = new
Object[tamanhoAdequado]; public void
add(Comparable x) {
// código
não mostrado if(x.compareTo(elementosOrdenados[K]) < 0) { //
faça algo } else { //
faça outra coisa } } } |
• Outro exemplo de redução de acoplamento: polimorfismo com interfaces
• Temos vários tipos de composites (coleções) que não pertencem a uma mesma hierarquia
› ColeçãoDeAlunos
› ColeçãoDeProfessores
› ColeçãoDeDisciplinas
• Temos um cliente comum dessas coleções
› Digamos um selecionador de objetos usado numa interface gráfica para abrir uma list box para selecionar objetos com um determinado nome
• Exemplo:
› Quero listar todos os alunos com nome "João" e exibi-los numa list box para escolha pelo usuário
› Idem para listar professores com nome "Alfredo"
› Idem para listar disciplinas com nome "Programação"
› Queremos fazer um único cliente para qualquer uma das coleções
• O exemplo abaixo tem polimorfismo em dois lugares
interface SelecionávelPorNome { Iterator<Nomeável>
getIteradorPorNome(String nome); } interface Nomeável { String getNome(); } classe
ColeçãoDeAlunos implements
SelecionávelPorNome { //
... Iterator<Nomeável> getIteradorPorNome(String
nome) { //
... } } classe
Aluno implements Nomeável { //
... String getNome() { ... } } classe
ColeçãoDeProfessores implements
SelecionávelPorNome { //
... Iterator<Nomeável> getIteradorPorNome(String
nome) { //
... } } classe
Professor implements Nomeável { //
... String getNome() { ... } } classe
ColeçãoDeDisciplinas implements
SelecionávelPorNome { //
... Iterator<Nomeável> getIteradorPorNome(String
nome) { //
... } } classe
Disciplina implements Nomeável { //
... String getNome() { ... } } classe
ComponenteDeSeleção { Iterator<Nomeável> it; //
observe o tipo do parâmetro (uma interface) public ComponenteDeSeleção(SelecionávelPorNome coleção,
String nome) { it =
coleção.getIteradorPorNome(nome); //
chamada polimórfica }
// ...
void geraListBox()
{
response.out.println("<select name=\"nome\"
size=\"1\">");
while(it.hasNext())
{ int i = 1; // observe o tipo do objeto Nomeável obj = it.next(); response.out.println("<option value=\"escolha" + i + "\">" + obj.getNome()
+ // chamada polimórfica "</option>");
}
response.out.println("</select>");
} } //
Como usar o código acima num servlet: //
supõe que as coleções usam o padrão Singleton ComponenteDeSeleção cds = new ComponenteDeSeleção(
ColeçãoDeAlunos.getInstance(), "João"); cds.geraListBox(); cds = new
ComponenteDeSeleção(ColeçãoDeDisciplinas.getInstance(), "Programação"); cds.geraListBox(); |
• O problema:
› Como gerenciar a complexidade?
› A coesão mede quão relacionados ou focados
estão as responsabilidades da classe
i) Também chamado coesão funcional
› Uma classe com baixa coesão faz muitas coisas não relacionadas e leva aos seguintes problemas:
i) Difícil de entender
ii)Difícil de reusar
iii) Difícil de manter
iv) É “delicada”: constantemente sendo afetada por outras mudanças
› Uma classe com baixa coesão assume responsabilidades que pertencem a outras classes e deveriam ser delegadas a outras
• Solução:
› Atribuir responsabilidades que mantenham alta coesão
• Uma classe deve ter um pequeno número de variáveis e métodos que manipulam essas variáveis
› Coesão máxima ocorre quando todas as variáveis são manipuladas por todos os métodos da classe
› Quando a coesão é alta dizemos que os métodos e variáveis da classe são co-dependentes e formam uma unidade lógica
• Olhar coesão pode ajudar a quebrar classes
› Quebrar um método grande em vários menores pode gerar novas classes
› Quando descobrimos que eles manipulam conjuntos disjuntos de variáveis
› Geralmente descobrimos que a classe tem responsabilidades demais
• Exemplo: O que acha da classe que segue?
class Angu {
List números;
String texto;
public static int acharPadrão(String padrão) {
// ... lida
com texto aqui } public static int
média() { //
... lida com números aqui } public static outputStream abreArquivo(string nomeArquivo) { //
... } } class Xpto extends Angu { //
quer aproveitar código de Angu ... } |
• Classes com alta coesão são preferíveis: mais robustas, mais confiáveis mais fáceis de serem reusadas e entendidas;
› A coesão baixa resulta em código difícil de manter, de testar, de reusar e de entender!
› Classes devem ser pequenas. Quanto é pequeno?
› Mede-se o tamanho de uma classe pela quantidade de responsabilidades que elas têm
i) Deve ser possível descrever a classe rapidamente com umas 25 palavras
ii)Dar um nome à classe deve ser fácil
(1) O nome descreve suas responsabilidades
(2) O nome ajuda a determinar o tamanho de classe
(3) Se estiver difícil nomear a classe, desconfie de que existe um problema!
iii) O ideal é que uma classe deve ter apenas uma razão para mudar
(1) Classe que compila e imprime relatórios
(2) Pode mudar por diferentes razões
• Esta classe deve ser quebrada em pelo menos 2 outras!
Se você escreve classes
pequenas, coesas e que se relacionam com poucas outras classes também pequenas e
coesas; e se estas classes não dependerem fortemente umas das outras, então
você está no caminho certo.
• Como programador, você precisa treinar seu nariz para detectar “mau cheiro” em código
• Veremos um exemplo disso agora
• Melhoraremos um programa através de refatoramento
› Refatoramento altera um programa mas sem
afetar a funcionalidade que ele oferece
• Refatoramento sempre deve ser feito apoiando-se em Testes de Unidade para assegurar-se de que as transformações no código não quebrem código que funciona
› Não mostraremos os testes de unidade aqui por questão de tempo
• Referência: Livro de Fowler: “Refactoring”
• O programa é simples: ele calcula e emite um extrato de um cliente numa locadora de vídeo
• Vamos executar o programa para ver uma saída possível:
Registro
de Alugueis de Guilherme Lopes Batman 14.0 George, o curioso 1.5 O espetacular homem aranha 90.0 Power Rangers Mistic Force 12.0 Charlie e
Lola 12.0 Uma noite no museu
3.5 Valor
total devido: 133.0 Voce acumulou 8 pontos de alugador frequente |
• Eis o diagrama UML das classes principais
• A classe DVD é usada apenas para conter os atributos
package p2.exemplos.locadora; public class DVD { public static final int NORMAL = 0; public static final int LANÇAMENTO = 1; public static final int INFANTIL = 2; private String título; private int
códigoDePreço; public DVD(String título, int códigoDePreço) { this.título = título; this.códigoDePreço = códigoDePreço; } public String getTítulo() {
return título; } public int
getCódigoDePreço() { return códigoDePreço; } public void
setCódigoDePreço(int
códigoDePreço) { this.códigoDePreço = códigoDePreço; } } |
• A classe Aluguel representa o aluguel de um DVD por um certo número de dias
package p2.exemplos.locadora; public class
Aluguel { private DVD dvd; private int
diasAlugado; public Aluguel(DVD dvd, int diasAlugado) { this.dvd = dvd; this.diasAlugado = diasAlugado; } public DVD getDVD() { return dvd; } public int
getDiasAlugado() { return diasAlugado; } } |
• A classe Cliente representa um freguês da locadora de vídeo
package p2.exemplos.locadora; import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class Cliente { private String nome; private List<Aluguel> dvdsAlugados = new ArrayList<Aluguel>(); public Cliente(String nome) {
this.nome = nome;
} public String getNome() {
return nome; } public void
adicionaAluguel(Aluguel aluguel) { dvdsAlugados.add(aluguel); } public String extrato() { final String fimDeLinha = System.getProperty("line.separator");
double valorTotal =
0.0; int pontosDeAlugadorFrequente = 0; Iterator<Aluguel> alugueis = dvdsAlugados.iterator(); String resultado = "Registro de Alugueis de " + getNome() + fimDeLinha; while(alugueis.hasNext()) { double valorCorrente = 0.0; Aluguel cada = alugueis.next(); //
determina valores para cada linha switch(cada.getDVD().getCódigoDePreço()) { case DVD.NORMAL: valorCorrente += 2; if(cada.getDiasAlugado() > 2) { valorCorrente += (cada.getDiasAlugado()
- 2) * 1.5; } break; case DVD.LANÇAMENTO: valorCorrente +=
cada.getDiasAlugado() * 3; break; case DVD.INFANTIL: valorCorrente += 1.5; if(cada.getDiasAlugado() > 3) { valorCorrente +=
(cada.getDiasAlugado() - 3) * 1.5; } break; } //switch //
trata de pontos de alugador frequente pontosDeAlugadorFrequente++; //
adiciona bonus para aluguel de um lançamento por pelo menos 2 dias if(cada.getDVD().getCódigoDePreço() == DVD.LANÇAMENTO && cada.getDiasAlugado() > 1) { pontosDeAlugadorFrequente++; } //
mostra valores para este aluguel resultado += "\t"
+ cada.getDVD().getTítulo() + "\t" + valorCorrente + fimDeLinha; valorTotal += valorCorrente; } //
while //
adiciona rodapé resultado += "Valor total devido: " + valorTotal + fimDeLinha; resultado += "Voce acumulou " + pontosDeAlugadorFrequente + "
pontos de alugador frequente"; return resultado; } } |
• Finalmente, a classe Locadora exercita o programa
package p2.exemplos.locadora; public class
Locadora { public static void main(String[] args) { Cliente c1 = new Cliente("Guilherme
Lopes"); c1.adicionaAluguel(new Aluguel(new DVD( "Batman ", DVD.NORMAL), 10)); c1.adicionaAluguel(new Aluguel(new DVD( "George,
o curioso ", DVD.INFANTIL), 2)); c1.adicionaAluguel(new Aluguel(new DVD( "O
espetacular homem aranha ", DVD.LANÇAMENTO), 30)); c1.adicionaAluguel(new Aluguel(new DVD( "Power Rangers Mistic Force ", DVD.LANÇAMENTO), 4)); c1.adicionaAluguel(new Aluguel(new DVD( "Charlie
e Lola ", DVD.INFANTIL), 10)); c1.adicionaAluguel(new Aluguel(new DVD( "Uma
noite no museu ", DVD.NORMAL), 3)); System.out.println(c1.extrato()); } } |
• Nosso exemplo é pequeno devido a restrições de tempo, mas imagine o que ocorreria se um código grande fosse tão mal feito quanto o que veremos agora
• O programa não apresenta um bom projeto “orientado a objeto”
• O mau cheiro que indica isso é:
› O método extrato() é muito grande e faz tudo sozinho
› Não há responsabilidades assumidas pelas classes DVD e Aluguel
• Mas o que importa isso se o programa funciona?
› Código ruim é difícil de alterar/manter!
i) Se é difícil, então bugs serão introduzidos mais facilmente
› Exemplo: o que deve ser mudado para ter um extrato em HTML?
i) Nada pode ser reusado!
ii)Um novo método inteiro deve ser escrito, sem aproveitar código existente
› Claro que você pode resolver isso com “copy-and-paste”
i) Mas o que ocorre se as regras de preços mudarem?
ii)Vai ter que alterar código em dois lugares
› Outro exemplo: a classificação em 3 tipos de DVDs vai mudar mas os donos da locadora não sabem exatamente o que querem ainda e você pode ter certeza que haverá várias mudanças ao longo do tempo
i) Nosso código está pronto para lidar facilmente com um novo esquema de classificação de DVDs? Não.
› Nosso código está pronto para lidar facilmente com um novo esquema de pontos de alugador frequente? Não.
• Antes de continuar, repetimos: Para refatorar, você precisa ter testes automáticos
› Vamos supor que eles existam (não os veremos por questão de tempo)
• Ataquemos o primeiro problema: o método extrato() é muito grande e “faz tudo sozinho”
› Vamos decompor este método em pedaços menores
• Vamos pegar um bloco de código com alguma coesão e vamos extrair e colocá-lo num método
› Qual bloco escolher?
› A experiência é importante aqui mas também lembre a regra sobre Alta Coesão
› O switch é o cálculo de valorCorrente para um DVD e parece um pedaço coeso que merece um método à parte
i) Teste de coesão: O trabalho que o método faz pode ser dito numa frase curta?
ii)Se puder, é um bom método a ser criado
iii) Aqui, podemos dizer que o bloco extraído “calcula o preço de aluguel de um DVD” parece coeso
• Segue o código antes e depois do refatoramento, com o sublinha indicando as mudanças
• Antes
package p2.exemplos.locadora; import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class
Cliente { private String nome; private List<Aluguel> dvdsAlugados
= new
ArrayList<Aluguel>(); public Cliente(String nome) {
this.nome = nome;
} public String getNome() {
return nome; } public void
adicionaAluguel(Aluguel aluguel) { dvdsAlugados.add(aluguel); } public String extrato() { final String fimDeLinha = System.getProperty("line.separator");
double valorTotal =
0.0; int pontosDeAlugadorFrequente = 0; Iterator<Aluguel> alugueis = dvdsAlugados.iterator(); String resultado = "Registro de Alugueis de " + getNome() + fimDeLinha; while(alugueis.hasNext()) { double valorCorrente = 0.0; Aluguel cada = alugueis.next(); // determina valores para cada
aluguel switch(cada.getDVD().getCódigoDePreço()) { case DVD.NORMAL: valorCorrente +=
2; if(cada.getDiasAlugado() > 2) { valorCorrente +=
(cada.getDiasAlugado() - 2) * 1.5; } break; case DVD.LANÇAMENTO: valorCorrente +=
cada.getDiasAlugado() * 3; break; case DVD.INFANTIL: valorCorrente +=
1.5; if(cada.getDiasAlugado() > 3) { valorCorrente +=
(cada.getDiasAlugado() - 3) * 1.5; } break; } //switch //
trata de pontos de alugador frequente pontosDeAlugadorFrequente++; //
adiciona bonus para aluguel de um lançamento por pelo menos 2 dias if(cada.getDVD().getCódigoDePreço() == DVD.LANÇAMENTO && cada.getDiasAlugado() > 1) { pontosDeAlugadorFrequente++; } //
mostra valores para este aluguel resultado += "\t"
+ cada.getDVD().getTítulo() + "\t" + valorCorrente + fimDeLinha; valorTotal += valorCorrente; } //
while //
adiciona rodapé resultado += "Valor total devido: " + valorTotal + fimDeLinha; resultado += "Voce acumulou " + pontosDeAlugadorFrequente + "
pontos de alugador frequente"; return resultado; } } |
• Depois
package p2.exemplos.locadora; import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class
Cliente { private String nome; private List<Aluguel> dvdsAlugados
= new
ArrayList<Aluguel>(); public Cliente(String nome) { this.nome = nome; } public String getNome() { return nome; } public void
adicionaAluguel(Aluguel
aluguel) { dvdsAlugados.add(aluguel); } public String extrato() { final String fimDeLinha = System.getProperty("line.separator"); double valorTotal = 0.0; int pontosDeAlugadorFrequente = 0; Iterator<Aluguel> alugueis = dvdsAlugados.iterator(); String resultado = "Registro de Alugueis de " + getNome() + fimDeLinha; while (alugueis.hasNext()) { double
valorCorrente = 0.0; Aluguel cada = alugueis.next(); valorCorrente =
valorDeUmAluguel(cada); //
trata de pontos de alugador frequente pontosDeAlugadorFrequente++; //
adiciona bonus para aluguel de um lançamento por pelo menos 2 //
dias if (cada.getDVD().getCódigoDePreço() == DVD.LANÇAMENTO &&
cada.getDiasAlugado() > 1) { pontosDeAlugadorFrequente++; } //
mostra valores para este aluguel resultado += "\t"
+ cada.getDVD().getTítulo() + "\t" + valorCorrente + fimDeLinha; valorTotal += valorCorrente; } //
while //
adiciona rodapé resultado += "Valor total devido: " + valorTotal + fimDeLinha; resultado += "Voce acumulou " + pontosDeAlugadorFrequente + "
pontos de alugador frequente"; return resultado; } /* *
novo metodo extraido de extrato! */ private int valorDeUmAluguel(Aluguel cada) { int valorCorrente = 0; //
determina valores para cada linha switch (cada.getDVD().getCódigoDePreço()) { case DVD.NORMAL: valorCorrente += 2; if (cada.getDiasAlugado() > 2) { valorCorrente +=
(cada.getDiasAlugado() - 2) * 1.5; } break; case DVD.LANÇAMENTO: valorCorrente +=
cada.getDiasAlugado() * 3; break; case DVD.INFANTIL: valorCorrente += 1.5; if (cada.getDiasAlugado() > 3) { valorCorrente +=
(cada.getDiasAlugado() - 3) * 1.5; } break; } //
switch return valorCorrente; } } |
• Depois de uma mudança dessas, compilamos e testamos para verificar que não quebramos nada
› Ao testar, verificamos que vários testes falham!
› Examinando os testes que falharam e o código, observamos logo que usamos “int” em vez de “double”
• Daí a importância de sempre ter testes para refatorar
• O método é mudado para a versão seguinte:
private double valorDeUmAluguel(Aluguel cada) { double valorCorrente = 0; //
determina valores para cada linha switch (cada.getDVD().getCódigoDePreço()) { case DVD.NORMAL: valorCorrente += 2; if (cada.getDiasAlugado() > 2) { valorCorrente +=
(cada.getDiasAlugado() - 2) * 1.5; } break; case DVD.LANÇAMENTO: valorCorrente +=
cada.getDiasAlugado() * 3; break; case DVD.INFANTIL: valorCorrente += 1.5; if (cada.getDiasAlugado() > 3) { valorCorrente +=
(cada.getDiasAlugado() - 3) * 1.5; } break; } //
switch return valorCorrente; } |
• Separamos o método grande em dois
› Agora podemos continuar a trabalhar em cada pedaço individualmente
› Princípio da Divisão-e-Conquista para lidar com a complexidade
• Vamos fazer uma mudança pequena no método valorDeAluguel
› Algumas variáveis vão mudar de nome
private
double
valorDeUmAluguel(Aluguel umAluguel) { double valorDoAluguel = 0; //
determina valores para cada linha switch (umAluguel.getDVD().getCódigoDePreço()) { case DVD.NORMAL: valorDoAluguel += 2; if (umAluguel.getDiasAlugado() > 2) { valorDoAluguel +=
(umAluguel.getDiasAlugado() - 2) * 1.5; } break; case DVD.LANÇAMENTO: valorDoAluguel +=
umAluguel.getDiasAlugado() * 3; break; case DVD.INFANTIL: valorDoAluguel += 1.5; if (umAluguel.getDiasAlugado() > 3) { valorDoAluguel +=
(umAluguel.getDiasAlugado() - 3) * 1.5; } break; } //
switch return valorDoAluguel; } |
• Vale a pena mudar nomes de variáveis assim?
› Claro!
› O código deve comunicar bem seu propósito para outros programadores
› Nomes de variáveis são um meio básico de comunicação
|
Qualquer pessoa pode escrever código que
um computador entende. Bons programadores escrevem código que um ser humano
pode entender. |
• Examine o código do método valorDeUmAluguel()
› O método usa informação de um objeto da classe Aluguel, mas nada usa do Cliente
› Pela regra de design “Colocar as Responsabilidades com os Dados”, desconfiamos que o método está na classe errada
• Vamos mover o método para a classe Aluguel
• O novo código de Aluguel segue
package p2.exemplos.locadora; public class
Aluguel { private DVD dvd; private int
diasAlugado; public Aluguel(DVD dvd, int diasAlugado) { this.dvd = dvd; this.diasAlugado = diasAlugado; } public DVD getDVD() { return dvd; } public int
getDiasAlugado() { return diasAlugado; } /* * novo
metodo extraido de extrato! */ public double valorDeUmAluguel() { double valorDoAluguel = 0; //
determina valores para cada linha switch (getDVD().getCódigoDePreço()) { case DVD.NORMAL: valorDoAluguel += 2; if (getDiasAlugado() > 2) { valorDoAluguel +=
(getDiasAlugado() - 2) * 1.5; } break; case DVD.LANÇAMENTO: valorDoAluguel += getDiasAlugado() *
3; break; case DVD.INFANTIL: valorDoAluguel += 1.5; if (getDiasAlugado() > 3) { valorDoAluguel += (getDiasAlugado()
- 3) * 1.5; } break; } //
switch return valorDoAluguel; } } |
• Observe que o parâmetro Aluguel umAluguel sumiu
• Na prática, temos um passo intermediário em que delegamos Cliente.valorDeUmAluguel para Aluguel.valorDeUmAluguel
› Fazemos isso para fazer pequenas mudanças de cada vez ao refatorar
› Depois de testar, podemos remover o método valorDeUmAluguel e chamar getValorDoAluguel() diretamente
• Código antes
public class
Cliente { . . . public String extrato() {
.
.
. while (alugueis.hasNext()) { Aluguel cada = alugueis.next(); double valorCorrente = valorDeUmAluguel(cada); . . . |
• Código depois
public class
Cliente { . . . public String extrato() { . . . while (alugueis.hasNext()) { Aluguel
cada = alugueis.next(); double valorCorrente = cada.valorDeUmAluguel(); . . . |
• Isso parece muito mais orientado a objeto!
› A classe correta (Aluguel) assumiu a responsabilidade de calcular o preço do aluguel
• O próximo passo pode ser a remoção de variáveis temporárias desnecessárias
› Exemplo: valorCorrente em extrato()
› Variáveis a menos são coisas a menos para dar errado, não ter valor correto, não ser inicializada, etc.
• Código antes
public String extrato() { final String fimDeLinha = System.getProperty("line.separator"); double valorTotal = 0.0; int pontosDeAlugadorFrequente = 0; Iterator<Aluguel> alugueis = dvdsAlugados.iterator(); String resultado = "Registro de Alugueis de " + getNome() + fimDeLinha; while (alugueis.hasNext()) { Aluguel cada = alugueis.next(); double valorCorrente =
cada.valorDeUmAluguel(); //
trata de pontos de alugador frequente pontosDeAlugadorFrequente++; //
adiciona bonus para aluguel de um lançamento por pelo menos 2 //
dias if (cada.getDVD().getCódigoDePreço() == DVD.LANÇAMENTO &&
cada.getDiasAlugado() > 1) { pontosDeAlugadorFrequente++; } //
mostra valores para este aluguel resultado += "\t"
+ cada.getDVD().getTítulo() + "\t" + valorCorrente +
fimDeLinha; valorTotal += valorCorrente; } //
while //
adiciona rodapé resultado += "Valor total devido: " + valorTotal + fimDeLinha; resultado += "Voce acumulou " + pontosDeAlugadorFrequente + "
pontos de alugador frequente"; return resultado; } |
• Código depois
public String extrato() { final String fimDeLinha = System.getProperty("line.separator"); double valorTotal = 0.0; int pontosDeAlugadorFrequente = 0; Iterator<Aluguel> alugueis = dvdsAlugados.iterator(); String resultado = "Registro de Alugueis de " + getNome() + fimDeLinha; while (alugueis.hasNext()) { Aluguel cada = alugueis.next(); //
trata de pontos de alugador frequente pontosDeAlugadorFrequente++; //
adiciona bonus para aluguel de um lançamento por pelo menos 2 //
dias if (cada.getDVD().getCódigoDePreço() == DVD.LANÇAMENTO &&
cada.getDiasAlugado() > 1) { pontosDeAlugadorFrequente++; } //
mostra valores para este aluguel resultado += "\t"
+ cada.getDVD().getTítulo() + "\t" + cada.valorDeUmAluguel()
+ fimDeLinha; valorTotal += cada.valorDeUmAluguel(); } //
while //
adiciona rodapé resultado += "Valor total devido: " + valorTotal + fimDeLinha; resultado += "Voce acumulou " + pontosDeAlugadorFrequente + "
pontos de alugador frequente"; return resultado; } |
• Claro que depois de cada mudança, compile e teste
• O que fizemos com o valor do aluguel pode ser feito com o cálculo dos pontos de alugador frequente (PAF)
• Quem deve ter a responsabilidade de calcular os PAF?
› O cálculo depende de informação que Aluguel conhece
› Deixe portanto o cálculo na classe Aluguel
• Código antes:
public String extrato() { final String fimDeLinha = System.getProperty("line.separator"); double valorTotal = 0.0; int pontosDeAlugadorFrequente = 0; Iterator<Aluguel> alugueis = dvdsAlugados.iterator(); String resultado = "Registro de Alugueis de " + getNome() + fimDeLinha; while (alugueis.hasNext()) { Aluguel cada = alugueis.next(); // trata de pontos de alugador
frequente pontosDeAlugadorFrequente++; // adiciona bonus para aluguel de um lançamento por pelo menos
2 // dias if (cada.getDVD().getCódigoDePreço() ==
DVD.LANÇAMENTO &&
cada.getDiasAlugado() > 1) { pontosDeAlugadorFrequente++; } //
mostra valores para este aluguel resultado += "\t"
+ cada.getDVD().getTítulo() + "\t" + cada.valorDeUmAluguel() +
fimDeLinha; valorTotal +=
cada.valorDeUmAluguel(); } //
while //
adiciona rodapé resultado += "Valor total devido: " + valorTotal + fimDeLinha; resultado += "Voce acumulou " + pontosDeAlugadorFrequente + "
pontos de alugador frequente"; return resultado; } |
• Código depois
package p2.exemplos.locadora; public class
Aluguel { private static final int PONTO_EXTRA = 2; private static final int PONTO_SIMPLES = 1; . . . public int getPontosDeAlugadorFrequente() { if (getDVD().getCódigoDePreço() == DVD.LANÇAMENTO &&
getDiasAlugado() > 1) { return PONTO_EXTRA; } return PONTO_SIMPLES; } } public class Cliente {
.
.
. public String extrato() { final String fimDeLinha = System.getProperty("line.separator"); double valorTotal = 0.0; int pontosDeAlugadorFrequente = 0; Iterator<Aluguel> alugueis = dvdsAlugados.iterator(); String resultado = "Registro de Alugueis de " + getNome() + fimDeLinha; while (alugueis.hasNext()) { Aluguel cada = alugueis.next(); pontosDeAlugadorFrequente+=cada.getPontosDeAlugadorFrequente(); //
mostra valores para este aluguel resultado += "\t"
+ cada.getDVD().getTítulo() + "\t" + cada.valorDeUmAluguel() +
fimDeLinha; valorTotal +=
cada.valorDeUmAluguel(); } //
while //
adiciona rodapé resultado += "Valor total devido: " + valorTotal + fimDeLinha; resultado += "Voce acumulou " + pontosDeAlugadorFrequente + "
pontos de alugador frequente"; return resultado; } . . . } |
• Mais uma vez, vamos falar de variáveis temporárias
• Embora elas possam ser úteis, elas freqüentemente são indicativos de “mau cheiro”
• Examine, por exemplo, a variável valorTotal
› Ela é usada para calcular o valor total do extrato enquanto estamos no loop
› Na realidade, o loop está servindo para três coisas:
i) Montar o String do extrato
ii)Calcular o valor total
iii) Calcular os PAF
› Porém, esse trabalho talvez seja necessário em outro lugar
i) Por exemplo, posso querer saber o valor total em outro método (extratoHTML()) e terei portanto que repetir o cálculo do preço total neste lugar
› Faz sentido criarmos um método getValorTotal()?
i) Este método faz algo que podemos resumir em uma frase curta?
ii)Sim! Portanto, crie o método
• Foi o mesmo que aconteceu com a variável temporária pontosDeAlugadorFrequente
› Foi melhor criar um método getPontosDeAlugadorFrequente()
• Embora esses dois passos devam ser feitos separadamente com testes a cada passo, vamos logo ver o resultado dos dois passos
• Código antes:
public String extrato() { final String fimDeLinha = System.getProperty("line.separator"); double valorTotal = 0.0; int pontosDeAlugadorFrequente = 0; Iterator<Aluguel> alugueis = dvdsAlugados.iterator(); String resultado = "Registro de Alugueis de " + getNome() + fimDeLinha; while (alugueis.hasNext()) { Aluguel cada = alugueis.next(); pontosDeAlugadorFrequente+=cada.getPontosDeAlugadorFrequente(); //
mostra valores para este aluguel resultado += "\t"
+ cada.getDVD().getTítulo() + "\t" + cada.valorDeUmAluguel() +
fimDeLinha; valorTotal +=
cada.valorDeUmAluguel(); } //
while //
adiciona rodapé resultado += "Valor total devido: " + valorTotal + fimDeLinha; resultado += "Voce acumulou " + pontosDeAlugadorFrequente + "
pontos de alugador frequente"; return resultado; } |
• Código depois
public String extrato() { final String fimDeLinha = System.getProperty("line.separator"); Iterator<Aluguel> alugueis = dvdsAlugados.iterator(); String resultado = "Registro de Alugueis de " + getNome() + fimDeLinha; while (alugueis.hasNext()) { Aluguel cada = alugueis.next(); //
mostra valores para este aluguel resultado += "\t"
+ cada.getDVD().getTítulo() + "\t" + cada.valorDeUmAluguel() +
fimDeLinha; } //
while //
adiciona rodapé resultado += "Valor total devido: " + getValorTotal() + fimDeLinha; resultado += "Voce acumulou " + getPontosTotaisDeAlugadorFrequente() + "
pontos de alugador frequente"; return resultado; } private double getValorTotal() { double valorTotal = 0.0;
Iterator<Aluguel> alugueis = dvdsAlugados.iterator(); while(alugueis.hasNext()) {
Aluguel cada = (Aluguel)alugueis.next();
valorTotal += cada.valorDeUmAluguel(); } return valorTotal;
} private int
getPontosTotaisDeAlugadorFrequente() { int pontos = 0;
Iterator<Aluguel> alugueis = dvdsAlugados.iterator(); while(alugueis.hasNext()) {
Aluguel cada = (Aluguel)alugueis.next();
pontos += cada.getPontosDeAlugadorFrequente(); } return pontos; } |
• Ooops!!! Pare aí! Acabamos de deixar o código maior com a última mudança!
• Valeu a pena? Sim!
› Motivos:
i) Criamos dois métodos úteis que poderão ser usados mais na frente
ii)Eles poderão até ser tornados públicos se for necessário que entrem na interface da classe
iii) Organizamos o código melhor onde cada pedacinho é mais simples de entender
(1) Compare o método original extrato() com a última versão
› E quanto ao desempenho?? Temos mais loops do que antes!
i) É possível que haja um problema de desempenho mas só saberemos isso com um perfil de execução
ii)Neste caso, numa aplicação de locadora de vídeo onde clientes alugam poucos DVDs, eu garanto que o desempenho não será afetado
(1) Os loops adicionais vão adicionar alguns milissegundos ao processamento
(2) Mas quanto tempo demora para imprimir o extrato em papel?!!?
• Agora podemos ver como é fácil criar um extrato em HTML devido à existência dos dois métodos úteis que criamos
package p2.exemplos.locadora; import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class Cliente { . . . public String extratoHTML() { Iterator<Aluguel> alugueis = dvdsAlugados.iterator();
String resultado = "<H1>Registro
de Alugueis de <EM>"
+ getNome() + "</EM></H1><P>\n"; while(alugueis.hasNext()) {
Aluguel cada = alugueis.next();
// mostra
valores para este aluguel
resultado += cada.getDVD().getTítulo() + ": "
+ cada.valorDeUmAluguel() + "<BR>\n"; }
// while // adiciona rodapé
resultado += "<P>Valor
total devido: <EM>" +
getValorTotal() + "</EM>\n";
resultado += "<P>Voce
acumulou <EM>" +
getPontosTotaisDeAlugadorFrequente() + "</EM>
pontos de alugador frequente"; return resultado; } } |
• O switch está com problemas
› Ao ver um switch, verifique se o teste está sendo feito em cima dos seus próprios dados ou em cima dos dados de outro objeto
› Aqui, vemos que o Aluguel faz um switch em cima de dados do DVD!
› Portanto, faz mais sentido mover o método getValorDeUmAluguel() para a classe DVD
• Código antes:
package p2.exemplos.locadora; public class
Aluguel { . . . public double valorDeUmAluguel() { double valorDoAluguel = 0; //
determina valores para cada linha switch (getDVD().getCódigoDePreço()) { case DVD.NORMAL: valorDoAluguel += 2; if (getDiasAlugado() > 2) { valorDoAluguel +=
(getDiasAlugado() - 2) * 1.5; } break; case DVD.LANÇAMENTO: valorDoAluguel += getDiasAlugado() *
3; break; case DVD.INFANTIL: valorDoAluguel += 1.5; if (getDiasAlugado() > 3) { valorDoAluguel +=
(getDiasAlugado() - 3) * 1.5; } break; } //
switch return valorDoAluguel; } . . . } |
• Código depois
package p2.exemplos.locadora; public class
DVD { . . . /* *
novo metodo extraido de extrato! */ public double valorDeUmAluguel(int diasAlugado) { double valorDoAluguel = 0; //
determina valores para cada linha switch (getCódigoDePreço()) { case NORMAL: valorDoAluguel += 2; if (diasAlugado > 2) { valorDoAluguel += (diasAlugado -
2) * 1.5; } break; case DVD.LANÇAMENTO: valorDoAluguel += diasAlugado * 3; break; case DVD.INFANTIL: valorDoAluguel += 1.5; if (diasAlugado > 3) { valorDoAluguel += (diasAlugado -
3) * 1.5; } break; } //
switch return valorDoAluguel; } } |
package p2.exemplos.locadora; public class
Aluguel { . . . public double valorDeUmAluguel() { return dvd.valorDeUmAluguel(diasAlugado); } } |
• Podemos fazer o mesmo com os pontos de alugador freqüente
• Código antes:
package p2.exemplos.locadora; public class
Aluguel { . . . private static final int PONTO_EXTRA = 2; private static final int PONTO_SIMPLES = 1; public int
getPontosDeAlugadorFrequente() { if (getDVD().getCódigoDePreço() == DVD.LANÇAMENTO && getDiasAlugado() >
1) { return PONTO_EXTRA; } return PONTO_SIMPLES; } } |
• Código depois
package p2.exemplos.locadora; public class
DVD { . . . private static final int PONTO_EXTRA = 2; private static final int PONTO_SIMPLES = 1; public int
getPontosDeAlugadorFrequente(int
diasAlugado) { if (getCódigoDePreço() == DVD.LANÇAMENTO && diasAlugado > 1) { return PONTO_EXTRA; } return PONTO_SIMPLES; } } |
package p2.exemplos.locadora; public class
Aluguel { . . . public double getPontosDeAlugadorFrequente () { return dvd.getPontosDeAlugadorFrequente(diasAlugado); } } |
• Temos uma classe (DVD) que possui dois métodos que têm comportamento diferente dependendo de algum atributo do objeto
› Veja o switch de valorDeUmAluguel()
› Veja o teste em getPontosDeAlugadorFrequente()
• Isso é indicativo que o polimorfismo poderia limpar as coisas
• De fato, DVDs diferentes poderiam responder de forma diferente às duas perguntas valorDeUmAluguel() e getPontosDeAlugadorFrequente()
• Podemos portanto ter polimorfismo em cima de tipos de DVDs
• Melhor ainda: queremos isolar dois mundos
› O mundo das coisas que podem ser alugadas (DVDs, jogos, CDs, ...)
› O mundo que usa tais coisas
• Usaremos uma interface para isolar esses dois mundos
• Vamos primeiro definir uma interface para a situação
› Chamaremos a interface de Alugavel
› Agora, serão DVDs, mas depois poderão ser Blu-ray, jogos, etc.
• Código antes:
package p2.exemplos.locadora; public class
Aluguel { private DVD dvd; private int
diasAlugado; public Aluguel(DVD dvd, int diasAlugado) { this.dvd = dvd; this.diasAlugado = diasAlugado; } public DVD getDVD() { return dvd; } public int
getDiasAlugado() { return diasAlugado; } public int
getPontosDeAlugadorFrequente() { return dvd.getPontosDeAlugadorFrequente(diasAlugado); } public double valorDeUmAluguel() { return dvd.valorDeUmAluguel(diasAlugado); } } |
• Código depois
package p2.exemplos.locadora; public interface Alugavel { public String getTítulo(); public double getValorDoAluguel(int diasAlugada); public int
getPontosDeAlugadorFrequente(int
diasAlugada); } package p2.exemplos.locadora; public class DVD implements Alugavel { . . . } package p2.exemplos.locadora; public class
Aluguel { private Alugavel item; private int
diasAlugado; public Aluguel(Alugavel item, int diasAlugado) { this.item = item; this.diasAlugado = diasAlugado; } public Alugavel getItem() { return item; } public int
getDiasAlugado() { return diasAlugado; } public int
getPontosDeAlugadorFrequente() { return item.getPontosDeAlugadorFrequente(diasAlugado); } public double getValorDoAluguel() { return item.getValorDoAluguel(diasAlugado); } } |
• Ainda não temos polimorfismo porque apenas uma classe implementa a interface Alugavel
• A primeira solução que vem à mente é fazer como segue:
• Mas isso não funciona porque um DVD pode mudar sua classificação durante sua vida
› Não é bonito mudar a classe de um objeto importante durante sua vida
• Como lidar com isso?
› Separe o que é igual daquilo que muda
› Encapsule cada um em objetos diferentes
• Resultado:
• Observe que cada DVD agora será composto de dois objetos:
› Um para o DVD em si
› Um para a classificação do DVD
• Falamos que está havendo composição de objetos
• Para implementar getValorDoAluguel(), o DVD delega para o objeto de classificação
› Por que tudo isso é melhor?
i) A composição pode ser alterada de forma simples e elegante em tempo de execução
ii)Isto é, o DVD recebe um novo objeto composto de Classificacao
iii) Isso faz com que a composição seja freqüentemente superior à herança
iv) A herança ainda ocorre, mas não no mundo dos DVDs, mas no mundo das classificações. Algo simples, bem definido, coeso e em um lugar bem definido do programa
› Agora, vamos fazer isso acontecer no código
› São 3 passos:
i) Implementar a composição de objetos de forma a permitir a mudança dinâmica do objeto de classificação
ii)Mover o método getValorDoAluguel de DVD para Classificacao
iii) Substituir os testes (switch/if) com polimorfismo
• Façamos a primeira etapa
• Queremos que cada DVD vire dois objetos: um DVD associado a uma classificação
› Por enquanto, o objeto Classificacao é quem vai responder getCódigoDePreço()
› Está havendo delegação
i) Não quero que a interface externa mude para quem usa a classe DVD
ii)Portanto, quem cria o novo objeto é a própria classe DVD para esconder tudo
• Código depois:
package p2.exemplos.locadora; public class DVD implements Alugavel { public static final int NORMAL = 0; public static final int LANÇAMENTO = 1; public static final int INFANTIL = 2; private String título; private Classificacao classificação; public DVD(String título, int códigoDePreço) { this.título = título; setCódigoDePreço(códigoDePreço); } public void
setCódigoDePreço(int
códigoDePreço) { switch (códigoDePreço) { case NORMAL: classificação = new
ClassificacaoNormal(); break; case LANÇAMENTO: classificação = new
ClassificacaoLancamento(); break; case INFANTIL: classificação = new ClassificacaoInfantil(); break; } } public String getTítulo() { return título; } public int
getCódigoDePreço() { return classificação.getCódigoDePreço(); } /* *
novo metodo extraido de extrato! */ public double getValorDoAluguel(int diasAlugado) { return classificação.getValorDoAluguel(diasAlugado); } public int
getPontosDeAlugadorFrequente(int
diasAlugado) { return classificação.getPontosDeAlugadorFrequente(diasAlugado); } } |
package p2.exemplos.locadora; public abstract class
Classificacao { abstract int
getCódigoDePreço(); abstract double getValorDoAluguel(int diasAlugada); int
getPontosDeAlugadorFrequente(int
diasAlugadas) { return 1; } } package p2.exemplos.locadora; class ClassificacaoInfantil extends Classificacao { int
getCódigoDePreço() { return DVD.INFANTIL; } double getValorDoAluguel(int diasAlugado) { double valorDoAluguel = 1.5; if (diasAlugado > 3) { valorDoAluguel += (diasAlugado - 3) *
1.5; } return valorDoAluguel; } } package p2.exemplos.locadora; class ClassificacaoNormal extends Classificacao { int
getCódigoDePreço() { return DVD.NORMAL; } double getValorDoAluguel(int diasAlugado) { double valorDoAluguel = 2; if (diasAlugado > 2) { valorDoAluguel += (diasAlugado - 2) *
1.5; } return valorDoAluguel; } } package p2.exemplos.locadora; class ClassificacaoLancamento extends Classificacao { int
getCódigoDePreço() { return DVD.LANÇAMENTO; } double getValorDoAluguel(int diasAlugado) { return diasAlugado * 3; } int
getPontosDeAlugadorFrequente(int
diasAlugados) { return (diasAlugados > 1) ? 2 : 1; } } |
• Observe que, em tempo de execução, podemos mudar a classificação de uma fita
› Basta chamar setCódigoDePreço(), como antes
• Os objetos de classificação devem implementar getValorDoAluguel() e getPontosDeAlugadorFrequente() que estão em DVD
› Mais uma vez, teremos delegação
› O objeto DVD delega para Classificacao o cálculo de getValorDoAluguel() e getPontosDeAlugadorFrequente()
› Estes métodos são chamados polimorficamente
• Valeu a pena tanto esforço para introduzir polimorfismo?
› Primeiramente, um bom programador já teria colocado polimorfismo desde o início
› Segundo, valeu a pena sim: o código é muito mais simples de mudar quando houver um novo esquema de preços, por exemplo.
• Uma faceta importante do refactoring é o ritmo
› Testa, mude um pouco, testa, mude um pouco, ...
› Este ritmo permite fazer refatoramento de forma simples, rápida e segura