Será que o Struts 1 estava certo?

Só para você não estranhar. O blog é sobre Spring e os exemplos usados durante o post são baseados no Spring MVC. Só que esse realmente é um post de um tema mais geral.

Há muito tempo atrás tínhamos o Struts1 e o seu jeito pouco automático de fazer as coisas. Recebíamos request response como argumentos e, além disso, recebíamos também um outro parâmetro que, se não me engano, era do tipo ActionForm. Esse bendito/maldito desse ActionForm fazia o povo agonizar. Você basicamente tinha que copiar os atributos da sua classe para lá e, se tudo chegasse como deveria, você criava seu objeto de domínio e seguia com sua vida. Ele até “evoluiu” e permitiu que você tivesse atributos do mesmo tipo do seu domínio, para minimizar a duplicidade.

    public class AutorForm extends ActionForm {
      private Autor autor = new Autor();

      public void setAutor(Autor autor) {
        this.autor = autor;
      }

      public Autor getAutor(){
        return this.autor;
      }
    }

E talvez essa tenha sido a pior contribuição do Struts para os frameworks web do mundo Java. Todo mundo começou a achar que os formulários da aplicação deveriam mapear para objetos de classes do seu domínio. Claro temos os casos felizes, como o que segue abaixo.

    public String novo(@Valid Autor autor,BindingResult bindingResult){
      if (bindingResult.hasErrors()) {
        return form(autor);
      }   

      autorDao.save(autor);
      return "redirect:/autores";
    }

Só que, o meu sentimento, é que na maioria das vezes o caso não é feliz, como esse aqui.

    public String novo(@Valid Livro livro,BindingResult bindingResult){
      if (bindingResult.hasErrors()) {
        return form(livro);
      }   

      autorDao.save(livro);
      return "redirect:/livros";
    }

Você pode até parar e pensar: calma ê, isso daí é um cadastro de um livro com autor, então faz sentido criar um objeto Livro  direto. Só que é isso mesmo que o framework faz para você? O objeto do tipo Livro que foi criado, está em perfeito estado para uso? Vamos ver.

    if(livro.getAutor().getEstado().equals(Estado.BLA)) {
      ...
    }

O resultado desse código é um belo de um NPE. O estado do autor não foi preenchido automaticamente e o programador, olhando apenas para essa parte do código, não tem como saber disso. Dessa situação começam a proliferar as verificações de null no código. Ah, então vou fazer com que o método retorne Optional! Só que o estado é obrigatório, essa solução não deveria passar num código review.

Os exemplos não param aqui. Agora temos uma tela onde é necessário atualizar apenas o email do autor. E o que acontece muito é o código acabar com esse método no controller.

    public String atualizaEmail(Autor autor,BindingResult bindingResult){
      if (bindingResult.hasErrors()) {
        return form(livro);
      }   

      ...
    }

E agora? Como você vai validar que o email não veio em branco? O @Valid vai tentar aplicar a validação no objeto inteiro e ele não está completamente preenchido. Você pode até buscar uma saída e ter perfis de validação na Bean Validation. Uma outra solução que eu já vi, é a de retirar as anotações de validação do modelo e criar métodos que fazem validações específicas. Perceba, uma simples tela afetou a definição do seu modelo!

Para finalizar, quero que você note que eu nem falei do fato de você ter um monte de setter e getter no seu domínio que você não necessariamente quer. E uma parte importante, eu também era meio que a favor de fazer a maioria das coisas que eu estou criticando no post. Para mim parecia um tradeoff ok. Deixou de ser quando eu percebi o quanto de problema básico a gente tinha nos sistemas.

  1. NPE por conta de objetos inconsistentes
  2. Validações falhando por conta de ficar preso ao lance de usar a classe de domínio
  3. Métodos claramente de view explodindo nos domínios. Você tenta buscar pelo método na sua IDE e um monte só é achado na view.
  4. Dificuldade em montar um fluxo de uma tela porque você fica dependente de já ter o modelo escrito. Aí, já que vai escrever o modelo, já mapeia para o ORM e por aí vai.
  5. Uso quase nulo do construtor, potencializando a criação de objetos que não possuem a consistência exigida pelo negócio.

Talvez existam até mais motivos, mas esses foram o que vieram direto na minha mente.

Já faz um bom tempo que não associo mais formulários diretamente com domínio. E eu agradeço o Struts1 por isso! Os caras eram visionários, de verdade. Podemos agradecer também ao livro DDD, mas prefiro agradecer ao Struts mesmo :P. Está definido na minha vida que a parte web da aplicação é algo separado do domínio e faço de tudo para que as coisas não se misturem. Pegando o primeiro exemplo que eu dei aqui, que já o caso feliz. Eu faria assim:

    public String novo(@Valid NovoAutorForm form,BindingResult bindingResult){
      if (bindingResult.hasErrors()) {
        return novoForm(form);
      }   

      Autor autor = form.toAutor();
      ...
    }

Agora pegando o exemplo do cadastro de autor.

    public String novo(@Valid NovoLivroForm form,BindingResult bindingResult){
      if (bindingResult.hasErrors()) {
        return novoForm(form);
      }   

      Livro livro = form.toLivro(autorDao);
      ...
    }

Para fechar, o exemplo da atualização do cadastro do email.

    public String atualizaEmail(@Valid AtualizaEmailAutorForm form,BindingResult bindingResult){
      if (bindingResult.hasErrors()) {
        return novoForm(form);
      }   

      Autor autor = autorDao.findById(form.getId());
      autor.setEmail(form.getEmail());

      ...
    }

Para todos os exemplos eu crio objetos que representam o formulário, até para o caso super feliz. Tive uma diminuição absurda em erros bobos na aplicação, como validações simples que falhavam e NPEs. As classes de domínio ficaram bem mais enxutas, apenas com métodos que realmente precisam ser acessados. Depois de um tempo usando, posso dizer que o lance da duplicidade é coisa pequena perto dos ganhos. E para ser sincero, não é bem uma duplicação. É apenas uma incrível coincidência e, muitas vezes, uma “forçada de coincidência” por parte do programador(a) .

As classes que representam forms, em geral, tem os atributos que devem ser mapeados do formulário html(id do autor é criado como um Integer ou Long) e também possuem um método que pegam as informações e criam os objetos em questão. E aí, por exemplo, já temos um belo de um uso para o construtor das classes de domínio. Outro ponto possivelmente ruim é essa passagem do DAO como argumento do método toYYY.  Eu considero má prática, mas acho pior ainda criar uma outra classe de conversão para isso. Entre duas ruins, fui com a que eu achava menos deprimente.

Claro que agora temos alguma duplicidade e precisamos também escrever mais código. Só que os ganhos tem sido tão bons, que estou bem ok com isso. Você agora tem uma classe super coesa em relação ao formulário em questão e, se precisar, pode criar quantos métodos quiser específicos para aquele cenário.

Pelo que eu vejo no mercado, o padrão é atrelar com o domínio mesmo. Eu não tenho feito mais assim e voltei para os velhos ActionForms :).  E você, como faz? Fique a vontade, deixe o seu comentário e enriqueça a discussão.

8 thoughts on “Será que o Struts 1 estava certo?

  1. Oi Alberto, muito bacana o post!

    Realmente a gente criticava e achava bizarro esse modelo do Struts, mas agora percebemos que ele faz bastante sentido 😀
    É muito comum ver lógicas de view acopladas nas classes de domínio, algo que acaba gerando esses problemas que você citou.

    Uma outra coisa que também é comum nos projetos é misturar código de specs/libs nas classes de dominio. Por exemplo: @Entity(da JPA) nas classes de dominio.
    Isso deixa seu código de domínio acoplado à JPA, o que eventualmente pode ser ruim.
    Daria pra evitar isso de uma forma parecida que você mostrou no post, com um pouco de “duplicação” de código, mas o trade-off acaba valendo a pena.

    Essas ideias vão de encontro com o Clean Architecture do Uncle Bob: https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html

    Like

  2. Muito bom, Alberto.

    Trabalhei muito tempo da minha vida com Struts 1.2, já faz alguns bons anos…

    A idéia do Struts de ser *obrigado* a criar um ActionForm é muito ruim, embora a intenção seja boa. Eu realmente nunca gostei, especialmente quando se tratava de sistemas com centenas de CRUDs, onde 90% dos formulários se encaixavam perfeitamente bem com o modelo OO e/ou relacional. Nesse caso usar um ActionForm tem o mesmo prejuízo de usar um DTO entre camadas lógicas (layers), repetição de código e dificuldade na manutenção.

    Contudo, ter um objeto que representa o formulário da UI ou mesmo parte desse formulário é muito indicado em telas complexas ou interações rebuscadas, pois força o desenvolvedor a pensar na assinatura de seus métodos do controller, nas informações necessárias e seus tipos, e como você bem disse, permite desenhar o modelo o mais próximo possível do domínio (sem o ruído da view), bem no estilo proposto por práticas como DDD. Além disso, abre espaço para sanitização prévia de dados, diminui brechas de segurança, como white/black list dos parâmetros enviados pelo usuário, e permite mudança na view sem impacto no modelo. Esse objeto tem até um nome especial cunhado pelo Fowler: LocalDTO (https://martinfowler.com/bliki/LocalDTO.html).

    Enfim, apesar de concordar contigo, não curto essa prática quando se trata de CRUDs, pois no final acabamos com 2 objetos iguais em camadas lógicas distintas. Claro, exceções existem e devem sempre serem levadas em conta.

    Like

    • Obrigado pelos comentários, Rafael e Rodrigo. Em relação ao comentário de Rafael :). Realmente os formulários, muitas vezes, mapeiam direto para um registro numa tabela. O meu problema em passar antes pelo dominio, é que um crud simples já gera um objeto inconsistente. Como esse caso do livro que citei… O autor do livro não está completamente preenchido. E aí, nesse ponto, acho que entra um pouco da história vivida de cada um… Em vários dos sistemas que eu presenciei, este tipo de coisa foi se alastrando e os pontos negativos que eu citei ficaram quase que impossíveis de voltar atrás. Mas sem dúvida, mapear direto para o domínio é muito mais rápido!

      Like

  3. Boa reflexão, Alberto!

    Bacana esse pensamento de aproveitar coisas antigas que, muitas vezes, tratamos com desdém.

    O tio Bob, no Clean Architecture, separa os “mecanismos de entrega” (web, API, app, arquivo) do “core domain” criando objetos para os “casos de uso”. Cada “caso de uso” teria um “request model” e um “response model” que teriam os dados, validações e lógicas para entradas e saída de dados, respectivamente.

    Algo mais próximo da view, como uma lógica para destacar ou pintar de vermelho algo específico, ficaria apartado em um “view model”, parte do código de UI mesmo. A view ficaria sem lógica nenhuma.

    https://image.slidesharecdn.com/cleanarchitectureconfituratosend-140712181252-phpapp01/95/clean-architecture-11-638.jpg?cb=1405188813

    Tudo isso é muito lindo, mas muito complicado pros CRUDzão! 🙂

    Like

  4. Sinto que por muito tempo eu desprezei o crudzao… eles continuam simples, mas, pelo menos na minha experiência, continuam gerando problemas nos sistemas… Por conta disso tenho mantido esse lance de separação total dos dados que vem do form em relação ao domínio…

    Like

  5. Olá Alberto!
    Você poderia fazer um exemplo completo naquele projeto da casa do código do seu livro de Spring MVC. Seria muito show 😉

    Parabéns pelo o post e pelo o livro.

    Like

  6. Realmente! Pensando por este lado o Struts1 tinha muito sentido mesmo, eu normalmente crio uma Classe de resource (famosas DTOs).

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @EqualsAndHashCode(callSuper = true)
    @Relation(value="product", collectionRelation="products")
    public class ProductResource extends ResourceSupport {
    	private String code;
    	private String description;
    	private BigDecimal valueUnit;
    	private boolean active;	
    }
    

    E utilizo o Resource na validação do meu Recurso(API).

    	@PostMapping
    	public ResponseEntity<ProductResource> post(@Valid @RequestBody ProductResource productResource) {
    		Product product = this.productResourceAssembler.toDomain(productResource);
    		product = this.productService.save(product);
    		return new ResponseEntity<>(this.productResourceAssembler.toResource(product), HttpStatus.OK);
    	}
    

    Para este exemplo utilizei o productResourceAssembler para a conversão de resource para domain.
    Para este caso sem a classe de Resource poderia poluir ainda mais a classe de Domain, pelo fato de eu estar utilizando o Spring Hateos,
    Sempre fico na dúvida de utilizar Spring Hateos e as classes de Resources.

    Gostei do Post e da Reflexão. Muito Bom!

    Like

Leave a comment