Capítulo 2

Trabalhando com Branches

Depois de liberarmos o código de uma aplicação para o cliente, continuamos melhorando funcionalidades existentes e criando novas funcionalidades, sempre comitando essas alterações. Mas, e se houver um bug que precisa ser corrigido imediatamente? O código com a correção do bug seria liberado junto com funcionalidades pela metade, que ainda estavam em desenvolvimento.

Para resolver esse problema, a maioria dos sistemas de controle de versão tem branches: linhas independentes de desenvolvimento nas quais podemos trabalhar livremente, versionando quando quisermos, sem atrapalhar outras mudanças no código.

Em projetos que usam Git, podemos ter tanto branches locais, presentes apenas na máquina do programador, quanto branches remotas, que apontam para outras máquinas. Por padrão, a branch principal é chamada master, tanto no repositório local quanto no remoto. Idealmente, a master será uma branch estável, isto é, o código nessa branch estará testado e pronto para ser entregue.

Para listar as branches existentes em seu repositório Git, basta executar:

    git branch

Para saber mais: Branches e o HEAD

No Git, uma branch é bem leve: trata-se apenas de um ponteiro para um determinado commit. Podemos mostrar os commits para os quais as branches estão apontando com o comando
git branch -v.

Há também um ponteiro especial, chamado HEAD, que aponta para a branch atual.

Criando uma branch

Uma prática comum é ter no repositório branches novas para o desenvolvimento de funcionalidades que ainda estão em andamento, contendo os commits do que já foi feito até então. Para criar uma branch nova de nome work a partir do último commit da master, faça:

    git branch work

Ao criar uma nova branch, ainda não estamos automaticamente nela. Para selecioná-la:

    git checkout work

Criando e selecionando uma branch

É possível criar e selecionar uma branch com apenas um comando:

        git checkout -b work

Para visualizar o histórico de commits de todas as branches, podemos fazer:

git log --all

Para uma representação gráfica baseada em texto do histórico, há a opção:

git log --graph

2.1 Juntando commits de outra branch

E como fazemos para liberar as melhorias e novas funcionalidades? É preciso mesclar o código de uma branch com a branch master.

Em geral, os sistemas de controle de versão tem um comando chamado merge, que permite fazer a junção de uma branch em outra de maneira automática.

Vamos dizer que temos o seguinte cenário: nossa master tem os commits A e B. Então, criamos uma branch work, implementamos uma nova funcionalidade e realizamos o commit D. Depois, voltamos à master e, ao obter do repositório remoto as mudanças feitas por um outro membro do time, recebemos o commit C.

Para saber mais: Os commits e seus pais

No Git, todo commit tem um ou mais pais, com exceção do primeiro. Para mostrar os commits com seus respectivos pais, podemos utilizar o comando git log --oneline --parents.

Se estivermos na branch master, podemos fazer o merge das alterações que estão na branch work da seguinte maneira:

    git merge work

Quando fizermos o merge, as alterações da branch work são colocadas na branch master e é criado um commit M só para o merge. O git até mesmo abre um editor de texto para que possamos definir a mensagem desse commit de merge.

Se visualizarmos o gráfico de commits da master com os comandos git log --graph ou gitk, teríamos algo como:

O merge do tipo fast-forward

Há um caso em que um git merge não vai gerar um commit de merge: quando não há novos commits na branch de destino. No nosso caso, se não tivéssemos o commit C, o merge seria feito simplesmente apontando a branch master para o commit D. Esse tipo de merge é chamado de fast-forward.

Simplificando o histórico com rebase

À medida que a aplicação vai sendo desenvolvida, é natural que vários merges sejam feitos, mas, se olharmos o histórico de commits da nossa branch master, veremos que haverá um commit específico para cada merge feito.

Como esses commits de merge criam um desvio na árvore de commits, o histórico do projeto acaba ficando bem poluído.

Para resolver isso, o Git possui o comando rebase, que permite mesclar o código de uma branch em outra, mas deixando o histórico de maneira mais linear. Para efetuar o rebase da master na work:

    git rebase master

Após o rebase, a branch work teria sua base alterada do commit B para o commit C, linearizando o histórico de commits. Porém, ao efetuar o rebase, o histórico é modificado: o commit D é descartado e um novo commit quase idêntico, o D', é criado.

Com a base da branch work refeita, é possível fazer um merge do tipo fast-forward na master, sem gerar um commit de merge.

Boa prática: não fazer rebase na master

Se fizermos um git rebase work enquanto estivermos na branch principal, os commits da master seriam descartados, "clonados" e aplicados na branch work.

Mudar commits da master é perigoso pois é a branch que compartilhamos com a nossa equipe. O Git se perderia facilmente.

Uma boa prática no uso do Git é modificar o mínimo possível a branch principal. No caso de um rebase, nunca o faça de um jeito que possa alterar o histórico da master.

O bom rebase

Mas então fazer um rebase de uma outra branch na master sem correr o risco de mudar a branch principal?

Para saber mais: a controvérsia entre merge e rebase

Há uma grande controvérsia na maneira considerada ideal de trabalhar com Git: usar rebase, deixando o histórico limpo, ou usar merge, ganhando total rastreabilidade do que aconteceu?

Recomendamos uma abordagem que utiliza rebase porque, com o histórico limpo, tarefas como navegar pelo código antigo, revisar código novo e reverter mudanças pontuais ficam mais fáceis.

Mais detalhes em: http://www.vidageek.net/2009/07/06/git-workflow/

Aprenda se divertindo na Alura Start!

Na Alura Start você vai criar games, apps, sites e muito mais! É hora de transformar suas ideias em programas de verdade! Suas animações e programas também te ajudam a conhecer mais de biologia, matemática e como as coisas funcionam. Estude no seu ritmo e com a melhor didática. A qualidade da conceituada Alura, agora para Starters.

Conheça os cursos online da Alura Start!

2.2 Exercício - Criando nossas branches

  1. Crie uma nova branch para podermos trabalhar fora da branch de produção:

             git branch work
    
  2. Verifique se a branch foi criada:

             git branch
    
  3. Agora vá para a branch criada. Para isso, execute o comando:

             git checkout work
    
  4. Altere o arquivo README.md, colocando informações sobre o projeto e avisando que é você quem está desenvolvendo.

  5. Comite a alteração, mantendo-se na branch work:

             git commit -am "colocando mais informações no README.md"
    
  6. Veja que a alteração foi gravada no repositório, lembrando que para sair do log precisamos apertar a tecla q:

         git log
    
  7. Volte à branch master:

         git checkout master
    
  8. Veja que o arquivo README.md ainda está com o conteúdo anterior!

  9. Indique no pom.xml que você é o desenvolvedor do projeto:

     <project ...>
         <!-- restante omitido... -->
         <developers>
             <developer>
                 <name>John Doe</name>
                 <email>jdoe@example.com</email>
             </developer>
         </developers>
     </project>
    
  10. Ainda na branch master, comite as alterações do pom.xml:

             git commit -am "adicionando developer no pom.xml"
    
  11. Observe no gráfico dos quatro últimos commits que houve uma divergência nas branches work e master:

     git log --oneline --all --graph -n 4
    
  12. Vamos considerar nossa tarefa como concluída e que podemos fazer com que nosso trabalho entre em produção. Para isso, primeiramente, faça o rebase da branch master na work:

             git checkout work
             git rebase master
    
  13. Veja no gráfico de commits das branches que o histórico foi linearizado e o commit da branch work foi aplicado depois do commit da master:

     git log --oneline --all --graph -n 4
    

    Note que o commit da branch work mudou de identificador. Já o da master permaneceu intocado.

  14. Repare que as alterações no pom.xml estão disponíveis na branch work!

  15. Agora que a branch work já tem as novidades da master, podemos fazer um merge da work na master. O merge será do tipo fast-forward.

             git checkout master
             git merge work
    
  16. Observe, no histórico de commits, que o commit foi incorporado à branch master:

     git log
    
  17. Delete a branch work, já que você não a usará mais nesse instante:

             git branch -d work
    

2.3 Começando a trabalhar no Sistema

Para nosso sistema funcionar da maneira correta, precisamos permitir a venda de ingressos, porém, ao irmos no cinema nós não compramos o ingresso diretamente para o filme, compramos para uma Sessão que representa o filme sendo passado em um determinado horário e em uma sala específica. Nosso sistema deve tratar os objetos da forma mais próxima possível do mundo real, então vamos modelar a classe Sessão que deve conter as informações do horário, da sala e do filme.

Como queremos que as Sessões sejam armazenadas no banco de dados, precisamos falar que nosso modelo também representa uma entidade:


    @Entity
    public class Sessao {

        @Id
        @GeneratedValue
        private Integer id;

        private LocalTime horario;

        @ManyToOne
        private Sala sala;

        @ManyToOne
        private Filme filme;


        /**
        * @deprecated hibernate only
        */
        public Sessao() {
        }

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

        //getters e setters

    }

Todas as configurações utilizadas acima são para o Hibernate. A maioria delas já vimos no curso de Java para Web, mas há duas novas: a @ManyToOne, que determina que a relação entre os modelos é de muitas sessões para uma sala e também para um filme, e o comentário em Javadoc, avisando que o construtor sem argumentos só está lá porque o Hibernate precisa dele para fazer o mapeamento.

Agora que já temos a modelagem pronta, precisamos manipular as sessões em algum ponto da nossa aplicação. A responsabilidade de direcionar uma requisição que vem da tela a uma lógica específica, seguindo o padrão MVC, é da camada controller.

Nosso controller deverá direcionar uma chamada para uma tela de cadastro de sessão e, portanto, criaremos um método que retorne a tela que está no arquivo sessao/sessao.jsp. Essa tela já está criada no projeto, assim como todas as outras que veremos.

Toda sessão será exibida em uma sala. Portanto, precisamos saber qual é a sala em que a sessão será alocada no momento de sua criação. Receberemos essa informação no momento da chamada para a tela:

    @Controller
    public class SessaoController {

        @GetMapping("/admin/sessao")
        public ModelAndView form(@RequestParam("salaId") Integer salaId) {

            ModelAndView modelAndView = new ModelAndView("sessao/sessao");


            return modelAndView;
        }
    }

Usamos a anotação @GetMapping("url") para deixar claro que este método será chamado apenas quando alguém fizer um requisição do tipo get para o endereço informado. Para não haver dúvida de qual o parâmetro que esperamos receber em uma determinada variável, usamos a anotação @RequestParam("nomeDoParametro") logo antes da variável. Por fim, nosso método devolve um objeto do tipo ModelAndView, diferente do retorno em texto com que já estávamos acostumados. Essa classe une o trabalho da Model, que disponibiliza objetos na view, com o retorno em texto do método, que informa qual página será processada. No exemplo acima, informamos a view já na construção do objeto do tipo ModelAndView.

A tela de cadastro deve mostrar a sala em que a sessão está sendo criada e uma lista com os filmes disponíveis. Desse modo, precisamos enviar essas informações, que temos no banco de dados, para a view. Acessaremos a sala e a lista de filmes utilizando o padrão de projeto DAO, ou seja, através de objetos do tipo SalaDao e FilmeDao. Como estes objetos são gerenciados pelo Spring e queremos reduzir ao máximo o acoplamento entre nossas classes, usaremos o conceito de injeção de dependência para conseguirmos pegar estes objetos. Para fazermos a injeção, usaremos a anotação @Autowired:

    @Controller
    public class SessaoController {

        @Autowired
        private SalaDao salaDao;

        @Autowired
        private FilmeDao filmeDao;

        //Método form omitido
    }

Com os objetos já injetados, para mandar a sala e os filmes para a view, usaremos o método addObject() da classe ModelAndView, que recebe uma String para identificar o objeto na página e o objeto em si:

    @Controller
    public class SessaoController {

        @Autowired
        private SalaDao salaDao;

        @Autowired
        private FilmeDao filmeDao;

        @GetMapping("/admin/sessao")
        public ModelAndView form(@RequestParam("salaId") Integer salaId) {

            ModelAndView modelAndView = new ModelAndView("sessao/sessao");

            modelAndView.addObject("sala", salaDao.findOne(salaId));
            modelAndView.addObject("filmes", filmeDao.findAll());

            return modelAndView;
        }
    }

Agora, precisamos recuperar os dados que foram escolhidos na tela, mas eles são representados por diversos objetos e só teremos o id de cada. Para deixar claro que esses dados não representam uma sessão completa, pois são as informações que vieram do formulário, vamos criar uma classe pra representar especificamente o formulário de sessão. Essa classe terá a responsabilidade de nos devolver uma sessão completa:


    public class SessaoForm {


        private Integer id;

        @NotNull
        private Integer salaId;

        @DateTimeFormat(pattern="HH:mm")
        @NotNull
        private LocalTime horario;

        @NotNull
        private Integer filmeId;

        //getters e setters

        public Sessao toSessao(SalaDao salaDao, FilmeDao filmeDao){
            Filme filme = filmeDao.findOne(filmeId);
            Sala sala = salaDao.findOne(salaId);

            Sessao sessao = new Sessao(horario, filme, sala);
            sessao.setId(id);

            return sessao;
        }
    }

Depois que for preenchido o formulário, precisamos realizar a persistência. Como o estado do servidor será alterado e não queremos mostrar os dados na url, eles serão enviados pelo metódo http POST, método diferente de quando foi feita a chamada para mostrar o formlário. Por isso, foi possível manter a mesma url, /admin/sessao.
Para a inserção no banco de dados, precisamos do objeto do tipo Sessao completo, então vamos chamar o método do SessaoForm que fará isso pra gente. Além disso, temos que deixar o Spring ciente de que essa operação deverá ser feita dentro de uma transação. Faremos isso com a anotação @Transactional. Depois de inserir, vamos redirecionar para a listagem de sessões através da url /sala/id/sessoes, onde id é o valor do id da sala:


    @Controller
    public class SessaoController {

        @Autowired
        private SalaDao salaDao;

        @Autowired
        private FilmeDao filmeDao;

        @Autowired
        private SessaoDao sessaoDao;

        //demais métodos

        @PostMapping(value = "/admin/sessao")
        @Transactional
        public ModelAndView salva(@Valid SessaoForm form) {


            ModelAndView modelAndView = new ModelAndView("redirect:/sala/"+form.getSalaId()+"/sessoes");

            Sessao sessao = form.toSessao(salaDao, filmeDao);

            sessaoDao.save(sessao);

            return modelAndView;
        }

    }

O método que recebe a chamada para /sala/id/sessoes já existe, o listaSessoes, assim como a página de listagem, mas falta disponibilizarmos a lista de filmes para a view. Para isso, vamos criar um método no DAO para pegar a lista de filmes no banco de dados e alterar o método listaSessoes para inserir a lista no objeto do tipo ModelAndView.

Voltando para o SessaoForm, vemos que colocamos nele diversas validações, então, caso alguma dessas validações não seja respeitada, o código de persistência não deveria sequer ser processado. Para isso, precisaremos validar se não ocorreu nenhum erro e, caso seja identificado algum, nós retornaremos o usuário para o próprio form. Assim como já fizemos no curso FJ-21, vamos pedir para o Spring nos fornecer um objeto do tipo BindingResult e usá-lo para a validação.
Porém, se o usuário tiver preenchido o formulário com erros, ele vai querer ver as informações que havia preenchido antes. Vamos então enviar para o método form o objeto do tipo SessaoForm:


    @PostMapping(value = "/admin/sessao")
    @Transactional
    public ModelAndView salva(@Valid SessaoForm form, BindingResult result) {

        if (result.hasErrors()) return form(form.getSalaId(),form);

        ModelAndView modelAndView = new ModelAndView("redirect:/sala/"+form.getSalaId()+"/sessoes");

        Sessao sessao = form.toSessao(salaDao, filmeDao);

        sessaoDao.save(sessao);

        return modelAndView;
    }

Por fim, vamos alterar o método form para receber esse objeto como parâmetro e disponibilizá-lo na view:


    @Controller
    public class SessaoController {
        .
        .
        .

        @GetMapping("/admin/sessao")
        public ModelAndView form(@RequestParam("salaId") Integer salaId, SessaoForm form) {

            form.setSalaId(salaId)

            ModelAndView modelAndView = new ModelAndView("sessao/sessao");

            modelAndView.addObject("sala", salaDao.findOne(salaId));
            modelAndView.addObject("filmes", filmeDao.findAll());
            modelAndView.addObject("form", form);

            return modelAndView;
        }

        //demais métodos
    }

2.4 Exercício - Adicionando a sessão ao sistema

  1. Crie uma nova branch para começar a trabalhar no projeto:

     git checkout -b work
    
  2. No pacote br.com.caelum.ingresso.controller, crie uma classe com o nome SessaoController e anote-a com @Controller para que ela seja um controller para o Spring. Nessa classe, crie um método para atender as requisições na URI /admin/sessao para o verbo http GET. Esse método deve receber o id da sala como parâmetro na URI, o nome desse parâmetro será salaId:

    @Controller
    public class SessaoController {
    
      @GetMapping("/admin/sessao")
      public ModelAndView form(@RequestParam("salaId") Integer salaId) {
    
          ModelAndView modelAndView = new ModelAndView("sessao/sessao");
    
          return modelAndView;
      }
    }
    
  3. Crie uma classe de modelo no pacote br.com.caelum.ingresso.model para representar a sessão e anote-a com @Entity do pacote javax.persistence. Ela deve ter id, horario e deve estar vinculada a uma Sala e a um Filme. Para o horário, utilizaremos o tipo LocalTime do Java 8:

     @Entity
     public class Sessao {
    
         @Id
         @GeneratedValue
         private Integer id;
         private LocalTime horario;
    
         @ManyToOne
         private Sala sala;
    
         @ManyToOne
         private Filme filme;
    
         /**
         * @deprecated hibernate only
         */
         public Sessao() {
         }
    
         public Sessao(LocalTime horario, Filme filme, Sala sala) {
             this.horario = horario;
             this.setFilme(filme);
             this.sala = sala;
         }
    
         public LocalTime getHorarioTermino() {
             return this.horario.plusMinutes(filme.getDuracao().toMinutes());
         }
    
         //demais getters e setters
     }
    
  4. O método form, que mostra o formulário, deve disponibilizar para a tela a sala, baseado no parâmetro salaId, e a lista com os filmes:

    @Controller
    public class SessaoController {
    
      //atributos injetados com @Autowired
    
      @GetMapping("/admin/sessao")
      public ModelAndView form(@RequestParam("salaId") Integer salaId) {
    
          ModelAndView modelAndView = new ModelAndView("sessao/sessao");
    
          modelAndView.addObject("sala", salaDao.findOne(salaId));
          modelAndView.addObject("filmes", filmeDao.findAll());
    
          return modelAndView;
      }
    }
    
  5. Como o formulário da sessão usa mais de um modelo, criaremos uma classe chamada SessaoForm no pacote br.com.caelum.ingresso.model.form para representar especificamente o formulário:

    
    public class SessaoForm {
    
      private Integer id;
    
      @NotNull
      private Integer salaId;
    
      @DateTimeFormat(pattern="HH:mm")
      @NotNull
      private LocalTime horario;
    
      @NotNull
      private Integer filmeId;
    
      //getters e setters
    
    }
    
  6. Precisamos agora disponibilizar nosso SessaoForm na request para o formulário. Vamos aproveitar e já setar o id da sala para esse form:

    @Controller
    public class SessaoController {
    
     //atributos injetados com @Autowired
    
     @GetMapping("/admin/sessao")
     public ModelAndView form(@RequestParam("salaId") Integer salaId, SessaoForm form) {
    
         form.setSalaId(salaId);
    
         ModelAndView modelAndView = new ModelAndView("sessao/sessao");
    
         modelAndView.addObject("sala", salaDao.findOne(salaId));
         modelAndView.addObject("filmes", filmeDao.findAll());
         modelAndView.addObject("form", form);
    
         return modelAndView;
     }
    }
    
  7. Como precisamos salvar sessões no banco de dados e precisamos listar as sessões de uma sala, vamos criar o DAO de sessão no pacote dos daos:

     @Repository
     public class SessaoDao {
    
         @PersistenceContext
         private EntityManager manager;
    
         public void save(Sessao sessao) {
             manager.persist(sessao);
         }
    
         public List<Sessao> buscaSessoesDaSala(Sala sala) {
             return manager.createQuery("select s from Sessao s where s.sala = :sala",  
                                         Sessao.class)
                     .setParameter("sala", sala)
                     .getResultList();
         }
     }
    
  8. Na SessaoController, vamos criar um método que receberá o POST do nosso form e salvará uma sessão. Para isso, precisaremos criar também um método na classe SessaoForm que retorne um objeto Sessao.

    public class SessaoForm {
    
      //atributos
      //getters e setters
    
      public Sessao toSessao(SalaDao salaDao, FilmeDao filmeDao){
          Filme filme = filmeDao.findOne(filmeId);
          Sala sala = salaDao.findOne(salaId);
    
          Sessao sessao = new Sessao(horario, filme, sala);
          sessao.setId(id);
    
          return sessao;
      }
    }
    
    @Controller
    public class SessaoController {
    
      //demais atributos injetados com @Autowired
    
      @Autowired
      private SessaoDao sessaoDao;
    
      //demais métodos
    
      @PostMapping(value = "/admin/sessao")
      @Transactional
      public ModelAndView salva(@Valid SessaoForm form, BindingResult result) {
    
          if (result.hasErrors()) return form(form.getSalaId(),form);
    
          ModelAndView modelAndView = new ModelAndView("redirect:/admin/sala/"+
                                          form.getSalaId()+"/sessoes");
    
          Sessao sessao = form.toSessao(salaDao, filmeDao);
    
          sessaoDao.save(sessao);
    
          return modelAndView;
      }
    }
    
  9. Para mostrar as sessões de uma sala, vamos injetar o SessaoDao na classe SalaController:

     @Autowired
     private SessaoDao sessaoDao;
    
  10. Em seguida, precisamos disponibilizar as sessões na página de listagem de sessões de uma sala. Para isso, vamos alterar o método listaSessoes na classe SalaController, adicionando a instrução:

    view.addObject("sessoes", sessaoDao.buscaSessoesDaSala(sala));
    
  11. Agora que incluímos a sessão no projeto, vamos testar todas as funcionalidades que nosso projeto faz no momento, principalmente as relacionadas com a sessão.

  12. Com nossa tarefa de adicionar a sessão no sistema finalizada e testada, podemos juntar o que foi feito na branch work para a branch master através do rebase.

Para saber mais: Em casa

Se for utilizar o projeto em casa, lembre que o usuário e senha informados ao datasource que o Spring está gerenciando no xml do Spring devem ser os mesmos que o usuário e senha do seu banco de dados

Seus livros de tecnologia parecem do século passado?

Conheça a Casa do Código, uma nova editora, com autores de destaque no mercado, foco em ebooks (PDF, epub, mobi), preços imbatíveis e assuntos atuais.
Com a curadoria da Caelum e excelentes autores, é uma abordagem diferente para livros de tecnologia no Brasil.

Casa do Código, Livros de Tecnologia.

2.5 Analisando a Regra de Negócio

Implementamos a funcionalidade de adicionar a sessão na sala, mas temos que pensar no mundo real, onde a sessão que o usuário quer adicionar pode estar em conflito com as sessões já existentes. Portanto, antes de simplesmente adicionar, a sessão precisa passar por uma validação, que será feita por uma classe que criaremos com essa única responsabilidade. Vamos chamá-la de GerenciadorDeSessao e ela terá a função de verificar se a sessão que estamos tentando adicionar cabe entre as sessões daquela sala:


    public class GerenciadorDeSessao {

        public boolean cabe(Sessao sessaoAtual) {

            return true;
        }

    }

Para podermos fazer a verificação, temos que ter acesso a todas as sessões que acontecerão naquela sala, para isso pediremos a lista de sessões como parâmetro no construtor:


    public class GerenciadorDeSessao {

        private List<Sessao> sessoesDaSala;

        public GerenciadorDeSessao(List<Sessao> sessoesDaSala) {                
            this.sessoesDaSala = sessoesDaSala;
        }


        public boolean cabe(Sessao sessaoAtual) {

            return true;
        }
    }

Agora, precisamos percorrer a lista de sessões já existentes e, para cada sessão já alocada na sala, vamos checar:

Estamos usando a API de data do Java 8, que foi baseada na biblioteca Joda Time e possui vários métodos que vão nos ajudar a fazer essas validações:



    public boolean cabe(Sessao sessaoAtual) {
        for (Sessao sessaoDoCinema : sessoesDaSala) {
            if (!horarioIsValido(sessaoDoCinema, sessaoAtual)) {
                return false;
            }
        }
        return true;
    }

    private boolean horarioIsValido(Sessao sessaoExistente, Sessao sessaoAtual) {

        LocalDate hoje = LocalDate.now();

        LocalDateTime horarioSessao = sessaoExistente.getHorario().atDate(hoje);
        LocalDateTime horarioAtual = sessaoAtual.getHorario().atDate(hoje);

        boolean ehAntes = horarioAtual.isBefore(horarioSessao);

        if (ehAntes) {

            return horarioAtual
                    .plus(sessaoAtual.getFilme().getDuracao())
                    .isBefore(horarioSessao);
        } else {

            return horarioSessao
                    .plus(sessaoExistente.getFilme().getDuracao())
                    .isBefore(horarioAtual);
        }
    }

Nosso código está validando todas as possibilidades que listamos ainda há pouco. Contudo, como já estamos usando alguns recursos do Java 8, seria interessante usarmos mais alguns para nos acostumarmos com esses novos comandos. Por exemplo, ao invés de fazermos esse for e if, podemos usar outro recurso bem interessante do Java 8, o 'Stream', que transforma nossa lista em um fluxo de sessões:


    public boolean cabe(Sessao sessaoAtual) {
        Stream<Sessao> stream = sessoesDaSala
                .stream();

        // restante do código
    }

Agora, para cada objeto do fluxo, queremos verificar se o horário da sessão nova é válido e, como resposta de cada comparação, teremos um boolean. Para conseguirmos pegar cada objeto do fluxo e retornar um boolean, usaremos um especialista nisso, o método map() do Stream, que devolverá um novo fluxo, convertido no tipo que desejamos. No nosso caso, teremos um Stream<Boolean>:


    public boolean cabe(Sessao sessaoAtual) {
        Stream<Sessao> stream = sessoesDaSala
                .stream();

        Stream<Boolean> booleanStream = stream
                .map(sessaoExistente -> horarioIsValido(sessaoExistente, sessaoAtual));

        // restante do código
    }

Como já possuímos um fluxo com todos os Booleans, vamos checar se em algum momento o horário da sessão nova estava inválido. Se isso acontecer, saberemos que a sessão não cabe naquela sala. Temos então que fazer alguma comparação entre todos eles, para ter essa resposta de uma vez. Utilizando o comparador &, ficaria algo dessa forma :


if (boolean1 && boolean2 && ... && boleanX){
    return true;

}

return false;

Repare que, em ambos os casos, nós acabamos reduzindo tudo apenas para um boolean. Mas gerar esse código pode ser um pouco trabalhoso: teremos que percorrer novamente o Stream, passando elemento por elemento.
Felizmente, esse objeto é inteligente e já sabe fazer isso para nós. Existe outro método que nos ajudará com esse serviço, o reduce(), que consegue reduzir vários elementos em um único, a partir de uma lógica que passarmos para ele. No nosso caso, precisamos apenas falar qual é a estratégia que deve ser executada. Desse modo, usaremos o método Boolean.logicalAnd(b1, b2), onde b1 e b2 são dois booleanos e que nos retorna o resultado da operação lógica AND entre os parâmetros:


    public boolean cabe(Sessao sessaoAtual) {
        Stream<Sessao> stream = sessoesDaSala
                .stream();

        Stream<Boolean> booleanStream = stream
                .map(sessaoExistente -> horarioIsValido(sessaoExistente, sessaoAtual));

        booleanStream.reduce(Boolean::logicalAnd)

        // restante do código
    }

Queremos que seja devolvido um Boolean e que seja feita uma redução a cada dois booleanos utilizando o operador lógico &&, mas o que vai acontecer se nossa lista estiver vazia? É uma situação fora do esperado e em algum momento receberemos uma exception. Para evitar isso, uma solução bem conhecida por nós desenvolvedores é fazer uma validação na lista antes de usá-la no método cabe. No entanto, no Java 8 há uma forma mais elegante de resolvermos esse impasse. O próprio método reduce nos devolve um objeto do tipo Optional, que por trás dos panos já realiza esta verificação implicitamente:


    public boolean cabe(Sessao sessaoAtual) {
        Stream<Sessao> stream = sessoesDaSala
                .stream();

        Stream<Boolean> booleanStream = stream
                .map(sessaoExistente -> horarioIsValido(sessaoExistente, sessaoAtual));

        Optional<Boolean> optionalCabe = booleanStream.reduce(Boolean::logicalAnd)

        return optionalCabe.orElse(true);
    }

Ao utilizarmos o método orElse ocorre uma verificação para checar se existe qualquer valor, no nosso caso, booleano, atribuído ao Optional e, caso não exista, estamos definindo que o valor padrão true deverá ser retornado.

2.6 Testes de Unidade

Testes de unidade são testes que testam apenas uma classe ou método, verificando se seu comportamento está de acordo com o desejado. Em testes de unidade, verificamos a funcionalidade da classe e/ou método em questão passando o mínimo possível por outras classes ou dependências do nosso sistema.

Unidade

Unidade é a menor parte testável de uma aplicação. Em uma linguagem de programação orientada a objetos como o Java, a menor unidade é um método.

O termo correto para esses testes é testes de unidade, porém o termo teste unitário propagou-se e é o mais encontrado nas traduções.

Em testes de unidade, não estamos interessados no comportamento real das dependências da classe, mas em como a classe em questão se comporta diante das possíveis respostas das dependências, ou então, se a classe modificou as dependências da maneira esperada.

Para isso, quando criamos um teste de unidade, simulamos a execução de métodos da classe a ser testada. Fazemos isso passando parâmetros, caso necessário, ao método testado e definimos o resultado que esperamos. Se o resultado for igual ao que definimos como esperado, o teste passa. Caso contrário, falha.

Atenção

Muitas vezes, principalmente quando estamos iniciando no mundo dos testes, é comum criarmos alguns testes que testam muito mais do que o necessário, mais do que apenas a unidade. Tais testes se transformam em verdadeiros testes de integração (esses sim são responsáveis por testar o sistemas como um todo).

Portanto, lembre-se sempre: testes de unidade testam apenas unidades!

2.7 JUnit

O JUnit (junit.org) é um framework muito simples que facilita a criação destes testes de unidade e em especial sua execução. Ele possui alguns métodos que tornam seu código de teste bem legível e fácil de fazer as asserções.

Uma asserção é uma afirmação: alguma invariante que em determinado ponto de execução você quer garantir que é verdadeira. Se aquilo não for verdade, o teste deve indicar uma falha, a ser reportada para o programador, indicando um possível bug.

À medida que você mexe no seu código, você roda novamente toda aquela bateria de testes com um comando apenas. Com isso você ganha a confiança de que novos bugs não estão sendo introduzidos (ou reintroduzidos) conforme você cria novas funcionalidades e conserta antigos bugs. Mais fácil do que ocorre quando fazemos os testes dentro do main, executando um por vez.

O JUnit possui integração com todas as grandes IDEs, além das ferramentas de build, que vamos conhecer mais a frente. Vamos agora entender um pouco mais sobre anotações e o import estático, que vão facilitar muito o nosso trabalho com o JUnit.

Usando o JUnit - configurando Classpath e seu JAR no Eclipse

O JUnit é uma biblioteca escrita por terceiros que vamos usar no nosso projeto. Precisamos das classes do JUnit para escrever nossos testes. E, como sabemos, o formato de distribuição de bibliotecas Java é o JAR, muito similar a um ZIP com as classes daquela biblioteca.

Precisamos então do JAR do JUnit no nosso projeto. Mas quando rodarmos nossa aplicação, como o Java vai saber que deve incluir as classes daquele determinado JAR (dependência) junto com nosso programa?

É aqui que o Classpath entra na história: é nele que definimos qual o "caminho para buscar as classes que vamos usar". Temos que indicar onde a JVM deve buscar as classes para compilar e rodar nossa aplicação.

Há algumas formas de configurarmos o classpath:

No nosso caso, o próprio Maven já está gerenciando as dependências através do arquivo pom.xml.

Agora é a melhor hora de respirar mais tecnologia!

Se você está gostando dessa apostila, certamente vai aproveitar os cursos online que lançamos na plataforma Alura. Você estuda a qualquer momento com a qualidade Caelum. Programação, Mobile, Design, Infra, Front-End e Business! Ex-aluno da Caelum tem 15% de desconto, siga o link!

Conheça a Alura Cursos Online.

2.8 Fazendo nosso primeiro teste

Nosso GerenciadorDeSessao tem uma lógica bem crucial para nossa aplicação, pois é através dele que poderemos armazenar nossas sessões e não seria legal descobrirmos um bug em produção, pois poderia acarretar à empresa um grande prejuízo. Para evitar essa situação, vamos entregar um software com mais qualidade, onde tudo esteja funcionando corretamente. E, para podermos garantir que tudo estará conforme esperamos é necessário gerarmos testes.

Para isso, geraremos nossa primeira classe de teste. Por convenção, ela deverá ficar na mesma pasta que a classe que estamos testando, contudo, no source folder de teste do nosso projeto (src/test/java):

Outra convenção que teremos é no nome da classe, ela terá o mesmo nome da classe que estamos testando acrescido do sufixo Test :

    public class GerenciadorDeSessaoTest {


    }

Com nossa classe de teste já definida, basta criarmos nossos testes. Sendo assim, precisaremos definir quais serão os cenários que vamos cobrir. Por exemplo :

Vamos iniciar pelo teste mais simples, que é validar se podemos adicionar uma Sessão se a lista estiver vazia. Para isso, precisaremos criar este cenário, atravéz de um método anotado com @Test em nossa classe. A annotatio @Test diz para o JUnit que o método deve ser interpretado como um teste.

    public class GerenciadorDeSessaoTest {

        @Test
        public void deveAdicionarSeListaDaSessaoEstiverVazia(){

        }

    }

Agora, será necessário criar o cenário para que o nosso teste seja executado dentro do método, desse modo, vamos criar uma lista vazia e passar para o nosso GerenciadorDeSessao:

    public class GerenciadorDeSessaoTest {

        @Test
        public void deveAdicionarSeListaDaSessaoEstiverVazia(){
            List<Sessao> sessoes = Collections.emptyList();
            GerenciadorDeSessao gerenciador = new GerenciadorDeSessao(sessoes);

        }

    }

Precisamos ainda criar nossa Sessao e pedir para nosso GerenciadorDeSessao a validar:

    public class GerenciadorDeSessaoTest {

        @Test
        public void deveAdicionarSeListaDaSessaoEstiverVazia(){
            List<Sessao> sessoes = Collections.emptyList();
            GerenciadorDeSessao gerenciador = new GerenciadorDeSessao(sessoes);

            Filme filme = new Filme("Rogue One", Duration.ofMinutes(120), "SCI-FI", BigDecimal.ONE);
            filme.setDuracao(120);
            LocalTime horario = LocalTime.parse("10:00:00");
            Sala sala = new Sala("");

            Sessao sessao = new Sessao(horario, filme, sala);

            boolean cabe = gerenciador.cabe(sessao);

        }

    }

O cenário do nosso teste está pronto, mas repare que em nenhum momento fizemos alguma validação para checar se tudo deu certo como imaginávamos que deveria. Portanto, faremos a parte mais importante do teste : Verificações. Usaremos a classe Assert que é uma especialista nisto. Como sabemos que o resultado do nosso teste deve ser true, então, vamos pedir para ela verificar isso:

    public class GerenciadorDeSessaoTest {

        @Test
        public void deveAdicionarSeListaDaSessaoEstiverVazia(){
            List<Sessao> sessoes = Collections.emptyList();
            GerenciadorDeSessao gerenciador = new GerenciadorDeSessao(sessoes);

            Filme filme = new Filme("Rogue One", Duration.ofMinutes(120), "SCI-FI", BigDecimal.ONE);
            filme.setDuracao(120);
            LocalTime horario = LocalTime.parse("10:00:00");
            Sala sala = new Sala("");

            Sessao sessao = new Sessao(horario, filme, sala);

            boolean cabe = gerenciador.cabe(sessao);

            Assert.assertTrue(cabe);
        }

    }

2.9 Exercício - Garantindo que a validação de horários para cadastrar uma sessão está correta

  1. Precisamos verificar se o horário da sessão que estamos gravando não irá encavalar com o horário de uma sessão já existente. Sendo assim, vamos criar uma classe que faça essa validação.

    Crie a classe GerenciadorDeSessao no pacote br.com.caelum.ingresso.validacao e, baseado em uma lista de sessões de uma sala, verifique se o horário da sessão que queremos gravar cabe nessa sala.

    public class GerenciadorDeSessao {
    
      private List<Sessao> sessoesDaSala;
    
      public GerenciadorDeSessao(List<Sessao> sessoesDaSala) {
          this.sessoesDaSala = sessoesDaSala;
      }
    
      private boolean horarioIsValido(Sessao sessaoExistente, Sessao sessaoAtual) {
    
          LocalDate hoje = LocalDate.now();
    
          LocalDateTime horarioSessao = sessaoExistente.getHorario().atDate(hoje);
          LocalDateTime horarioAtual = sessaoAtual.getHorario().atDate(hoje);
    
          boolean ehAntes = horarioAtual.isBefore(horarioSessao);
    
          if (ehAntes) {
    
              return horarioAtual
                      .plus(sessaoAtual.getFilme().getDuracao())
                      .isBefore(horarioSessao);
          } else {
    
              return horarioSessao
                      .plus(sessaoExistente.getFilme().getDuracao())
                      .isBefore(horarioAtual);
          }
      }
    
      public boolean cabe(Sessao sessaoAtual) {
    
          Optional<Boolean> optionalCabe = sessoesDaSala
                                          .stream()
                                          .map(sessaoExistente ->
                                              horarioIsValido(sessaoExistente, sessaoAtual)
                                          )
                                          .reduce(Boolean::logicalAnd);
    
          return optionalCabe.orElse(true);
      }
    }
    
  2. Precisamos garantir que a validação que acabamos de implementar está funcionando. Para isso, criaremos um teste unitário que vai validar os cenários que previmos.
    Crie a classe GerenciadorDeSessaoTest no pacote br.com.caelum.ingresso.validacao, porém, no source folder de teste (src/test/java). Caso as pastas test e java não existam, crie elas primeiro. A anotação @Test e a classe Assert devem ser importadas do pacote org.junit :

     public class GerenciadorDeSessaoTest {
    
         @Test
         public void garanteQueNaoDevePermitirSessaoNoMesmoHorario() {
    
             Filme filme = new Filme("Rogue One", Duration.ofMinutes(120), "SCI-FI");
             LocalTime horario = LocalTime.parse("10:00:00");
    
             Sala sala = new Sala("");
    
             Sessao sessao = new Sessao(horario, filme, sala);
    
             List<Sessao> sessoes = Arrays.asList(sessao);
    
             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");
             LocalTime horario = LocalTime.parse("10:00:00");
    
             Sala sala = new Sala("");
             List<Sessao> sessoes = Arrays.asList(new Sessao(horario, filme, sala));
    
             Sessao sessao = new Sessao(horario.minusHours(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");
             LocalTime horario = LocalTime.parse("10:00:00");
             Sala sala = new Sala("");
    
             List<Sessao> sessoesDaSala = Arrays.asList(new Sessao(horario, filme, sala));
    
             GerenciadorDeSessao gerenciador = new GerenciadorDeSessao(sessoesDaSala);
    
             Sessao sessao = new Sessao(horario.plusHours(1), filme, sala);
    
             Assert.assertFalse(gerenciador.cabe(sessao));
    
         }
    
         @Test
         public void garanteQueDevePermitirUmaInsercaoEntreDoisFilmes() {
             Sala sala = new Sala("");
    
             Filme filme1 = new Filme("Rogue One", Duration.ofMinutes(90), "SCI-FI");
             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");
             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);
    
             Sessao sessao = new Sessao(LocalTime.parse("13:00:00"), filme, sala);
    
             Assert.assertTrue(gerenciador.cabe(sessao));
    
         }
     }
    
  3. Execute a classe GerenciadorDeSessaoTest pelo Eclipse usando o JUnit. Para isso, clique com o botão direito do mouse na classe, escolha a opção Run as e, em seguida, clique em JUnit Test.

  4. Depois de termos testado nosso GerenciadorDeSessao e nos certificarmos que ele está pronto para uso, podemos usá-lo em nosso controller para validar as sessões antes de serem inseridas:

    @Controller
    public class SessaoController {
    
      // restante do código
    
      @PostMapping("/admin/sessao")
      @Transactional
      public ModelAndView salva(@Valid SessaoForm form, BindingResult result) {
    
          if (result.hasErrors()) return form(form.getSalaId(), form);
    
          Sessao sessao = form.toSessao(salaDao, filmeDao);
    
          List<Sessao> sessoesDaSala = sessaoDao.buscaSessoesDaSala(sessao.getSala());
    
          GerenciadorDeSessao gerenciador = new GerenciadorDeSessao(sessoesDaSala);
    
          if (gerenciador.cabe(sessao)) {
              sessaoDao.save(sessao);
              return new ModelAndView("redirect:/admin/sala/" + form.getSalaId() + "/sessoes");
          }
    
          return form(form.getSalaId(), form);
      }
    
    }
    
  5. Execute a aplicação e tente adicionar algumas sessões com horários conflitantes para garantir que a validação está ocorrendo.