Porque você deve trocar o Maven pelo Gradle?

Gradle

Qual ferramenta de build é a melhor?

Esse assunto é polêmico hein! Razão de inúmeros flamewars, brigas, mortes, guerras, protestos, atentados terroristas.

Não vou dizer que farei um análise imparcial aqui, porque não vou. Mas também não vou apenas ficar cuspindo no prato que comi – embora não sei porque isso é um problema, cuspir… em um prato vazio… qual problema… já comi mesmo…

Enfim! Minha ideia é apenas mostrar bons motivos para se usar o Gradle no lugar do Maven, assim como alguns anos atrás eu tive bons motivos para usar o Maven no lugar do Ant.

Muitos já se convenceram que o Gradle é uma alternativa mais interessante, inclusive o novo sistema de build do Android SDK é em Gradle.

Existe até uma teoria – bem justificada – do Neil Ford da Thoughtworks que eventualmente todo mundo vai ficar puto com o Maven e abandoná-lo.

Mas voltaremos nisso mais pra frente. Primeiro queria fazer um pequeno retrospecto autobiográfico…

Minha história com ferramentas de build

Meu background profissional é principalmente relacionado a Java e posso afirmar com bastante segurança que no mundo Java as ferramentas de build mais conhecidas e usadas são duas: o Ant e o Maven.

Tive a oportunidade de usar ambas na prática, em grandes e pequenos projetos, e essa foi minha experiência resumida com cada uma:

Experiências com Ant

– car***o, não tenho idéia do que esse build.xml de 32989 linhas faz…
– meu deus do céu, que target que eu rodo primeiro?

Experiências com Maven

– que maravilha, todos os 726362 projetos estão padronizados. É só fazer mvn install
\o/

– ah, agora eu sei que o source está em src/main/java e os resources em src/main/resources
\o/

– preciso implementar, buildar e distribuir um maldito plugin só pra fazer isso?!?!
<o>

– meu projeto precisa herdar desse mega-gigante pom.xml da empresa? Mas eu estou duplicando praticamente tudo novamente só para especializar meu build.
<o>

Explicando o que aconteceu:
Na época do Ant existia um grande problema de falta de padronização entre diferentes projetos. Cada um colocava o código fonte e os recursos onde julgava melhor, cada um colocava os artefatos gerados em uma pasta diferente, sem falar que muitos projetos não usavam nenhuma ferramenta para gerenciamento de dependências.

Além disso, os próprios arquivos descritores de build eram despadronizados. Se alguém trocava de projeto, a primeira coisa era descobrir qual target do Ant rodar primeiro.

Quando implantamos o Maven na empresa em que eu trabalho houve um ganho instantâneo em termos de padronização, adaptação de novas pessoas nas equipes e gerenciamento de dependências.

Claro que nem tudo é perfeito, no começo tivemos problemas com integração com IDEs, resistência e adaptação das pessoas, builds mais lentos. Mas os ganhos gerais compensaram essas dores de cabeça iniciais.

Mas depois de muitos anos usando o Maven, você começa a esbarrar em problemas muito inconvenientes a medida em que os builds se torna mais sofisticados e fora do comum, te obrigando a fazer vários malabarismos somente para contornar as imposições e limitações da ferramenta.

O Maven fez sua parte, mas já pode ir embora agora

Por ser uma ferramenta de opinião muito forte, a ponto de ser até considerada dogmática, ele conseguiu trazer uma padronização muito grande entre projetos da comunidade Java, criando praticamente uma nova cultura:

  • repositórios Maven
  • layout padrão de diretórios: src/main, src/test, target
  • build lifecycle

Mas se o Maven trouxe tantos benefícios, porque alguns estão trocando ele pelo Gradle?

Gosto de pensar no Maven como uma grande ditadura totalitária que veio instaurar a ordem quando tudo estava em um estado de total caos. Você tem que seguir o modelo rígido de build que ele impõe, seguir somente as fases que ele determina, ou então você terá muito trabalho subvertendo o sistema!

Imposition over Configuration

Assim o Maven funciona muito bem para 90% das coisas mais comuns de um build, mas complica muito para aqueles 10% de detalhes específicos do seu projeto (leia mais sobre a Dietzler’s Law no link do Neal Ford acima), já que a maneira como o Maven permite extensão é limitada e complicada demais.

Já está na hora dessa ditadura acabar e dar origem a um sistema mais flexível.

Gradle junta o melhor dos dois mundos e vai além

Enquanto o Ant oferece total flexibilidade para você definir as tarefas do seu build e como elas se sucedem umas as outras, o Maven vem com a proposta de um ciclo de vida rígido que todo build deve seguir.

A proposta do Gradle é continuar reforçando essa cultura e padronização do Maven através de convenção ao invés de imposição.

O Gradle incorporou o melhor dos dois lados:
Do Ant:

  • tasks altamente configuráveis
  • ciclo de vida de build flexível baseado em grafo aciclico direcionado.
  • gerenciamento de dependências com repositórios Ivy

Do Maven:

  • padronização do layout de diretórios
  • padronização do build lifecycle
  • builds multi-projeto
  • gerenciamento de dependências com repositórios Maven

Mas o Gradle vai muito além do Ant e do Maven: ele implementa uma verdadeira DSL em Groovy, permitindo a maior flexibilidade possível; no fim das contas, seu descritor de build é um script!

Enquanto no Maven e no Ant para criar uma coisinha diferente para seu build é preciso que você implemente e disponibilize um plugin, no Gradle você pode programar sua task ali mesmo, no build file.

Perai, o descritor de build é um script, então significa que vou voltar para o caos novamente?

Não, não vai, e o segredo está em seu sistema de plugins.

Conhecendo melhor o Gradle

No Gradle, a unidade de trabalho é a Task; equivalente ao Target do Ant ou ao Phase e o Goal do Maven.

Como no Ant, essas tasks podem ter dependência para outras tasks, criando um grafo direcionado acíclico. Dessa forma, quando você executa uma task, o Gradle executa antes todas as dependências dessa task.

No entanto, o Gradle conta com um sistema de plugin, e vários plugins nativos, que evitam que os builds voltem para o caos novamente, através da convenção.

Um plugin no Gradle é essencialmente um conjunto de tasks e configurações pré-definidas, que você pode importar em seu projeto. É como se um plugin no Gradle fosse uma Trait do Scala ou um Mixin do Ruby, ficando muito fácil estender um build através da composição e não da herança.

Portanto com o Gradle temos tanto a convenção, que nos permite ter a padronização e o inicio rápido de um novo projeto, como também a flexibilidade para customizar o build da maneira que for necessária, através de uma poderosa DSL Groovy.

Veja como é simples um build file para um projeto Java, com classes, resources e testes unitários.

Arquivo build.gradle:

apply plugin: 'java'

Sim, com uma linha você tem um projeto Java que compila, testa e empacota um JAR.

Outras vantagens do Gradle

Só uma listinha para te convencer a usar o Gradle:

  • Descritor de build em Groovy e não em XML
  • Suporte a repositórios Maven e Ivy
  • Build incremental que funciona de verdade
  • Composição ao invés de Herança
  • Suporte nativo a várias linguagens como Groovy e Scala
  • Integração com Eclipse, IntelliJ
  • Dezenas de plugins disponíveis
  • Android SDK e Hibernate já usam (vai ficar fora dessa?)

Estou convencido! E agora?

Shut up and take my money
Eu pretendo fazer outros posts sobre o funcionamento do Gradle, tentando destilar um pouco da documentação que tem por aí, mas para quem ficou entusiasmado com o Gradle, o guia dele é bem completo.

Também recomendo o livro Building and Testing with Gradle que é gratuito para leitura online.

Anúncios

Type-safe null usando Option

Reconhecendo o erro bilionário

Em 1965, Sir Charles Antony Richard Hoare (Tony Hoare para os chegados), inventava o null, as referências nulas, enquanto projetava a linguagem orientada a objetos ALGOL W.

Muitos anos depois, em 2009 ele mesmo retoma esse fato em uma palestra:

I call it my billion-dollar mistake.

Ele chama o null de seu erro bilionário! Fonte de inúmero erros, vulnerabilidades e problemas em sistemas no mundo todo. De fato uma grande cagada!

O Problema e sua Consequências

Certamente o null é tratado com muita cautela por nós programadores quando escrevemos nosso código. Sendo profissionais cautelosos, sempre pensamos que uma ou outra referência pode estar nula, desconfiados de toda e qualquer variável.

O programador Java incauto, faria coisas do tipo:

Animal elefante = animais.get("elefante"); 
elefante.anda(); // Pode dar merda
Pessoa p = PessoaDAO.findByName("Felipe");
Documento d = p.getDocumento(); // Vai dar merda
d.getNumero(); // Já deu merda

Já o programador cauteloso e experiente não se arriscaria a tomar um NullPointerException na cara:

Animal elefante = animais.get("elefante");
if(elefante != null)
  elefante.anda();  // Agora é seguro
Pessoa p = PessoaDAO.findByName("Felipe");
if(p != null) {
  Documento d = p.getDocumento();
  if(d != null) { 
    d.getNumero();  
  }
}

Ser desconfiado dá trabalho, e deixa nosso código cheio de if-guards, que são feios e chatos de ler.

Claro que, com um bom design, é possível diminuir a quantidade de ifs, mas o grande problema é que eventualmente alguém vai esquecer do maldito if e um NullPointerException (ou Segmentation Fault) poderá explodir a qualquer momento em produção!

O problema é que o compilador não nos diz nada. Se fosse possível fazer o compilador trabalhar para gente, e pegar todos esses erros em compilação seria ótimo…

Evitando o erro

Muitas pessoas inteligentes perceberam o risco de se ficar manipulando nulls e inventaram maneiras para evitar seu uso.

Null Objects

Um exemplo é o pattern Null Object, que sugere a construção de objetos “sem comportamento” como uma alternativa.

O grande problema desses Null Objects é que se forem usado sem cuidado, podem introduzir bugs difíceis de se encontrar, pois eles de certa forma disfarçam o problema. Imagine: você tem um objeto na mão, chama alguns métodos sem nenhum problema só que na verdade esses métodos não estão fazendo nada.

Option

Um conceito muito interessante e bastante simples, trazido diretamente do mundo da programação funcional é o do tipo Option, também chamado de Maybe em algumas linguagens.

O Option é um tipo abstrato parametrizado (portanto Option[T]) que tem apenas dois filhos: Some[T] e None. De maneira muito simplificada, em Java seria algo do tipo:

public abstract class Option<T> {
	  public static <T> Option<T> of(final T conteudo) {
		if(conteudo == null) {
			return new None<T>();
		} else {
			return new Some<T>() {
		    	public T get() {
		    		return conteudo;
		    	}
		    };	
		}
	  }
}

abstract class Some<T> extends Option<T> {
  public abstract T get();
}

class None<T> extends Option<T> {}

Usamos o Option assim:

Option<String> some = Option.of("Tem coisa"); // devolve Some<String>
Option<String> none = Option.of(null); // devolve None

Portanto, sempre que uma função tem a possibilidade de devolver um valor inválido ou inexistente para determinado argumento, podemos embrulhar o resultado em um Option.

Por exemplo:

Option<Pessoa> pessoa = pessoaDAO.findById(1234L); 

Legal, dessa maneira pessoa nunca será null, mesmo que o registro 1234 não exista no banco de dados.

Só que quando eu quiser o conteúdo do Option, teria que fazer isso:

if(pessoa instanceof Some<?>) { 
  String nome = ((Some<Pessoa>) pessoa).get().getNome(); 
  System.out.println("Encontrei pessoa com nome: " +  nome); 
} else {
  System.out.println("Não encontrei ninguém");
}

Esse código, apesar de terrivelmente feio, é mais type-safe, já que o compilador nos força a fazer uma verificação antes de usar a instância de Pessoa.

Outra desvantagem dessa abordagem é que, além de ser desengonçado, é mais prático fazer um if(pessoa != null) . Por isso é difícil convencer um programador a usar isso. E agora?

Bom, em Scala e em outras linguagens mais funcionais temos Pattern Matching para melhorar as coisas:

Option<Pessoa> pessoa = pessoaDAO.findByPK(1234L); 
pessoa match {
  case Some(p) => println("Encontrei pessoa com nome: " + p.nome)
  case None => println("Não encontrei ninguém")
}

Ainda um pouco verboso, mas bem melhor!

Pattern Matching é ideal quando temos dois fluxos completamente diferentes dependendo se o Option é Some ou None. Para casos em que só se quer passar o valor para uma função, ou manipulá-lo de maneiras mais simples, podemos usar as construções apresentadas na próxima seção.

Indo um pouco além do óbvio

Option surgiu no mundo funcional, e é muito mais do que um simples container de coisas.

Na realidade, é possível manipular um Option de maneiras muito práticas e poderosas, especialmente em linguagens em que conseguimos passar funções como parâmetro. Essas construções geralmente são mais enxutas do que fazer Pattern Matching.

Tony Morris fez um post em seu blog mostrando algumas formas mais funcionais de se substituir diversos casos implementados com Pattern Matching, usando a própria API do Option.

Veja alguns exemplos em Scala:

Option<Pessoa> pessoa = pessoaDAO.findByPK(1234L);  // mesmo exemplo

// foreach
// Se for Some executa a função passada, se for None não faz nada.
pessoa.foreach(p => println(p.nome))

// map
// Se for Some executa a função e embrulha o retorno em um Option. Se for None devolve None.
val endereco:Option[Endereco] = pessoa.map(_.endereco) 

// getOrElse
// Se for Some devolve o conteúdo, se for None devolve o resultado do bloco passado como parâmetro
val p = pessoa.getOrElse(new Pessoa())

Note que muitos métodos de Option são os mesmos que existem nas Collection em Scala.
E pasmem, é possível usar Option em for-comprehensions, podendo até misturar listas com options.

val lista:List[Integer] = List(1,2,3)
val option:Option[Integer] = Some(1)

// Conhecemos for em listas
for( num <- lista) {
  println(num)
}

// Mas é possível usar Option
for(num <- option) {
  println(num)
}

Imagine que você quer navegar por uma estrutura de objetos cujos métodos retornam Option. É possível usar uma construção for sofisticada em Scala:

for { 
  pessoa <- pessoaDAO.findByPK(1234L)  
  endereco <- pessoa.getEndereco
  numero <- endereco.getNumero 
} {
  println("O número da casa da pessoa de ID 1234 é " + numero)
}

Se em alguma dessas linhas, o objeto for um None, o corpo do for não será executado. É como se um Option fosse uma lista de um único elemento.

Por fim…

Option é uma poderosa abstração para substituir o famigerado null, causa de tantos NullPointerExceptions inesperados.

Seu objetivo é tornar o código mais type-safe e, mesmo adicionando uma camada extra para alcançar isso, sua API engenhosa permite que o programador manipule os Option de maneiras muito eficientes, concisas e poderosas.

Quando estiver projetando uma API, considere o uso de Option em funções que podem ou não devolver um objeto. Use para tratar os casos de exceção, como por exemplo quando um registro não é encontrado no banco de dados, ou quando uma função não está definida para determinado argumento.