Capítulo 3

Adicionando Preço

3.1 Melhorando a modelagem do Sistema

Agora que temos bastante coisa funcionando, precisamos deixar nosso sistema fazer algo bem importante para a regra de negócio, que é poder ganhar por sessão comprada.

Nossa regra de negócio diz que cada filme deve ter seu preço, para poder ser algo dinâmico e rentável para o cinema, onde filmes que tendam a ter mais audiência devem ter um preço superior. Logo, precisaremos ter um atributo preço em nosso modelo Filme. Mas, qual será o seu tipo?


@Entity
public class Filme {

    @Id
    @GeneratedValue
    private Integer id;
    private String nome;
    private Duration duracao;
    private String genero;
    private ??? preco;
}

Poderíamos facilmente colocar como double, contudo, existem alguns problemas nisso! O principal deles é o fato do double ser inexato. Dado este problema corriqueiro, foi criada uma classe que cuida da exatidão do valor, a classe BigDecimal.


@Entity
public class Filme {

    @Id
    @GeneratedValue
    private Integer id;
    private String nome;
    private Duration duracao;
    private String genero;
    private BigDecimal preco;
}

Algo bem importante que deve ser observado é que não podemos deixar um filme estar sem preço em nenhum lugar que estivermos o manipulando. Portanto, teremos que forçar o Filme a ter preço e, para isso, atribuiremos essa informação no construtor da classe:


public Filme(String nome, Duration duracao, String genero, BigDecimal preco) {
    this.nome = nome;
    this.duracao = duracao;
    this.genero = genero;
    this.preco = preco;
}

Agora, precisamos adicionar o campo preço tanto na listagem de filmes quanto no formulário de cadastro de filme para que possamos obter e disponibilizar essas informações.


<!-- restante do form -->
<div class="form-group">
    <label for="preco">Preço:</label>
    <input id="preco" type="text" name="preco" class="form-control"
           value="${filme.preco}">
    <c:forEach items="${bindingResult.getFieldErrors('preco')}" var="error">
        <span class="text-danger">${error.defaultMessage}</span>
    </c:forEach>
</div>

<!-- botão de gravar -->

Além disso, temos outra regra de negócio muito importante que define que cada sala deve possuir seu preço, já que podemos ter salas onde há um diferencial como ser 3D, iMax, fora a parte de manutenção. Portanto, a sala também deverá ter seu próprio preço.

Já sabemos o que é necessário para isso : adicionar o atributo, o receber através do construtor e atualizar as telas.

Algo que ainda não falamos mas acaba ficando implícito é : ao irmos ao cinema não pagamos pela sala ou pelo filme, mas sim pela sessão, então, precisamos fazer com que a sessão tenha seu preço correto, que deve ser a soma dos preços da sala e do filme:

@Entity
public class Sessao {

    // demais métodos

    private BigDecimal preco;

    public Sessao(LocalTime horario, Filme filme, Sala sala) {
        this.horario = horario;
        this.setFilme(filme);
        this.sala = sala;
        this.preco = sala.getPreco().add(filme.getPreco());
    }

    public BigDecimal getPreco(){
        return preco;
    }

Já conhece os cursos online Alura?

A Alura oferece centenas de cursos online em sua plataforma exclusiva de ensino que favorece o aprendizado com a qualidade reconhecida da Caelum. Você pode escolher um curso nas áreas de Programação, Front-end, Mobile, Design & UX, Infra e Business, com um plano que dá acesso a todos os cursos. Ex aluno da Caelum tem 15% de desconto neste link!

Conheça os cursos online Alura.

3.2 Exercício - Colocando preço na Sala e Filme

  1. Adicione um atributo preço do tipo BigDecimal na classes Sala. Crie os métodos get e set para o mesmo em ambas as classes e adicione um novo parâmetro nos construtores para receber um valor para o novo atributo.

     @Entity
     public class Sala {
    
         // Demais atributos
    
         private BigDecimal preco;
    
         /**
         * @deprecated hibernate only
         */
         public Sala() {
         }
    
         public Sala(String nome, BigDecimal preco) {
             this.nome = nome;
             this.preco = preco;
         }
    
         //getters e setters e demais métodos
     }
    
  2. Faça o mesmo para a classe Filme.

     @Entity
     public class Filme {
    
         // Demais atributos
    
         private BigDecimal preco;
    
         /**
         * @deprecated hibernate only
         */
         public Filme() {
         }
    
         public Filme(String nome, Duration duracao, String genero, BigDecimal preco) {
             this.nome = nome;
             this.duracao = duracao;
             this.genero = genero;
             this.preco = preco;
         }
    
         //getters e setters e demais métodos
     }
    
  3. Ao adicionar o novo parâmetro ao construtor (ou o próprio construtor com todos os parâmetros), quebramos nossos testes da classe GerenciadorDeSessaoTest. Vamos passar todos os argumentos no momento de instanciar Sala e Filme:

     public class GerenciadorDeSessaoTest {
    
         @Test
         public void garanteQueNaoDevePermitirSessaoNoMesmoHorario() {
    
             Filme filme = new Filme("Rogue One", Duration.ofMinutes(120),  
                             "SCI-FI", BigDecimal.ONE);
             LocalTime horario = LocalTime.parse("10:00:00");
    
             Sala sala = new Sala("Eldorado - IMAX", BigDecimal.ONE);
             List<Sessao> sessoes = Arrays.asList(new Sessao(horario, filme, sala));
    
             Sessao sessao = new Sessao(horario, filme, sala);
    
             GerenciadorDeSessao gerenciador = new GerenciadorDeSessao(sessoes);
    
             Assert.assertFalse(gerenciador.cabe(sessao));
         }
    
         @Test
         public void garanteQueNaoDevePermitirSessoesTerminandoDentroDoHorarioDeUmaSessaoJaExistente()  
         {
             Filme filme = new Filme("Rogue One", Duration.ofMinutes(120),  
                             "SCI-FI", BigDecimal.ONE);
             LocalTime horario = LocalTime.parse("10:00:00");
    
             Sala sala = new Sala("Eldorado - IMAX", BigDecimal.ONE);
             List<Sessao> sessoes = Arrays.asList(new Sessao(horario, filme, sala));
    
             Sessao sessao = new Sessao(horario.plusHours(1), filme, sala);
             GerenciadorDeSessao gerenciador = new GerenciadorDeSessao(sessoes);
    
             Assert.assertFalse(gerenciador.cabe(sessao));
    
         }
    
         @Test
         public void garanteQueNaoDevePermitirSessoesIniciandoDentroDoHorarioDeUmaSessaoJaExistente()  
         {
             Filme filme = new Filme("Rogue One", Duration.ofMinutes(120),  
                             "SCI-FI", BigDecimal.ONE);
             LocalTime horario = LocalTime.parse("10:00:00");
             Sala sala = new Sala("Eldorado - IMAX", BigDecimal.ONE);
    
             List<Sessao> sessoesDaSala = Arrays.asList(new Sessao(horario, filme, sala));
    
             GerenciadorDeSessao gerenciador = new GerenciadorDeSessao(sessoesDaSala);
             Assert.assertFalse(gerenciador.cabe(new Sessao(horario.minusHours(1),  
                                     filme, sala)));
    
         }
    
         @Test
         public void garanteQueDevePermitirUmaInsercaoEntreDoisFilmes() {
             Sala sala = new Sala("Eldorado - IMAX", BigDecimal.ONE);
    
             Filme filme1 = new Filme("Rogue One", Duration.ofMinutes(120),  
                             "SCI-FI", BigDecimal.ONE);
             LocalTime dezHoras = LocalTime.parse("10:00:00");
             Sessao sessaoDasDez = new Sessao(dezHoras, filme1, sala);
    
             Filme filme2 = new Filme("Rogue One", Duration.ofMinutes(120),  
                             "SCI-FI", BigDecimal.ONE);
             LocalTime dezoitoHoras = LocalTime.parse("18:00:00");
             Sessao sessaoDasDezoito = new Sessao(dezoitoHoras,  
                                             filme2, sala);
    
             List<Sessao> sessoes = Arrays.asList(sessaoDasDez, sessaoDasDezoito);
    
             GerenciadorDeSessao gerenciador = new GerenciadorDeSessao(sessoes);
    
             Assert.assertTrue(gerenciador.cabe(new Sessao(LocalTime.parse("13:00:00"),  
                                 filme2, sala)));
         }
    
     }
    
  4. Vamos alterar as páginas de formulário e listagem da Sala e do Filme para adicionar o campo de preço. Use o atalho do Eclipse Ctrl + Shift + C para remover ou adicionar comentários:

    • No arquivo src/main/webapp/WEB-INF/views/sala/sala.jsp, remova os comentários ao redor do input para o preço.

    • No arquivo src/main/webapp/WEB-INF/views/sala/lista.jsp, remova os comentários para as TAGs <th> e <td> para a coluna preço:

        <th class="text-center">Preço</th>
      
        <td class="text-center">${sala.preco}</td>
      
    • No arquivo src/main/webapp/WEB-INF/views/filme/filme.jsp, remova os comentários ao redor do input para o preço.

    • No arquivo src/main/webapp/WEB-INF/views/filme/lista.jsp, remova os comentários para as TAGs <th> e <td> para a coluna preço:

        <th>Preço</th>
      
        <td>${filme.preco}</td>
      
  5. Adicione um novo atributo preço do tipo BigDecimal na classe Sessao e, no construtor da classe, atribua a ele o resultado da soma dos preços da Sala e do Filme. Não se esqueça de criar os métodos get e set do novo atributo, para que possamos recuperar seu valor futuramente:

     @Entity
     public class Sessao {
    
         // Demais atributos
    
         private BigDecimal preco;
    
         /**
         * @deprecated hibernate only
         */
         public Sessao() {
         }
    
         public Sessao(LocalTime horario, Filme filme, Sala sala) {
             this.horario = horario;
             this.setFilme(filme);
             this.sala = sala;
             this.preco = sala.getPreco().add(filme.getPreco());
         }
    
         // demais getters e setters
    
     }
    
  6. Crie um teste no pacote br.com.caelum.ingresso.model para garantir que a sessão retorna a soma dos preços da Sala e Filme:

     public class SessaoTest {
    
         @Test
         public void oPrecoDaSessaoDeveSerIgualASomaDoPrecoDaSalaMaisOPrecoDoFilme() {
    
             Sala sala = new Sala("Eldorado - IMax", new BigDecimal("22.5"));
             Filme filme = new Filme("Rogue One", Duration.ofMinutes(120),  
                             "SCI-FI", new BigDecimal("12.0"));
    
             BigDecimal somaDosPrecosDaSalaEFilme = sala.getPreco().add(filme.getPreco());
    
             Sessao sessao = new Sessao(LocalTime.parse("10:00:00"), filme, sala);
    
             Assert.assertEquals( somaDosPrecosDaSalaEFilme, sessao.getPreco() );
    
         }
    
     }
    
  7. Altere o arquivo de listagem da sessão src/main/webapp/WEB-INF/views/sessao/lista.jsp, remova os comentários para as TAGs <th> e <td> para a coluna preço.

  8. Rode a aplicação e verifique se o preço da sessão é corretamente exibido na tela do sistema.

3.3 Aplicando Strategy

No último passo nós adicionamos o preço à sessão, mas quando vamos ao cinema não compramos uma sessão e sim um ingresso para poder ingressar numa sessão. Como o ingresso tem um peso bem grande em nosso sistema, é interessante que tenhamos uma classe para representá-lo. Também é necessário definir quais serão os atributos que nosso Ingresso vai possuir. Precisaremos ao menos da própria Sessão e de um preço:


public class Ingresso {

    private Sessao sessao;
    private BigDecimal preco;

}

Quando é feita a compra de um ingresso, o preço pode sofrer um desconto, por exemplo, para estudante, bancos ou outras promoções. Mas nosso sistema ainda não está preparado para tratar descontos.

Uma maneira bem elegante de definirmos que cada desconto possua sua própria regra de negócio e ainda assim faça tudo que um desconto precisa fazer é criarmos uma interface Desconto:


public interface Desconto {

    public BigDecimal aplicarDescontoSobre(BigDecimal precoOriginal);

}

Agora, podemos criar nossos descontos, por exemplo, para estudante :


public class DescontoEstudante implements Desconto {

    private BigDecimal metade = new BigDecimal(2.0);

    @Override
    public BigDecimal aplicarDescontoSobre(BigDecimal precoOriginal) {
        return precoOriginal.divide(metade);
    }
}

Desta maneira, nosso ingresso passa a poder usar o Desconto para calcular o preço! E, assim que construirmos nosso objeto Ingresso, devemos informar qual é o Desconto utilizado:


public class Ingresso {
  this.sessao = sessao;

    private Sessao sessao;
    private BigDecimal preco;

    public Ingresso(Sessao sessao, Desconto desconto) {
        this.preco = desconto.aplicarDescontoSobre(sessao.getPreco());
    }

    //getters
}

Com essa nova implementação, não importa qual é o desconto que será passado, mas que ele execute a regra de negócio, ainda que tenhamos um desconto que realmente não aplique desconto.

A forma como resolvemos esse impasse, usando polimorfismo para permitir passar qualquer classe que implemente uma interface, é uma solução bem conhecida pela comunidade, tanto que é um Design Pattern conhecido como Strategy.

3.4 Exercício - Criando descontos e ingresso

  1. Crie a interface Desconto no pacote br.com.caelum.ingresso.model.descontos:

     public interface Desconto {
    
         BigDecimal aplicarDescontoSobre(BigDecimal precoOriginal);
    
     }
    
  2. Crie a classe com implementação de desconto para Estudantes no pacote
    br.com.caelum.ingresso.model.descontos:

     public class DescontoEstudante  implements Desconto {
    
         @Override
         public BigDecimal aplicarDescontoSobre(BigDecimal precoOriginal) {
             return precoOriginal.divide(new BigDecimal("2.0"));
         }
     }
    
  3. Crie a classe com implementação de desconto para Bancos:

     public class DescontoDeTrintaPorCentoParaBancos implements Desconto {
    
         @Override
         public BigDecimal aplicarDescontoSobre(BigDecimal precoOriginal) {
             return precoOriginal.subtract(trintaPorCentoSobre(precoOriginal));
         }
    
         private BigDecimal trintaPorCentoSobre(BigDecimal precoOriginal) {
             return precoOriginal.multiply(new BigDecimal("0.3"));
         }
     }
    
  4. Ainda é necessário criarmos outra implementação, para casos onde não haverá desconto:

     public class SemDesconto implements Desconto {
    
         @Override
         public BigDecimal aplicarDescontoSobre(BigDecimal precoOriginal) {
             return precoOriginal;
         }
     }
    
  5. Crie a classe Ingresso no pacote br.com.caelum.ingresso.model com atributos Sessao e Preco. Crie um construtor que receba uma Sessao e um Desconto como parâmetros. Faça com que o preço do ingresso seja o resultado da aplicação do desconto sobre o preço da sessão. Crie, também, um construtor sem parâmetros, para que nossa classe possa ser utilizada futuramente pelo Hibernate:

     public class Ingresso {
    
         private Sessao sessao;
         private BigDecimal preco;
    
         /**
         * @deprecated hibernate only
         */
         public Ingresso(){
    
         }
    
         public Ingresso(Sessao sessao, Desconto tipoDeDesconto) {
             this.sessao = sessao;
             this.preco = tipoDeDesconto.aplicarDescontoSobre(sessao.getPreco());
         }
    
         public Sessao getSessao() {
             return sessao;
         }
    
         public BigDecimal getPreco() {
             return preco;
         }
     }
    
  6. Reinicie a aplicação e verifique se os descontos estão sendo aplicados corretamente.

Saber inglês é muito importante em TI

Na Alura Língua você reforça e aprimora seu inglês! Com Spaced Repetitions a metodologia se adapta ao seu conhecimento. Com FlashCards, os exercícios são objetivos e focados em uma habilidade específica, para praticar tudo que aprendeu em cada lição. Diversas estatísticas indicarão como prosseguir, analizando quais são seus pontos fortes e fracos, frases e estruturas já conhecidas.

Pratique seu inglês na Alura Língua.

3.5 Exercícios de Teste

  1. Crie uma classe de teste DescontoTest no pacote br.com.caelum.ingresso.model.desconto, garantindo que os três descontos são aplicados corretamente aos ingressos:

    public class DescontoTest {
    
       @Test
       public void deveConcederDescontoDe30PorcentoParaIngressosDeClientesDeBancos() {
    
           Sala sala = new Sala("Eldorado - IMAX", new BigDecimal("20.5"));
           Filme filme = new Filme("Rogue One", Duration.ofMinutes(120),
                           "SCI-FI", new BigDecimal("12"));
           Sessao sessao = new Sessao(LocalTime.parse("10:00:00"), filme, sala);
           Ingresso ingresso = new Ingresso(sessao, new DescontoDeTrintaPorCentoParaBancos());
    
           BigDecimal precoEsperado = new BigDecimal("22.75");
    
           Assert.assertEquals(precoEsperado, ingresso.getPreco());
    
       }
    
       @Test
       public void deveConcederDescontoDe50PorcentoParaIngressoDeEstudante() {
    
           Sala sala = new Sala("Eldorado - IMAX", new BigDecimal("20.5"));
           Filme filme = new Filme("Rogue One", Duration.ofMinutes(120),
                           "SCI-FI", new BigDecimal("12"));
           Sessao sessao = new Sessao(LocalTime.parse("10:00:00"), filme, sala);
           Ingresso ingresso = new Ingresso(sessao, new DescontoEstudante());
    
           BigDecimal precoEsperado = new BigDecimal("16.25");
    
           Assert.assertEquals(precoEsperado, ingresso.getPreco());
    
       }
    
       @Test
       public void naoDeveConcederDescontoParaIngressoNormal() {
    
           Sala sala = new Sala("Eldorado - IMAX", new BigDecimal("20.5"));
           Filme filme = new Filme("Rogue One", Duration.ofMinutes(120),
                           "SCI-FI", new BigDecimal("12"));
           Sessao sessao = new Sessao(LocalTime.parse("10:00:00"), filme, sala);
           Ingresso ingresso = new Ingresso(sessao, new SemDesconto());
    
           BigDecimal precoEsperado = new BigDecimal("32.5");
    
           Assert.assertEquals(precoEsperado, ingresso.getPreco());
    
       }
    }
    
  2. Reinicie a aplicação e verifique os resultados dos testes para garantir que os descontos estão sendo aplicados corretamente.