terça-feira, 13 de dezembro de 2011

Estrutura de Dados Parte 1 - Pilha

Este será o primeiro de uma série de post's relacionados à estrutura de dados.

Nessa sequência vou explicar o que é cada uma das estruturas, seus respectivos funcionamentos e aplicações através de exemplos práticos em Java.

Neste primeiro post vou falar sobre a estrutura que eu acredito ser a mais simples, a Pilha.

Antes de definirmos tecnicamente, vamos tentar imaginar exemplos de pilhas no dia-a-dia.
Olhando em volta do quarto onde estou escrevendo, vejo uma pilha de caixas e sei que em alguma delas existe uma pilha de livros. De onde estou sentado consigo ver a pia da cozinha, e adivinhem, existe uma pilha de pratos para serem lavados (irei lavá-los assim que terminar esse post :-P ).

Como o exemplo da pilha de pratos já é bem discutido em outras literaturas, vou aproveitar as caixas e tentar diversificar um pouco.

Recentemente me mudei para um local mais próximo do meu trabalho e na hora de encaixotar os meus livros lembrei que lá não teria uma estante para colocá-los. Me dei conta de que os livros permaneceriam encaixotados por muito tempo, então tentei organizá-los conforme a minha necessidade. Sabia que os livros com menor probabilidade de leitura deveriam ficar no fundo, isso porque se eu precisasse de algum deles o acesso seria mais fácil.

Para pegar um dos primeiros livros colocados na caixa, terei que remover cada um dos que estão no topo.

Com isso, já conseguimos extrair algumas características de uma pilha, como por exemplo, os primeiros livros colocados serão os últimos a serem retirados da caixa. Esse conceito é mais conhecido como LIFO (Last In First Out).

Então tecnicamente falando, pilhas são estruturas de dados que servem para guardar vários elementos, onde os últimos elementos armazenados serão os primeiros a sair.

As pilhas possuem algumas operações básicas de manipulação: PUSH e POP.

PUSH: operação para adicionar um novo elemento no topo da pilha.
POP: operação para remover o elemento que está no topo da pilha.

Outras operações adicionar que podem ser úteis:

first: operação que informa qual é o elemento no topo da pilha.
last: operação que infroma qual é o elemento na base da pilha.
isEmpty: operação que informa se a pilha está vazia.
isFull: operação que informa se a pilha está cheia.

Uma implementação simples de Pilha em Java seria:

public class Pilha {

 private int[] elementos;
 private int posicao = -1;

 public Pilha(int tamanho) {
  this.elementos = new int[tamanho];
 }

 public void push(int i) {
  verificaSeAListaEstaCheia();
  elementos[++posicao] = i;
 }

 public int pop() {
  verificaSeAListaEstaVazia();
  return elementos[posicao--];
 }
 
 public final boolean isEmpty() {
  return posicao < 0;
 }
 
 public final boolean isFull() {
  return posicao==elementos.length-1;
 }
 
 public int size() {
  return posicao < 0 ? 0 : posicao+1;
 }

 private void verificaSeAListaEstaVazia() {
  if(isEmpty())
   throw new IllegalStateException("A Pilha esta vazia");
 }
 
 private void verificaSeAListaEstaCheia() {
  if(isFull())
   throw new IllegalStateException("A Pilha esta cheia");
 }

 public static void main(String[] args) {
  Pilha pilha = new Pilha(5);
  int numero = 1;
  while(!pilha.isFull()){
   pilha.push(numero++);
  }
  
  while(!pilha.isEmpty()){
   System.out.println(pilha.pop());
  }
 }

}

Reparem que ao executar o método main dessa classe, a saída na console será 5 4 3 2 1, nessa ordem, justamente por que o 5 foi  o último elemento inserido.

Fugindo um pouco do escopo do post, não foi tão simples chegar ao resultado final da class Pilha, nem à estrutura da classe, nem saber quais seriam os métodos, os nomes e a interação entre eles.
Fiz muitas modificações até chegar nesse resultado, e por várias vezes acabei gerando bugs em trechos de códigos que já estavam funcionando. O que fazer em um momento como esse?! Simples: Testes de Unidade. Não vou entrar em detalhes do que são Testes de Unidade, vou deixar isso para um post mais adiante. Abordei esse assunto apenas para colocar a minha classe de teste:

public class PilhaTest {

 private Pilha pilha;
 
 @Before
 public void before(){
  pilha = new Pilha(5);
 }

 @Test
 public void quandoIniciarUmPilhaDeveTerTamanhoZero() {
  assertEquals(0, pilha.size());
 }

 @Test
 public void quandoAdicionarUmElementoNaListaDeveModificarOTamanho() {
  pilha.push(1);
  assertEquals(1, pilha.size());
 }
 
 @Test
 public void quandoAdicionarDoisElementoNaListaDeveModificarOTamanho() {
  pilha.push(1);
  pilha.push(2);
  assertEquals(2, pilha.size());
 }
 
 @Test
 public void aPilhaDeveRetornarOUltimoElementoParte1() {
  pilha.push(3);
  assertEquals(3, pilha.pop());
 }
 
 @Test
 public void aPilhaDeveRetornarOUltimoElementoParte2() {
  pilha.push(3);
  pilha.push(9);
  assertEquals(9, pilha.pop());
 }
 
 @Test
 public void aPilhaDeveRetornarOUltimoElementoParte3() {
  pilha.push(3);
  pilha.push(9);
  assertEquals(9, pilha.pop());
  pilha.push(10);
  pilha.push(11);
  assertEquals(11, pilha.pop());
  assertEquals(10, pilha.pop());
  assertEquals(3, pilha.pop());
 }
 
 @Test
 public void oPopDeveRemoverOElementoDaLista() {
  pilha.push(3);
  pilha.push(9);
  assertEquals(9, pilha.pop());
  assertEquals(3, pilha.pop());
 }
 
 @Test
 public void oPopDeveAlterarOTamanhoDaLista() {
  assertEquals(0, pilha.size());
  
  pilha.push(3);
  pilha.push(9);
  assertEquals(2, pilha.size());
  
  assertEquals(9, pilha.pop());
  assertEquals(1, pilha.size());
  
  assertEquals(3, pilha.pop());
  assertEquals(0, pilha.size());
 }
 
 @Test
 public void perguntarParaUmaPilhaVaziaSeEleaEstaVazia(){
  assertTrue(pilha.isEmpty());
 }
 
 @Test
 public void perguntarParaUmaPilhaCheiaSeEleaEstaVazia(){
  pilha.push(10);
  assertFalse(pilha.isEmpty());
 }
 
 @Test
 public void perguntarParaUmaPilhaQueJaFoiBemManipuladaSeElaEstaVaziaParte1(){
  pilha.push(10);
  pilha.pop();
  assertTrue(pilha.isEmpty());
 }
 
 @Test
 public void perguntarParaUmaPilhaQueJaFoiBemManipuladaSeElaEstaVaziaParte2(){
  pilha.push(10);
  pilha.pop();
  pilha.push(10);
  pilha.pop();
  assertTrue(pilha.isEmpty());
 }
 
 @Test
 public void perguntarParaUmaPilhaQueJaFoiBemManipuladaSeElaEstaVaziaParte3(){
  pilha.push(10);
  pilha.pop();
  pilha.push(10);
  assertFalse(pilha.isEmpty());
 }
 
 @Test(expected=IllegalStateException.class)
 public void quandoChamarOPopEmUmaPilhaVaziaParte1() {
  pilha.pop();
 }
 
 @Test(expected=IllegalStateException.class)
 public void quandoChamarOPopEmUmaPilhaVaziaParte2() {
  pilha.push(1);
  assertEquals(1, pilha.pop());
  pilha.pop();
  
 }
 
 @Test(expected=IllegalStateException.class)
 public void quandoChamarOPushEmUmaListaQueEstaCheia() {
  pilha.push(1);
  pilha.push(2);
  pilha.push(3);
  pilha.push(4);
  pilha.push(5);
  
  pilha.push(6);
 }
 
 @Test
 public void perguntarParaUmaPilhaCheiaSeEleaEstaCheia(){
  pilha.push(1);
  pilha.push(2);
  pilha.push(3);
  pilha.push(4);
  pilha.push(5);
  assertTrue(pilha.isFull());
 }
 
 @Test
 public void perguntarParaUmaPilhaVaziaSeEleaEstaCheia(){
  assertFalse(pilha.isFull());
 }
 @Test
 public void perguntarParaUmaPilhaSemiVaziaSeEleaEstaCheia(){
  pilha.push(1);
  pilha.push(2);
  assertFalse(pilha.isFull());
 }
 
 @Test
 public void perguntarParaUmaPilhaQueJaFoiBemManipuladaSeElaEstaCheia(){
  pilha.push(1);
  pilha.push(2);
  pilha.push(3);
  pilha.push(4);
  pilha.push(5);
  assertTrue(pilha.isFull());
  pilha.pop();
  assertFalse(pilha.isFull());
 }
}


Pilhas podem ser usadas para várias situações, exemplos clássicos são os de uma linguagem de programação que usa a pilha para guardar a chamada de métodos (veja mais neste post sobre recursividade) ou uma calculadora que faz o empilhamento das operações para depois executá-las na ordem em que foram inseridas.
Aguardem o próximo post sobre Filas.

quarta-feira, 23 de novembro de 2011

Seu código é legível?

Não sei como isso funciona para as outras pessoas, mas sempre que volto para um código que escrevi há algum tempo, tenho dificuldade em lembrar o propósito ou o funcionamento desse código. Pior que isso só quando tenho que mostrar pra alguém e esse alguém percebe que nem eu estou entendendo o que eu escrevi. Isso porque no começo da minha jornada no mundo da programação, dava pouco valor a alguns detalhes que podem fazer uma enorme diferença na hora de ler e entender o código.

Imaginando o cenário de uma locadora, vamos analisar o seguinte trecho de código:

    Cliente cliente = new Cliente();
    cliente.setNome("Geraldo Ferraz");
 
    Filme filme = new Filme();
    filme.setNome("Star Wars");
 
    Locadora locadora = new Locadora();
 
    locadora.getFilmes().add(filme);
    locadora.getClientes().add(cliente);
 
    Locacao locacao = new Locacao();
    locacao.setCliente(cliente);
    locacao.setFilme(filme);
    locacao.setData(Calendar.getInstance());
 
    locadora.getLocacoes().add(locacao);
    locadora.getLocacoes().remove(locacao);

O que esse código faz?
Se você concluiu rapidamente que esse código está cadastrando o cliente "Geraldo" e imediatamente fazendo a locação do filme "Star Wars" é porque provavelmente você está familiarizado com essa  implementação. Pra quem não está e mesmo assim chegou na mesma conclusão, onde sentiu mais dificuldade?

Teria sido mais fácil se o código estivesse assim:

    Cliente geraldo = new Cliente("Geraldo Ferraz");
    Filme starWars = new Filme("Star Wars");

    Locadora locadora = new Locadora();
    locadora.associar(geraldo);
    locadora.adicionarAoAcervo(starWars);
 
    locadora.locarFilme(starWars).para(geraldo);
    geraldo.devolverPara(locadora).oFilme(starWars);

Não importa o nível de experiência que você tenha com programação, o segundo código é mais legível até por quem não é desenvolvedor. Isso porque o segundo código é muito mais expressivo e semântico.

Antes de continuarmos, vamos definir o que é expressividade e semântica.

Semântica:
É o estudo do significado. A semântica linguística estuda o significado usado por seres humanos para se expressarem através da linguagem.

Expressividade:
Claro, significativo, que dá a entender, enérgico.

Então se aplicarmos o conceito de semântica e expressividade nos nossos códigos, teremos códigos que são compreensíveis, claros e sem qualquer tipo de dupla interpretação.
Para conseguirmos um código expressivo e semântico precisamos ser críticos com o que escrevemos e temos sempre que fazer a seguinte pergunta: "será que outros entenderão o que escrevi?".

Ainda dentro do mesmo cenário (locadora), vamos analisar o seguinte código:

 public void executar(Locacao loc) {
     long dif = 0;
     
     if (loc.getDataEntrega().getTimeInMillis() < Calendar.getInstance().getTimeInMillis()) {
      
      long hj = Calendar.getInstance().getTimeInMillis();
      long dtLoc = loc.getDataEntrega().getTimeInMillis();
      dif = (hj-dtLoc)/1000/60/60/24;
      
     }
     loc.setValorPago(loc.getFilme().getCategoria().getPreco() * (dif+1));
     
     filmes.add(loc.getFilme());
 }

O código acima não possui expressividade ou semântica alguma. Podemos notar isso claramente quando demoramos alguns minutos para saber que esse código está fazendo a devolução de um filme para a locadora.

Para tornar esse código mais expressivo e semântico, precisamos ser críticos com pequenos detalhes.

"executar" é algo muito genérico para o nome de um método, precisamos deixá-lo com um significado mais forte, algo que deixe claro qual é a verdadeira intenção do método. Vamos experimentar "efetuarDevolucao".

Muitos desenvolvedores estão acostumado a abreviar os nomes das variáveis. Nunca entendi essa prática, pensava que fosse por hábito ou alguma convenção, mas logo percebi que era por pura preguiça. Com todo o poder que as IDEs têm hoje em dia (auto-complete), não justifica abreviar os nomes das variáveis. Algumas pessoas podem até defender a Notação Húngara, mas eu acredito que se o nome for expressivo o suficiente chegaremos à mesma conclusão que a Notação Húngara propõe.
Vamos mudar "loc" para "locacao", "dif" para "diferencaDeDias", "dtLoc" para "dataLocacao" e "hj" para "hoje".

Vejamos como ficou nosso código depois dessas pequenas modificações:

public void efetuarDevolucao(Locacao locacao) {
     long diferencaDeDias = 0;
     
     if (locacao.getDataEntrega().getTimeInMillis() < Calendar.getInstance().getTimeInMillis()) {
      
      long hoje = Calendar.getInstance().getTimeInMillis();
      long dataLocacao = locacao.getDataEntrega().getTimeInMillis();
      diferencaDeDias = (hoje-dataLocacao)/1000/60/60/24;
      
     }
     locacao.setValorPago(locacao.getFilme().getCategoria().getPreco() * (diferencaDeDias+1));
     
     filmes.add(locacao.getFilme());
 }

Bem melhor, mas ainda está longe do ideal. Vamos analisar o primeiro "if", que pega a data da entrega e verifica se a mesma é menor que a data de hoje. O que, em muitas palavras, está perguntando se a devolução está atrasada. Essa pergunta está relacionada diretamente ao estado da locação, então devemos fazer essa pergunta diretamente pra a locação. Vejamos o resultado:

public void efetuarDevolucao(Locacao locacao) {
     long diferencaDeDias = 0;
     
     if (locacao.estaAtrasada()) {
      
      long hoje = Calendar.getInstance().getTimeInMillis();
      long dataLocacao = locacao.getDataEntrega().getTimeInMillis();
      diferencaDeDias = (hoje-dataLocacao)/1000/60/60/24;
      
     }
     locacao.setValorPago(locacao.getFilme().getCategoria().getPreco() * (diferencaDeDias+1));
     
     filmes.add(locacao.getFilme());
 }

Repare que até aqui não houve mudança na estrutura, apenas modificações simples nos nomes dos métodos e variáveis e mesmo assim já conseguimos ter uma leitura muito melhor do código.

Robert C. Martin, autor do livro Agile Software Development, Principles, Patterns, and Practices e Clean Code (um dos melhores livros que já li), fala sobre um princípio muito importante: Single responsability principle. O princípio explica que um módulo deve ter uma única responsabilidade, e no livro ele descreve a "responsabilidade" como "um motivo para mudar", e cita o exemplo de um gerador de relatório. Esse gerador tem dois  motivos para mudar, primeiro, o conteúdo exibido, segundo, o layout. Esses dois motivos bem definidos são considerados motivos diferentes, portanto responsabilidades diferentes.
Com base nesse conceito, podemos perceber que o método "efetuarDevolucao" está com mais responsabilidades do que uma simples devolução. Para resolver isso temos que entender qual é o fluxo da devolução:
  1. Efetuar o pagamento
  2. Devolver o filme para o acervo.
Então devemos isolar a regra do pagamento na própria classe Locacao, resultando no seguinte código:

public void efetuarDevolucao(Locacao locacao) {
 locacao.efetuarPagamento();
 devolverFilmeParaOAcervo(locacao.getFilme());
}

private void devolverFilmeParaOAcervo(Filme filme) {
 filmes.add(filme);
}

Resolvemos a primeira parte, agora vamos ver como ficou a classe locação.

public void efetuarPagamento(){
  long diferencaDeDias = 0;
      
      if (dataEntrega.getTimeInMillis() < Calendar.getInstance().getTimeInMillis()) {
       
       long hoje = Calendar.getInstance().getTimeInMillis();
       long dataLocacao = dataEntrega.getTimeInMillis();
       diferencaDeDias = (hoje-dataLocacao)/1000/60/60/24;
       
      }
      valorPago = filme.getCategoria().getPreco() * (diferencaDeDias+1);
 }

Bem, parece que apenas empurramos o problema pra dentro da classe Locacao. Ainda não está claro como o método efetuarPagamento funciona. Para melhorar esse código devemos seguir os mesmos passos anteriores. Veja que as variáveis já possuem nomes significativos, então vamos definir quais são os passos para efetuar o pagamento e identificarmos se o método está com mais responsabilidade do que deveria.
  1. Verificar se já está pago.
  2. Verificar se está atrasado.
  3. Se estiver atrasado, cobrar multa.
  4. Se não estiver atrasado, fazer cobrança normal.
Com um pouco de trabalho, chegamos ao seguinte resultado:

public class Locacao {
 
 private DateUtil util = new  DateUtil();
 
 public void efetuarPagamento() {
  if(!estaPago()){
   if (estaAtrasada()) {
    efetuarPagametnoAtrasado();
   }else{
    efetuarPagamentoPontual();
   }
  }
 }
 
 private boolean estaPago(){
  return valorPago > 0;
 }
 
 private boolean estaAtrasada() {
  return util.ehDepoisDeHoje(dataEntrega);
 }

 private void efetuarPagametnoAtrasado() {
  valorPago = filme.getCategoria().getPreco() * quantidadeDeDiasAtrasados();
 }
 
 private void efetuarPagamentoPontual() {
  valorPago = filme.getCategoria().getPreco();
 }

 private long quantidadeDeDiasAtrasados() {
  return util.diasEntre(dataEntrega, DateUtil.HOJE);
 }
}
Agora conseguimos entender facilmente o que o "efetuarPagamento" está fazendo. Para uma leitura rápida, não temos a necessidade de olhar o conteúdo de cada método, faremos isso apenas se realmente houver a necessidade de modificarmos algum deles especificamente.

O blog VidaGeek.net recentemente fez um post falando sobre expressividade e nele o autor tenta mostrar a importância da expressividade em contextos do mundo real, como o de um escritor.
O autor fala também que um escritor tem à sua disposição vários recursos dentro de sua ferramenta que é a linguagem. Cabe a cada escritor escolher qual recurso é mais recomendado no momento ou para o público alvo.

Discutir semântica não é uma tarefa fácil, talvez por ser algo subjetivo, o que é mais significativo pra mim talvez não seja para outra pessoa. Em termos técnicos, acredito que a melhor opção é tentar ser o mais simples e breve possível e lembrar sempre que um código legível não facilita a manutenção apenas para os outros, mas pra si mesmo também.


Referências:
Semantica
Separation of concern
Single Responsability
Cohesion

segunda-feira, 21 de novembro de 2011

Recursividade

O assunto deste post será Recursividade e para prender a sua a atenção vou começar com uma piada clássica (dedicado a @lacerdaph):

Para entender a recursividade, antes, você tem que entender a recursividade.




 Agora vamos a parte técnica. Primeiro vou tentar definir de maneira simples, o que é recursividade:

Recursividade é a capacidade que uma função/método tem de invocar a si mesmo. 

Em outras palavras a recursividade é a maneira de dividir um problema em vários subproblemas sem adicionar nova complexidade.

Com isso em mente temos o seguinte exemplo:

public static void main(String[] args) {
 diminui(10);
}

private static void diminui(int i) {
 System.out.println(i);
 if (i > 0)
 diminui(i - 1);
}
O método "diminui" serve para imprimir uma sequencia de números de maneira decrescente até o número zero. Note que para isso não existe nenhuma estrutura de loop como "while" ou "for", apenas uma chamada a ele mesmo com um argumento menor que o inicial. Podemos dizer que o método diminui é recursivo por que ele faz uma chamado a si mesmo se o número ainda for maior que zero.

Podemos aplicar a recursividade em exemplos mais complexos como exibir uma estrutura de pastas e arquivos. Em java ficaria assim:

public static void main(String[] args) {
 listaConteudo(new File("/home/geraldo"));
}

private static void listaConteudo(File arquivo) {
 if (arquivo.isDirectory()) {

  System.out.println("Diretorio " + arquivo.getName());

  File[] subArquivos = arquivo.listFiles();
  for (File subArquivo : subArquivos) {
   listaConteudo(subArquivo);
  }

  } else {
   System.out.println(" Arquivo " + arquivo.getName());
  }
 }
}


Ou em exemplos mais didáticos como a sequência de Fibonacci:

public static void main(String[] args) {
 System.out.println(fibonacci(10));
}

public static int fibonacci(int n) {
 return n == 1 || n == 0 ? 1 : fibonacci(n - 1) + fibonacci(n - 2);
}

Algo pra se ter em mente é que todo algoritmo recursivo pode ser feito também de maneira interativa. Em termos de performance é quase sempre mais vantajoso usar o algoritmo interativo, isso porque as linguagens que permitem execuções recursivas trabalham usando uma pilha de chamada ou um stack de execução.

Podemos entender a pilha como sendo o local onde a linguagem guarda o estado de cada método sendo executado. O último método na pilha é o que está sendo executado no momento como mostra a imagem a seguir:

Com isso podemos verificar que a maior limitação de uma chamada de método recursivo é o tamanho da pilha. Isso quer dizer que  se chegarmos a capacidade máxima da pilha não conseguiremos fazer chamada de nenhum outro método provocando um estouro na pilha (StackOverflow).

Existem alguns tipos de chamada recursiva e a escolha da mesma pode melhorar/otimizar o uso da pilha permitindo mais empilhamento de métodoos.

Recursividade simples: é quando fazemos apenas uma chamada recursiva por vez. Ex: o método diminui.
Recursividade dupla: é quando fazemos 2 ou mais chamadas recursivas por vez. Ex: o método fibonacci.
Recursividade em Cauda: é quando a chamada recursiva é a última instrução executada.

Vou focar nesta última pois acho que é a que traz o ganho mais significativo.
Para se trabalhar com a recursão em cauda, não basta ser a última linha de código, tem que ser a ultima instrução a ser executada pelo processador. No exemplo abaixo vamos calcular o fatorial de um número, parece que estamos usando recursão em cauda, mas note que a ultima instrução executada será a multiplicação de "n" e o retorno da chamada recursiva:

public static long fatorial(long n) {
 if (n == 0) return 1;
  return n * fatorial(n - 1);
 }
}

Isso faz com que a linguagem tenha que manter o método na pilha pra poder executar a multiplicação. No exemplo abaixo, visto que não tem mais nada pra fazer quando voltar da chamada recursiva, não é necessário manter esse método na pilha, então a linguagem pode remove-lo abrindo espaço para outra chamada.

public static long fatorial(long n, long acumulador) {
 if (n == 0) 
  return acumulador;
 return fatorial(n - 1, n * acumulador);
}

public static long factorial(long n) {
 return fatorial(n, 1);
}

Uma outra forma de ganhar performance e otimizar o uso da pilha é aplicar uma técnica chamada Memoization que consiste em guardar algum valor previamente calculado evitando assim um novo empilhamento e evitando o custo de processamento. Veja no exemplo abaixo novamente o cálculo da sequencia de Fibonacci:

private static Map<Long, Long> jaCalculados = new HashMap<Long, Long>();

public static long fibonacciMemoization(long n) {
 if (jaCalculados.containsKey(n))
  return jaCalculados.get(n);

 Long valorCalculado;
 if (n == 0 || n == 1)
  valorCalculado = n;
 else
  valorCalculado = fibonacciMemoization( n - 1 )+ fibonacciMemoization(n - 2);

 jaCalculados.put(new Long(n), valorCalculado);
 
 return valorCalculado;

}

Após muita conversa com amigos (@moreira_caelum e @renatoargh)  cheguei a conclusão de que gosto de recursividade quando a implementação torna o código mais expressivo e legível sem impactar significativamente no desempenho. A partir do momento em que a recursividade prejudica o entendimento ou a performance do algoritmo, prefiro o modelo iterativo.


Referências:
Recursividade (Wikipedia)
Recursividade Simples, Dupla e em Cauda
Recursividade em cauda
Memoization (Wikipedia)
Dica de leitura Analise de Algoritmo

sexta-feira, 1 de julho de 2011

Primeiro de Muitos

Vou postar assuntos relacionados a desenvolvimento de software, não me limitando a nenhuma linguagem, processo, metodologia, técnologia, framework específico.

O objetivo principal é postar coisas que aprendo no dia a dia para debater se aquela é a melhor maneira de se fazer, se existem melhores práticas, etc.