Categoria: Desenvolvimento (Página 4 de 8)

Dissecando o padrão de projetos Singleton

singleton O padrão de projetos Singleton consiste em uma forma de garantirmos que teremos uma única instância de uma determinada classe no programa atual.

Por exemplo, em um programa Java para desktop podemos criar um Singleton da classe que gerencia a conexão com o banco de dados.

Este é um dos design patterns mais simples que existem, mas ele possui algumas nuances importantes de se entender do ponto de vista de implementação.

Implementação inicial em Java

Para garantirmos uma única instância de uma classe, a abordagem mais comum é criar um método estático que retorne sempre o mesmo objeto. Exemplo:

public class Singlegon {
    private static Singleton instance = new Singleton();
    public static Singleton getInstance() {
        return instance;
    }
    private Singleton() { }
}

O atributo estático instance armazena o objeto criado para retornar a cada chamada de getInstance().

O trecho private Singleton() { }; é um construtor privado, garantindo que nenhuma outra classe poderá criar inadvertidamente uma instância desta.

Postergando a criação do objeto

Tudo ok, mas nem sempre queremos criar o objeto no modo agressivo (eager), isto é, instanciá-lo logo que a classe é carregada. Em muitas situações, é desejável postergar a criação do objeto até a primeira chamada. Exemplo:

public class Singlegon {
    private static Singleton instance;
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

No código acima, o primeiro acesso ao método getInstance() irá disparar a criação do objeto, que será então retornada nas demais chamadas.

Problemas de sincronizção

O problema do código acima é que se houver mais de uma chamada concorrente no primeiro acesso ao método getInstance() ele pode criar duas instâncias de Teste. Duas threads poderiam entrar dentro do if, certo?

A solução mais básica para isso é sincronizar o método:

public static synchronized Singleton getInstance() {
    if (instance == null) {
        instance = new Singleton();
    }
    return instance;
}

O problema desta abordagem é que todas as chamadas estarão sujeitas a bloqueios, deixando a execução geral do programa mais lenta. Imagine um método assim num servidor de aplicação com vários usuários acessando o sistema! É terrível.

Uma solução melhor seria um bloco synchronized dentro do if, assim:

public static Singleton getInstance() {
    if (instance == null) {
        synchronized (Singleton.class) {
            instance = new Singleton();
        }
    }
    return instance;
}

Isso resolve o problema da sincronização em todos os acessos, mas é uma solução “ingênua”, pois na verdade voltamos ao problema inicial. Como o if não está sincronizado, duas threads diferentes podem entrar no bloco de criação ao mesmo tempo e, mesmo com a sincronização, elas retornarão instâncias diferentes quando instance == null.

Então, a solução mais “pura” para o singleton pattern seria acrescentar uma verificação dupla, assim:

public static Singleton getInstance() {
    if (instance == null) {
        synchronized (Singleton.class) {
            if (instance == null) {
                instance = new Singleton();
            }
        }
    }
    return instance;
}

Com esta última abordagem garantimos que não haverá perda de desempenho por causa de sincronização desnecessária do método inteiro.

Além disso, garantimos uma única instância de Teste, pois mesmo que duas chamadas concorrentes entrem dentro do primeiro if, temos uma nova verificação sincronizada.

No pior caso, se houver duas ou mais chamadas concorrentes no primeiro acesso a getInstance (quando INSTANCE ainda é null), apenas estas primeiras chamadas serão sincronizadas, sendo que após a primeira atribuição de INSTANCE, nenhuma chamada posterior será sincronizada.

Além do Singleton: padrão Registry

Alguns argumentam que o padrão Singleton está depreciado e deve ser abandonado. De certa forma eu concordo, pois em ambientes onde múltiplas threads e múltiplas aplicações executam concorrentemente, ter apenas um objeto quase nunca é desejável.

O padrão de projeto Registry permite armazenarmos uma coleção de objetos, cada um contendo um identificador ou um escopo específicos. É como se limitássemos o escopo do Singleton de “um objeto por programa” para “um objeto por qualquer escopo que quisermos“.

A implementação varia muito, mas podemos encontrar exemplos claros desse padrão em:

  • Threadlocal, que permite armazenar valores para uma thread, ou seja, um Singleton para cada uma.
  • HttpSession, que retorna sempre o mesmo objeto para cada usuário do sistema web, ou seja, um Singleton por usuário.
  • Frameworks de Injeção de Dependências como Spring ou CDI, os quais gerenciam a criação de objetos em diferentes escopos e permitem inclusive usar o padrão Singleton declarativamente.

Não entrarei em detalhes sobre o Registry neste artigo.

Indicações de Leitura

Uma leitura mais completa sobre o assunto está no Head First Design Patterns (Use a Cabeça! Padrões de Projeto). Embora tendo suas falhas, este é um livro muito bom para quem ainda está começando a entender Padrões de Projetos.

Outros detalhes interessantes sobre Singleton, como variações de implementação, podem ser encontrados na Wikipédia (em Inglês).

Uma breve definição do padrão Registry pode se encontra no catálogo de padrões do Martin Fowler.

O TDD está morto?

Test Driven Development (Desenvolvimento Orientado a Testes) é uma metodologia de desenvolvimento de software bastante popular, que é centrada (adivinhe) em testes.

No entanto, apesar de muito propagada, há várias críticas a essa metodologia por profissionais e pensadores de destaque.

Por esses dias rolou uma conversa bastante interessante sobre o assunto entre, nada mais nada menos, que Martin Fowler (que elaborou a ideia de Injeção de Dependência), Kent Beck (idealizador do TDD e do framework JUnit) e David Heinemeier Hansson (criador do Ruby on Rails).

Vamos analisar os pontos altos da conversa nos próximos tópicos. Mas antes, uma breve revisão sobre TDD…

Como o TDD funciona

Ao contrário do processo “tradicional” de desenvolvimento que envolve Especificação, Implementação e Testes, o TDD começa pelos testes em cima das interfaces do sistema.

A princípio todos os testes irão falhar. O objetivo do desenvolvedor é fazer com que eles sejam executados com sucesso. Em tese, isso garante o foco no que realmente deve ser feito e fornece uma medida mais real do progresso.

Os testes utilizados no TDD geralmente são Testes Unitários. Ambos os conceitos estão muito relacionados, mas não são sinônimos.

Testes Unitários

Testes Unitários são aqueles onde uma única função ou método do sistema é testada de cada vez. Em geral esses testes são automatizados.

Isso pode parecer simples, mas implica em que a rotina em questão não pode depender de outras rotinas, caso contrário o teste é sujeito a falhas de terceiros.

Dessa maneira, a fim de testar as rotinas que naturalmente possuem dependências de outras classes e métodos, de recursos como bancos de dados ou arquivos e outros, os desenvolvedores criam simulações desses elementos para que a rotina a ser testada funcione, porém não dependa diretamente desses outros elementos durante o teste.

Este é o conceito de mock, isto é, algo que imita o elemento original.

Por exemplo, um método que normalmente retorna um valor do banco de dados poderia ser substituído por um método que retorna um valor fixo num teste em particular. Cada linguagem, framework e ferramenta deve prover formas adequadas para se fazer isso, sendo inclusive uma forma de medir a qualidade da mesma.

Benefícios desta abordagem

Escrever os testes antecipadamente e então desenvolver com base neles traz várias vantagens:

  • Melhora o entendimento dos requisitos, já que para escrever o teste é necessário que o desenvolver entenda logo de início o que o sistema deve fazer.
  • Provê um objetivo claro a ser alcançado durante o desenvolvimento, isto é, fazer os testes executarem sem erros.
  • Aumenta a visibilidade do que está ou não pronto, já que os testes servem como indicadores mais confiáveis do que algo que está implementado e “só falta testar”.
  • A automação dos testes possibilita a Integração Contínua com certa garantia do funcionamento do sistema, já que todos os testes unitários podem ser reexecutados como testes de regressão sempre que necessário.

Desvantagens

Como regra geral, tudo que traz vantagens tem seus pontos negativos. Sempre temos que tomar cuidado com certos “evangelistas” que irão fazer uma tecnologia ou metodologia parecer não ter um lado negativo ou impacto zero.

Isso se aplica também ao TDD. Vejamos algumas considerações sobre ele:

Aumento do esforço

Será necessário gastar muito tempo para criar os testes e todos os mocks, além da infraestrutura de Integração Contínua. É claro que isso pode ser visto como um investimento em qualidade ao invés de uma pena a ser paga, mas na prática nem todos podem se dar a esse luxo.

Aumento da complexidade

A arquitetura acaba sempre mais complexa, usando muita Injeção de Dependências e outras técnicas de desacoplamento.

Mudanças nos requisitos são muito mais custosas, já que além do código do sistema em si, todos os testes e mocks envolvidos deverão ser reescritos.

Se o arquiteto de software não souber o que está fazendo, a arquitetura do sistema pode acabar cheia de gambiarras.

Intrusividade no código principal

Em muitas situações, será necessário programar de uma forma específica para que um certo código possa ser testado.

A arquitetura da aplicação muitas vezes acaba sendo então afetada por elementos e fatores que, de outra forma, não fariam parte de sua natureza.

Enfim, o design da aplicação acaba sendo influenciado pelos testes, o que nem sempre é desejável, já que é mais uma coisa a ser levada em conta e pode desviar o foco do arquiteto.

Testar tudo é quase impossível

Muitas tecnologias são difíceis de testar com testes unitários ou mesmo com testes de integração.

Pense na interface do usuário, por exemplo. Quantas delas possibilitam a execução da interface de forma “desacoplada”? Existem ferramentas como o Selenium para sistemas web, mas com elas também vêm diversas outras limitações e dependências.

O TDD está morto?

Vimos que todo benefício traz um custo associado. É este todo o ponto da discussão sobre o TDD estar morto. O custo de testar tudo e criar os mocks necessários para todos os testes vale a pena? Vejamos alguns comentários a seguir.

David Hansson inicia a argumentação dizendo que muitas pessoas não conseguem trabalhar com TDD porque não faz muito sentido para determinados tipos de trabalho. Ele toca no ponto da necessidade de se ter muitos mocks e que isso acaba tornando a codificação difícil.

Kent Beck aproveita o gancho e relata de casos onde a equipe não conseguia refatorar o código porque os testes estavam tão acoplados com a implementação, com dois ou três níveis de mocks, que qualquer alteração afetava inúmeros deles. Ele também afirma que há cenários onde o TDD se aplica melhor, por exemplo, quando os requisitos do software a ser desenvolvido são claros o suficiente para escrevê-los em testes diretamente. Por outro lado, há situações onde simplesmente o desenvolvedor vai descobrindo aos poucos aquilo que deve fazer. Isso é recorrente na criação de frameworks ou algoritmos que precisam analisar dados não estruturados.

Fowler argumenta ainda que a maior vantagem do TDD é um código que consegue testar a si mesmo. Só isso poderia também ser alcançado de outras formas. Ele também comenta sobre aqueles que dizem que “você não está fazendo teste unitário de verdade” porque existem algumas dependências durante o teste. Segundo ele, o importante não é a pureza de uma definição, mas que o teste funcione e seja útil, até porque há variações da definição de testes unitários.

Conclusões

É importante entender que TDD não é a “bala de prata” do desenvolvimento de software. Ele traz consigo vários desafios e problemas em potencial, principalmente se não há pessoas suficientemente experientes na equipe.

Testar é essencial, mas para cada projeto podem haver diferentes formas de testes, cujo busto-benefício deve ser avaliado para cada caso.

O teste não deve ser um objetivo em si mesmo, ainda que adotemos o TDD. Ele apenas tem o objetivo de garantir que estamos entregando o que o cliente pediu.

O tempo e o esforço que são investidos em testes também devem ser avaliado. Isso afeta diretamente a qualidade, mas a qualidade tem o seu preço. Quem irá pagar o preço da qualidade?

Quanto a testes unitários, não devemos buscar toda a pureza possível. O importante é que ele teste, ou seja, funcione para aquela situação específica.

Concluindo, o conselho em geral é: use TDD com moderação.


Para quem consegue entender Inglês, aqui vai o link do Hangout com essas grandes personalizados:

Is TDD dead?

Caminhos relativos, absolutos e outras rotinas de arquivos em Java

A classe File do Java encapsula de forma simplificada um arquivo ou diretório do sistema de arquivos local. O seu construtor pode receber caminhos absolutos ou relativos ao diretório atual do programa, por exemplo:

new File(".") //--> diretório atual do programa

O problema de usar caminhos relativos é que pode haver confusão em algumas situações, já que o diretório atual do programa pode ser modificado. Além disso, se o usuário pode digitar o caminho ou parte dele em algum campo, em geral deve-se evitar que ele use caminhos relativos, com exceção no caso de ser uma configuração do próprio programa.

Verificando se um caminho é relativo

O método File.isAbsolute() nos ajuda nessas tarefas e diz se o caminho é absoluto.

Veja um exemplo:

File f1 = new File("..");
System.out.println(f1.isAbsolute()); //imprime false

File f2 = new File("c:\\temp");
System.out.println(f2.isAbsolute()); //imprime true

Recuperando o caminho absoluto

Outro método útil é getAbsolutePath(). Ele retorna o caminho completo de uma instância da classe File.

Veja mais um exemplo:

File arquivo1 = new File("\\pasta\\arquivo.xml");
System.out.println(arquivo1.getAbsolutePath()); //imprime C:\pasta\arquivo.xml

File arquivo2 = new File("c:\\pasta\\arquivo.xml");
System.out.println(arquivo2.getAbsolutePath()); //imprime c:\pasta\arquivo.xml

Outras funcionalidades interessantes de File

A classe File possui vários métodos interessantes para situações específicas, por exemplo

  • getParentFile: retorna um File apontando para o diretório que contém o arquivo ou diretório atual.
  • getAbsoluteFile: retorna outra instância de File com o caminho absoluto.
  • toURI: retorna uma URI (Universal Resource Identifier) que começa com file:. É interessante para uso na rede.
  • isFile e idDirectory: informa se File aponta para um arquivo ou diretório, respectivamente.
  • exists: informa se o arquivo existe.
  • canRead e canWrite: informa se o arquivo pode ser lido ou gravado, respectivamente.
  • createNewFile: cria um novo arquivo em branco.
  • delete: apaga o arquivo ou diretório (se estiver vazio).
  • length: retorna o tamanho do arquivo em bytes.
  • list e listFiles: lista arquivos e diretórios, caso File seja um diretório.
  • mkdir e mkdirs: cria um diretório, caso File seja um diretório. O último também cria os diretórios “pais”, caso não existam.
  • getFreeSpace: retorna o espaço disponível na unidade para onde File está apontando.
  • createTempFile: método estático que retorna um arquivo temporário único para ser usado pelo programa. O método deleteOnExit faz com que esse arquivo seja apagado quando o programa Java terminar de executar.

Além dos métodos, a classe File possui algumas constantes (atributos estáticos) importantes para leitura e gravação de arquivos em diferentes plataformas:

  • File.separator: separador de nomes de diretórios. No Unix e Linux é /, enquanto no Windows é \.
  • File.pathSeparator: separador de vários caminhos de diretórios, para permitir criar uma lista de vários diretórios, como a variável PATH do sistema. No Unix e Linux é :, enquanto no Windows é ;.

Este artigo foi baseado na minha resposta no StackOverflow em Português!

Relative, absolute paths and other file methods in Java

The Java File class encapsulates the filesystem in a simplified manner. Its constructor accepts absolute and relative (to the working directory of the program) paths. For instance:

new File(".") //--> program's current directory

But using relative paths can cause trouble in some situations, since the working directory could be modified. Besides that, if there are input fields for file paths you should avoid relative paths unless in special cases like if the selected file is part of your program.

Checking if a path is relative

The File.isAbsolute() method gives us a hand in this task, telling if the path is absolute. Example:

File f1 = new File("..");
System.out.println(f1.isAbsolute()); //prints false

File f2 = new File("c:\\temp");
System.out.println(f2.isAbsolute()); //prints true

How to get the absolute path

Another useful method is getAbsolutePath(). It returns the absolute path to an instance of File.

Another example:

File f1 = new File("\\directory\\file.xml");
System.out.println(f1.getAbsolutePath()); //prints C:\directory\file.xml

File f2 = new File("c:\\directory\\file.xml");
System.out.println(f2.getAbsolutePath()); //prints c:\directory\file.xml

Other nice features of File

File contains various interesting features for specific use cases, such as:

  • getParentFile: returns a File pointing to the directory that contains the current file or directory.
  • getAbsoluteFile: returns another instance of File with an absolute path.
  • toURI: returns a URI (Universal Resource Identifier) that begins with file:. It’s useful to network operations.
  • isFile e idDirectory: tells if File points to a file or directory, respectively.
  • exists: tells if the file or directory really exists in filesystem.
  • canRead e canWrite: tells if you can read or write to the file, respectively.
  • createNewFile: creates a new blank file.
  • delete: removes the file or directory (if empty).
  • length: retuns the file size in bytes.
  • list e listFiles: lists files and directories, if File is a directory.
  • mkdir e mkdirs: creates a new directory, if File is a directory. The latter also creates parent directories if needed.
  • getFreeSpace: returns the available space in the device to where File is pointing to.
  • createTempFile: static method that returns an unique temporary file to be used by the application. The method deleteOnExit will delete the file at the termination of the program.

The class File also contains some important static attributes useful to read and write files in different platforms:

  • File.separator: path-name separator character. On Unix and Linux it is /, while on Windows it is \.
  • File.pathSeparator: path-separator character, in order to create a path list with various directories, like PATH system variable. On Unix and Linux it is :, while on Windows it is ;.

This article was based on my answer on StackOverflow in Portuguese!

Strings em Java: há mais detalhes do que você imagina

Quem estudou um pouco sobre Java sabe que Strings possuem algumas peculiaridades. Provavelmente o leitor já sabe que elas são imutáveis, já ouviu falar do pool de Strings e que deve-se usar o método equals() ao invés do operador == para comparar o conteúdo de variáveis.

Neste artigo quero ir um pouco mais além, entendendo como isso funciona internamente.

Brincando com o == e com o pool de Strings

O Java utiliza um mecanismo chamado String interning, colocando as Strings num pool para tentar armazenar apenas uma cópia de cada sequência de caracteres em memória. Em tese, o programa usaria mesmo memória e seria mais eficiente em decorrência dessa otimização.

Quando o Java encontra literais String no código, ele retorna sempre uma mesma instância de String, que aponta para uma entrada no pool interno da JVM. Sendo assim, é bem possível usar o operador == para comparar duas variáveis que recebem literais String:

String literal = "str";
String outraLiteral = "str";

System.out.println(literal == outraLiteral); //exibe true

Inclusive, como o Java trata literais String como instâncias é possível comparar um literal diretamente, assim:

System.out.println(literal == "str"); //também exibe true

Por outro lado, não podemos confiar no operador de comparação quando não sabemos como a String foi criada, já que é possível criar outras instâncias de várias formas. Exemplo:

String novaInstancia = new String("str");
System.out.println("str" == novaInstancia); //exibe false

O código acima cria uma nova instância de String, que não é a mesma retornada pela JVM para o literal "str".

Mas, contudo, entretanto, isso não quer dizer que temos duas entradas de "str" no pool do Java. Como podemos verificar isso? Usando o método String.intern(), que retorna uma referência para a String que está no pool. Exemplo:

String novaInstancia = new String("str");
System.out.println("str" == novaInstancia.intern()); //exibe true

Outro exemplo:

String str1 = "teste";
String str2 = "outro teste".substring(6);
System.out.println(str1 == str2.intern()); //exibe true

Tudo muito interessante. Mas, e se criássemos uma String de uma forma mirabolante?

StringBuilder sb = new StringBuilder();
sb.append('s');
sb.append('t');
sb.append('r');
System.out.println("str" == sb.toString().intern()); //continua sendo true

Até aqui aprendemos que uma instância da classe String não representa diretamente o seu conteúdo, isto é, o conjunto de caracteres. Várias instâncias de String podem coexistir com o mesmo texto. A questão é que todas apontam para a mesma entrada no pool.

Continue lendo, pois ainda não esgotamos este assunto!

Mas então pare que serve o equals()?

Com as informações do tópico anterior poderíamos chegar precipitadamente à conclusão de que é sempre melhor comparar duas Strings usando o operador == e o método intern().

O método equals() da classe String compara todos os caracteres de duas Strings para verificar a igualdade, enquanto o == apenas verifica se as duas Strings apontam para a mesma entrada do pool, uma comparação numérica infinitamente mais eficiente do ponto de vista computacional.

Já que a comparação com == é muito mais rápida do que com o método equals(), devemos abandonar o equals() e usar o intern() em todo lugar? A resposta é não.

A verdade é que nem todas as Strings são internalizadas no pool imediatamente. Quando chamamos o método intern(), se ela não estiver lá, então o Java irá acrescentá-la.

O problema é que, uma vez no pool, a String vai para a memória permanente e não será mais coletada pelo garbage collector.

Quando se quer velocidade e o conjunto de valores é relativamente pequeno, usar o método intern() pode ser vantajoso. Mas se usarmos este recurso, por exemplo, para processamento de arquivos texto, XML, bancos de dados, logo esbarraremos num OutOfMemoryError.

Além disso, adicionar uma Strings no pool também pode ser uma operação “cara”. Além de ser necessário verificar se a String já existe lá (envolve o método hashCode() e modificação de um mapa), o Java provavelmente terá que tratar acessos concorrentes (mais de uma thread pode inserir elementos no pool).

Finalmente, uma grande desvantagem é o código ficar mais propenso a bugs (error prone), já que é preciso que o desenvolvedor sempre coloque o intern() quando necessário.

Concluindo, o conhecimento sobre o pool ajuda em casos específicos para otimização “fina” do código, mas o uso deve ser moderado.

Outras formas de comparação

Indo um pouco além da comparação exata de Strings, temos outras formas interessantes de comparação:

Case insensitive (sem considerar maiúsculas e minúsculas)

System.out.println("STR".equalsIgnoreCase("str")); //retorna true

Uma string contida em outra

System.out.println("###STR###".contains("STR")); //retorna true

Qual string é “maior” que a outra?

System.out.println("str1".compareTo("str2")); //retorna -1, pois "str1" é menor que "str2"

Ou:

System.out.println("str1".compareToIgnoreCase("STR2")); //retorna -1, ignorando a capitalização

O método compareTo retorna:

  • 1 se a primeira String for maior que a segunda
  • 0 se forem iguais
  • -1 se a primeira String for menor que a segunda

Começa com…

System.out.println("str1".startsWith("str")); //returna true, pois "str1" começa com "str"

Termina com…

System.out.println("str1".endsWith("r1")); //return true, pois "str1" termina com "r1"

Expressão regular

System.out.println("str2".matches("\\w{3}\\d")); //return true, pois corresponde à expressão regular

Está vazia?

String str1 = "";
System.out.println(str1.isEmpty());
System.out.println(str1.length() == 0);
System.out.println(str1.equals(""));

Particularmente eu prefiro o primeiro método para Java >= 6 e o segundo para as versões anteriores.


Este artigo foi baseado na minha resposta no StackOverflow em Português!

Uma introdução ao Ant

554px-Apache-Ant-logo.svg

Ambientes de desenvolvimento Java, tanto em Linux quanto em Windows, precisam de algum tipo de automação para diminuir o tempo despendido pelo desenvolvedor, por exemplo, para gerar uma release (versão do sistema para distribuição ou homologação).

Uma das formas de automatizar tarefas uniformemente em todos os ambientes é usar uma ferramenta como o Apache Ant.

Ant é uma ferramenta poderosa e versátil que permite a criação de builds para compilação de código, montagem de pacotes, tratamento e conversão de arquivos e muito mais.

Não estamos falando de uma linguagem de programação. Ant é uma forma de declaração de atividades (tasks) necessárias em um determinado processo. Isso é feito através de um ou mais arquivos XML.

Instalando o Ant

Baixe o pacote binário na página de download oficial, descompacte-o numa pasta e adicione o caminho ao PATH do seu sistema operacional.

No Windows podemos fazer isso para uma sessão do prompt de comando da seguinte forma:

set path=%path%;c:\caminho\apache-ant-1.9.3\bin

Esta técnica é adequada se for necessário usar mais de uma versão do Ant. Mas o melhor é alterar o PATH diretamente nas configurações de sistema para sempre tê-lo disponível em linha de comando.

Escrevendo um build no Ant

O seguinte projeto Ant faz uma substituição usando expressões regulares em diversas linhas de um arquivo:

<project name="MeuProjeto" default="substituicao" basedir=".">
    <target name="substituicao">
        <replaceregexp
                file="${file}"
                byline="true"
                match="meu nome é (\w+)"
                replace="me chamo \1"
                flags="gs" />
    </target>
</project>

A tag <project> declara o projeto atual e seus atributos básicos. Ela deve ser a raiz do arquivo. O atributo default indica qual target será executado se nenhum outro for informado via linha de comando. O atributo basedir define o diretório base onde as tarefas serão executadas.

A tag <target>, por sua vez, declara um conjunto das atividades. Nesse caso, temos apenas a task <replaceregexp>.

Note o valor do atributo file da nossa task: ${file}. A cifra com as chaves de abertura e fechamento é algo similar à expression language do JSP, só que bem simplificada, tratando-se de uma interpolação de propriedades simples. O Ant substitui essa expressão por um valor definido anteriormente, de forma análoga a uma variável. Porém, não declaramos isso em lugar algum, então o valor terá que ser informado via linha de comando.

Executando o Projeto

Ao ser executado, o Ant procura automaticamente por um arquivo chamado build.xml no diretório atual. Então, se file.txt é um arquivo a ser processado pelo nosso build, o comando a seguir irá realizar a substituição:

ant -Dfile=file.txt

Caso o projeto Ant tenha outro nome, pode-se usar o parâmetro -f:

ant -f /caminho/meu-build.xml -Dfile=file.txt

O que o Ant pode fazer

O Ant possui muitas tasks prontas, dentre as quais posso destacar:

  • Javac: compila classes Java.
  • Sshexec: executa comandos remotos via SSH.
  • Copy: copia um ou mais arquivos, possibilitando filtrar o conteúdo e substituir trechos do mesmo.
  • Jar, War, Ear: empacota arquivos em uma dessas estruturas.

Além disso, o Ant possui alguns pontos de extensão. Por exemplo, você pode criar tasks personalizadas ou até seu próprio interpolador de variáveis.

Note que o Ant não é uma linguagem procedural e não tem comandos de controle. Entretanto, existe um projeto chamado Ant Contrib que disponibiliza tasks adicionais como If, For e TryCatch. Isso vai um pouco contra a filosofia do Ant, mas pode ajudar seus build a serem mais poderosos.

Outro projeto que estende o Ant chama-se Flaka. Ele acrescenta uma expression language muito mais poderosa que a original, estruturas condicionais, tratamento de exceções e muitas tasks.

Aprendendo mais sobre o Ant

Minha dica é: apenas leia o manual todo, começando pela seção Writing a simple Buildfile. Ele não é muito extenso e explica bem os conceitos.

Entenda bem os conceitos gerais antes de usar as extensões mencionadas no tópico anterior a fim de evitar surpresas.

Ainda vale a pena usar o Ant se há outras opções para build?

Sim e não.

Hoje temos o Maven, por exemplo, que gerencia o ciclo de vida de um projeto, da compilação à publicação, de forma padronizada. Porém, a arquitetura dos builds Maven também limita a execução de atividades arbitrárias que são necessárias em alguns projetos. Por isso, quem conhece Ant pode usar o Maven Antrun Plugin para executar tarefas personalizadas em qualquer fase do processo de build. É muito mais simples que criar e manter, por exemplo, um plugin próprio para o Maven.

Outra ideia é criar tasks independentes de projetos. Por exemplo, para automatizar um processo batch executado no servidor ou mesmo uma tarefa repetitiva no ambiente de desenvolvimento, como compilação de relatórios JasperReports.

Por outro lado, não fique preso a uma única ferramenta. Procure ter um conhecimento geral sobre ferramentas de build como Ant, Maven, Graddle, Ivy e outros semelhantes. Em geral, é melhor ter um canivete suíço do que um facão grande para fazer tudo. 😉


Este artigo foi baseado na minha resposta no StackOverflow em Português!

JavaScript: substituição em Strings

Substituir texto em Strings é algo muito comum em qualquer linguagem. Neste artigo, quero analisar qual seria a forma mais eficiente de fazer isso em JavaScript.

Dada a seguinte frase numa String:

var frase = "O céu está azul hoje!";

Qual é a melhor forma de trocarmos a cor do céu na frase acima para “verde”?

“O céu está verde hoje!”

Métodos de substituição

Após algumas pesquisas cheguei a quatro variações das técnicas para substituição de Strings. Elas fazem substituição de todas as ocorrências da String a ser substituída, caso haja mais de uma.

Criando uma expressão regular com RegExp

var regex = new RegExp("azul", "g");
var resultado = frase.replace(regex, "verde");

O RegExp é um tipo de objeto que recebe como parâmetro em seu construtor uma expressão regular e um modificador. A expressão usada foi a mais simples possível (“azul”). O modificador "g" significa global, isto é, a expressão irá afetar todas as ocorrências na frase, caso contrário, somente a primeira seria localizada.

Após criar a expressão regular, usei-a no método replace() da String. O primeiro argumento é a expressão usada para localizar partes do texto, as quais serão substituídas pelo conteúdo do segundo argumento (“verde”).

Criando uma expressão regular “nativa”

var regex  = /azul/g;
var resultado = frase.replace(regex, "verde");

Este código faz o mesmo que o anterior, mas escrevendo a expressão regular diretamente no código.

Usando as funções split e join

var resultado = frase.split('azul').join('verde');

Explicando o código acima, a função split() da String divide nossa frase em duas partes: uma com o conteúdo anterior da palavra “azul” e outra com o conteúdo posterior. O resultado é o vetor ["O céu está ", " hoje!"]. Depois disso, a função join() do array une os itens do vetor separando-os pela palavra “verde”.

Usando a função indexOf e substring

var pos = frase.indexOf('azul');
var ultpos = 0;
var resultado = '';
while (pos >= 0) {
    resultado += frase.substring(ultpos, pos) + 'verde';
    ultpos = pos + 4;
    pos = frase.indexOf('azul', pos + 4);
}
if (ultpos < frase.length) {
    resultado += frase.substring(ultpos, frase.length);
}

O código acima procura pela ocorrência da palavra “azul” na nossa frase e, enquanto houver alguma, vai montando uma outra String com a palavra substituída.

Fazendo o teste de desempenho

O primeiro passo para analisar a eficiência das soluções foi utilizar Jsperf. Este site permite a criação de casos de teste comparativos, com quantas variações forem necessárias. Eles podem ser executados por qualquer usuário, em qualquer navegador.

Clique aqui para ver ou executar os testes no jsperf!

Alguns testes foram realizados e o gráfico na data em que escrevo o artigo é o seguinte:

string-replace-performance

Nota: o item “regex2” se refere à expressão regular “nativa” mencionada anteriormente.

Cada novo usuário que executar os testes irá contribuir para a análise, então é provável que logo o gráfico no site Jsperf esteja diferente.

Análise dos resultados

Não temos uma conclusão! Não existe um consenso sobre o método mais rápido!

Em resumo:

  • O método com regex foi o melhor nas versões 7 e 10 do Internet Explorer
  • O método com split e join ganha no Google Chrome
  • E o método com indexOf e substring venceu no Firefox e no Ópera

Minha sugestão, baseada numa análise geral, é usar expressões regulares “nativas”, isto é, sem o RegExp. Note como a barra laranja está sempre em primeiro ou segundo lugar.


Este artigo foi baseado na minha resposta no StackOverflow em Português!

Quanto tempo gastar com testes?

software-testing

Ao planejar um projeto, quanto tempo deve ser reservado para os testes? Não seria suficiente calcular uma porcentagem em relação ao tempo estimado para desenvolvimento? É preciso mesmo planejar isso?

No silver bullets

Não existe um método mágico e correto para estimar o tempo gasto com testes, assim como não há uma solução mágica para o problema da estimação de software.

A verdade é que mesmo especialista em testes sugerem chutar! A princípio, use um fator mágico arbitrário para determinar o tempo reservado para testes. No decorrer do projeto, ajuste a proporção conforme sua produtividade e o nível de qualidade exigido.

Valores “mágicos”

Brooks, autor do famoso livro The Mythical Man-Month, descreve no capítulo 2 sua “regra” para planejar uma tarefa:

  • 30% para planejamento
  • 20% para codificação
  • 25% para testes de componentes e testes antecipados de sistema
  • 25% para testes de sistema e de integração geral

Os valores originais estão em frações, mas adaptei para porcentagem para facilitar o entendimento. Note que os testes ocupam metade do tempo de desenvolvimento (50%), o equivalente a 2,5 vezes o tempo de codificação.

Tenho notado em minhas atividades individuais que o tempo de testes varia de 1 a 1,5 vezes o tempo de desenvolvimento, incluindo testes unitários efetivos antes da codificação cobrindo cenários com valores limite e excepcionais. Além disso, é necessário mais 1 a 1,5 vezes o tempo de desenvolvimento para testes de integração após a codificação individual.

Em resumo, posso dizer que o tempo total de testes, quando feitos adequadamente, varia entre 2 a 3 vezes o tempo de desenvolvimento. Minha observação pessoal corresponde às observações de Brooks.

Pressa: e se não testarmos?

A dura realidade é que nem todos se dão ao luxo de separar tempo suficiente para testar. O termo “luxo” aqui foi usado com um pouco de ironia, porque a verdade é que, em última análise, isso é uma questão cultural da empresa e do indivíduo. Não, não tem tanto a ver com o cliente, porque a qualidade do sistema não é responsabilidade dele.

É responsabilidade dos profissionais de TI fazer o que for necessário para garantir o sucesso do projeto de software para o bem da própria empresa e do cliente, mesmo que as demais áreas não compreendam isso. Se a qualidade exigida não for financeiramente viável, então o projeto como um todo não está bem formulado ou simplesmente deve ser arquivado.

Tenho algumas considerações importantes que talvez possam ajudar:

  1. O tempo investido em testes unitários antes da codificação efetivamente diminui o tempo de codificação, já que o desenvolvedor tem que considerar cuidadosamente as entradas e saídas. De outra forma, as pessoas tendem a iniciar a codificação de um módulo do programa sem a noção do todo e precisa constantemente revisar o que já fizeram ao se deparar com novos detalhes e requisitos.

  2. Criar testes antes da codificação pode ajudar muito no entendimento do problema, já que o desenvolvedor precisa entender os requisitos para validar os resultados através de asserções.

  3. Em projetos “legados”, que não possuem testes unitários ou automatizados, o tempo gasto com correções tende ao infinito, pois a taxa de erros nunca estabiliza. Sem testes, não podemos mensurar corretamente o impacto das alterações e correções. A cada passo para frente, damos dois para trás. Uma correção pontual pode causar efeitos colaterais não esperados em outras funcionalidades.

  4. Uma premissa falsa que muitos tomam por verdadeira é que se as partes de um sistema forem implementadas corretamente, então não haverá problemas ao juntá-las no final. Isso é sempre um erro. Mesmo grandes desenvolvedores que sempre criam código de qualidade não podem evitar testes de integração, de sistema e de aceitação.

  5. Quanto mais tarde um erro for descoberto, maior o custo para corrigi-lo. Quanto antes os testes entrarem em ação, mais cedo eles podem contribuir para encontrar esses erros e problemas em potencial. Considere a imagem abaixo, extraída do artigo The Incredible Rate of Diminishing Returns of Fixing Software Bugs:

cost-fix-project

Um tempo bem gasto com testes permitirá aos desenvolvedores terem um horizonte visível do que é esperado do sistema.

Sobre estimação de software

Em minha pós-graduação, desenvolvi uma monografia sobre estimação de software. No momento em que escolhi este tema, acreditava que iria encontrar um método mágico para determinar o tempo das atividades de um projeto. Obviamente, logo que comecei a pesquisa percebi que havia acreditado num grande engodo.

Um dos livros mais interessantes de minha bibliografia foi Software Estimation: Demystifying the Black Art de Steve McConnell, cujo título já esclarece muito sobre a essência das estimativas: elas são tentativas de prever o futuro. Estimativas são chutes, simples assim.

A consequência disso é que não existe, e nunca existirá, uma regra definitiva para estimação das atividades de desenvolvimento de software. Na verdade, métodos “matemáticos” de estimação (COCOMO, Function Point) acabam confundindo seus usuários no sentido de que estes acreditam que o resultado terá acurácia garantida, afinal há todas aquelas fórmulas matemáticas complicadas. Aliás, é comum confundirmos acurácia (resultado bom) com precisão (nível de detalhe, casas decimais), mas uma estimativa pode ser muito precisa, porém longe da realidade.

Em decorrência disso, as metodologias ágeis não usam valores absolutos para estimar, como horas e dias, mas story points (pontos de história), que são grandezas relativas que variam de acordo com a equipe, o projeto e maturidade do desenvolvedor.

Os métodos mais modernos também não buscam precisão, isto é, estimar em muito detalhe (horas, por exemplo), já que frequentemente isso diminui a acurácia. Estimar em dias, semanas ou, em alguns casos, meses pode parecer “bruto” demais, porém os resultados são mais realistas. Não concorda? Então pense: quão confiável é uma estimativa de 370 dias e 4 horas? Sinceramente, alguém pode prever tudo o que ocorrerá em praticamente um ano?

Então, cuidado com soluções mágicas que podem tentar lhe vender. Embora algumas técnicas de estimação pareçam melhores, na verdade ninguém pode afirmar absolutamente que determinado método é melhor. Fazer isto seria o mesmo que afirmar que você tem um método para jogar na loteria melhor que outras pessoas, sendo o resultado simplesmente aleatório.

Leia algumas conclusões adicionais sobre estimação de software no artigo Reflexões sobre a natureza do software e das estimativas de software.

Mas isso significa que devemos sempre “chutar” durante o planejamento? Claro que não! Podemos chutar com estilo. 😉

Estimação por analogia

Estimar é prever o futuro. Porém se as atividades de desenvolvimento de um novo projeto são análogas a experiências anteriores, os envolvidos podem ter uma boa ideia do esforço necessário para executá-las.

Isso não acaba com os imprevistos, nem garante um cronograma em dia, mas resultados empíricos mostram que uma técnica adequada e a experiência do planejador contribuem para melhores estimativas.

Uma outra abordagem indicada pelos autores e especialistas em estimação é medir e armazenar a produtividade da equipe para então projetar os resultados futuros. Individualmente, este é um dos principais objetivos do PSP (Personal Software Process ou Processo de Software Pessoal). Um dos pilares do Scrum, a Inspeção, deve permitir acompanhar o progresso e a produtividade da equipe.

Embora a estimação, tanto dos testes quanto das demais atividades do desenvolvimento de software, seja uma tarefa mais de intuição do que um processo científico, em geral observa-se uma melhora na qualidade das estimativas com o uso sadio de dados históricos e analogia das atividades novas com as já executadas.

Uma aplicação prática disso para os teste é muito simples. Imagine um cenário onde você entregou a versão inicial do produto e irá partir para a segunda fase de um projeto de N fases. Se você mediu o tempo gasto com os testes na primeira fase, calcule o desvio com relação ao tempo planejado e aplique-o no planejamento da segunda fase. O processo deve ser repetido a cada fase do projeto.

Note que isso é compatível com o conceito do Cone da Incerteza, o qual afirma que as estimativas são melhores a cada fase do projeto. Considere a imagem abaixo, extraído deste artigo):

cone-da-incerteza

Na medida em que o projeto se desenrola, em tese, podemos analisar com mais certeza o horizonte de sua conclusão. No entanto, a curva não é a mesma para todo projeto e empresa. Os autores afirmam que a curva acima é o caso ótimo e se não houver gerenciamento adequado a incerteza continuará grande mesmo na data de término do projeto!

Estimação em faixas

Isso nos leva a outro conceito: estimar em faixas de valores. Podemos usar esta técnica para estimar o melhor e o pior caso para uma determinada atividade. A diferença entre esses casos corresponde à incerteza atual.

Tenho usado faixas de valores para trabalhos como consultor independente e tem funcionado bem, principalmente em atividades que envolvem algo novo para mim, portanto não sei ao certo quanto tempo será necessário de pesquisa, desenvolvimento e testes.

Num exemplo simples, posso responder que a atividade poderá levar de 8 a 12 horas. Se tudo correr bem, pode ser até até menos. Se tudo der errado, talvez eu tenha que negociar horas a mais. Na maioria das vezes consigo acertar e cobro algo dentro da faixa.

Isso também permite ajustes relacionados a reuniões e telefonemas. Se em um projeto passei duas horas no telefone com o cliente, esse tempo é incluído no tempo da atividade. Se o cliente especificou bem a tarefa e não foi necessário muito tempo de análise, bom para ele, vai pagar menos.

É claro que isso envolve uma relação de confiança por parte dos clientes, mas a ética do profissional cedo ou tarde torna-se patente.

Estimativa ou compromisso?

Outro erro comum no dia-a-dia é confundirmos estimativas com compromisso por parte dos desenvolvedores.

Imagine que uma empresa estima os testes com uma regra mágica de 50% do tempo de desenvolvimento. Os desenvolvedores percebem que estão gastando muito mais do que isso.

Uma reação comum para não “atrasar o cronograma” é simplesmente escrever menos testes do que o planejado, como se a estimativa inicial fosse uma obrigação a ser seguida.

O correto seria revisar a estimativa inicial e não tentar se ajustar a ela. O problema é que na prática…

Frequentemente falta aos gerentes de software firmeza de fazer o pessoal esperar por um bom produto (Brooks)

A qualidade é um fator determinante

Usei a citação acima em um artigo sobre o Triângulo de Ferro que escrevi há algum tempo. Este conceito é importante pois demonstra que a qualidade possui uma relação direta e proporcional com o tempo despendido no projeto.

Isso implica em afirmar que mais qualidade exige mais tempo. Por isso, a decisão de investir em mais ou menos testes no início do projeto influenciará diretamente na qualidade final do produto.

Diminuindo o tempo despendido com testes sem prejudicar a qualidade

O título parece contradizer o que acabei de dizer. Mas, se tomarmos o conceito de separação entre atividades essenciais e acidentais do desenvolvimento como faz Brooks em No Silver Bullets, podemos dizer que, embora não haja como evitar os testes sem diminuir a qualidade, podemos diminuir as dificuldades acidentais da criação deles.

Isso pode ser alcançado de algumas formas:

  • Treinando a equipe para melhorar a produtividade
  • Usando ferramentas mais adequadas que facilitem a criação e execução dos testes
  • Investindo na automação
  • Usando tecnologias (frameworks, plataformas) que facilitem os testes

Enfim, podemos ser mais produtivos fazendo mais testes e menos trabalho repetitivo manual.


Este artigo foi baseado na minha resposta no StackOverflow em Português!

Entenda como Zebrar uma tabela com CSS

Colorir alternadamente as linhas de uma tabela é um requisito comum. A abordagem mais comum é usar um código personalizado na view para adicionar estilos alternados. No entanto, é possível fazer isso sem o uso de uma linguagem de back-end como Java, PHP ou .Net.

CSS 3

Podemos usar estilos CSS selecionando os elementos pares e ímpares, como no seguinte exemplo:

/* linhas pares (even) */
.tabela tbody tr:nth-child(even) {
    background-color: #CCC;
}
/* linhas ímpares (odd) */
.tabela tbody tr:nth-child(odd) {
    background-color: #FFF;
}

Continuando com o exemplo, agora só precisamos adicionar a classe tabela ao elemento <table> no HTML:

<table class="tabela">
    <thead>
        ....
    </thead>
    <tbody>
        ....
    </tbody>
</table>

Veja um exemplo funcional no Jsfiddle!

Que bruxaria é essa?

Para quem não conhece a sintaxe do CSS, ou nem sabe que tipo de tecnologia é essa, trata-se de um tipo de linguagem para aplicar primariamente estilos visuais nos elementos de uma página web.

Considere o seguinte exemplo:

seletor {
    atributo1: valor1;
    atributo2: valor2;
}

Esta é a estrutura de uma regra (rule) do CSS. Regras são compostas pelas seguintes partes:

  • Seletor: define quais elementos serão afetados pela regra.
  • Atributo: especifica qual atributo será afetado. Podem haver vários atributos por regra.
  • Valor: o respectivo valor de cada atributo.

Um seletor que começa com um ponto (.), como em .tabela, chama-se seletor de classes. Ele diz ao navegador para aplicar a regra aos elementos que possuem o atributo class com a respectiva classe, como em class="tabela". Um elemento pode conter várias classes, cujos nomes devem ser separados por espaços em branco, como em class="tabela outra-classe".

Voltemos agora ao exemplo do tópico anterior. O seletor.tabela tbody tr:nth-child(even) primeiramente seleciona elementos que contém o atributo class="tabela".

Em seguida, encontramos o trecho tbody. Este é um seletor de tag, isto é, ele seleciona as tags <tbody>, que define o corpo da tabela, de forma que não zebramos o título da mesma. Como isso vem depois de .tabela e é separado por um espaço em branco, incluiremos todos os elementos <tbody> filhos do elemento com class="tabela". O próximo trecho é tr, que seleciona todos os elementos <tr> filhos do elemento <tbody>. Note que, se houver uma tabela dentro de outra, as linhas da tabela mais interna também serão afetadas. Se quiséssemos especificar a seleção apenas dos filhos diretos, poderíamos usar o caractere maior (>), como em .tabela > tbody > tr.

O seletor tr é seguido de um caractere de dois pontos (:). Este é um pseudo-seletor. Ele altera o seletor anterior. Nesse caso o pseudo-seletor nth-child() permite especificar quais elementos do conjunto total de tags <tr> serão realmente incluídos. Dentro do parêntesis, poderíamos especificar um índice numérico. Por exemplo, tr:nth-child(5) iria selecionar apenas a quinta linha da tabela. Porém, usamos os valores especiais odd e even para definir índices ímpares e pares, respectivamente.

Finalmente, aplicamos a cor #CCC (um cinza claro) ao atributo background-color (cor de fundo) às linhas pares. Depois, aplicamos a cor #FFF (branco, em hexadecimal) como cor de fundo das linhas ímpares. Note que o valor #FFF é uma abreviação para #FFFFFF. Na versão com 6 letras, cada dupla de bytes representam uma cor do RGB (Red, Green, Blue).

Ufa! Entendeu? 😉

Se você lê Inglês e quer se aprofundar, recomendo a referência da fundação Mozilla.

Compatibilidade com navegadores antigos

A solução em CSS é muito legal, mas o seletor nth-child não vai funcionar no Internet Explorer 6, 7 e 8. Se precisar manter a compatibilidade com essas versões do navegador, uma alternativa é usar jQuery. O jQuery simula seletores mais novos mesmo em navegadores antigos através de código Javascript.

O seguinte trecho de código aplica a coloração em linhas ímpares e pares logo após o carregamento da página:

$(document).ready(function() {
    //linhas pares, iniciando em zero
    $('.tabela tbody tr:even').css('background-color', '#FFF'); 
    //linhas ímpares iniciando em zero
    $('.tabela tbody tr:odd').css('background-color', '#CCC'); 
});

Note que inverti odd e even. Isso é porque a versão CSS do seletor usa índices baseados em 1 (1, 2, 3, …, N), enquanto a versão jQuery usa índices de vetores Javascript, que são baseados em 0 (0, 1, 2, …, N – 1).

Veja o exemplo funcional da versão em Javascript no Jsfiddle!


Este artigo foi baseado na minha resposta no StackOverflow em Português!

Concorrência e objetos thread-safe

1a

Certamente você já concorreu com outras pessoas em sua vida. Uma vaga na faculdade, um emprego ou mesmo a fila do banco.

Sistemas de software também enfrentam certas restrições e competem por recursos computacionais. Na verdade, cada vez que você acessa um site está concorrendo, de alguma forma, com centenas ou milhares de pessoas. Um dos maiores desafios das empresas que disponibilizam serviços na Internet é atender a todos os acessos simultâneos.

Como um engenheiro de software, pretendo agora fazer uma breve análise microscópica sobre esse problema.

O que pode dar errado?

Em geral, um sistema que visa atender vários usuários ao mesmo tempo irá processar as solicitações paralelamente em vários processos ou threads. O problema começa quando as ações de dois ou mais usuários afetam o mesmo conjunto de dados.

Vamos supor que temos um objeto com um contador compartilhado entre diversas threads:

int contador = 0;
public int incrementa() {
    contador++;
    return contador;
}

Suponha ainda que duas threads T1 e T2 estão rodando em dois processadores de uma mesma CPU. Em um dado momento elas chamam, ao mesmo tempo, o método incrementa() e ocorre a seguinte sequência de execução:

  1. Processo T1 executa a linha contador++ (total = 1)
  2. Processo T2 executa a linha contador++ (total = 2)
  3. Processo T2 retorna o valor de total, ou seja, 2
  4. Processo T1 retorna o valor de total, ou seja, 2

Obviamente isso irá causar um efeito colateral indesejado! 🙁

Note ainda que problemas podem ocorrer mesmo que apenas uma das threads faça modificações e as demais estejam lendo valores, já que elas podem ler dados desatualizados e incorrer em exceções.

Um exemplo básico seria a iteração sobre uma lista. Se uma thread remove um item da lista, outras threads que estão no meio de uma iteração podem acabar em uma exceção ArrayIndexOutOfBoundsException.

Os exemplos são inesgotáveis e os efeitos colaterais os mais bizarros!

Objetos thread-safe

Para resolvermos a situação acima, onde um objeto é acessado por múltiplas threads, precisamos construir um objeto que seja à prova de acesso simultâneo, ou seja, um objeto thread-safe. O que diacho é isso? É um objeto que, em dado contexto de uso, garante o acesso seguro a dados compartilhados por várias threads de forma concorrente sem efeitos colaterais indesejados.

Bem, na verdade, essa é metade da história. O ideal do ponto de vista de desempenho seria que não houvessem acessos concorrentes. Isso significa que, num mundo perfeito, nós conseguiríamos distribuir as tarefas igualmente entre as várias threads e cada uma poderia concluir seu trabalho independentemente, sem uso de recursos compartilhados.

Na prática, é comum precisarmos compartilhar dados, então temos que aplicar um conceito muito popular da vida real: o semáforo.

Em nossas ruas, um semáforo é o que garante que somente os veículos de uma via por vez tenham o direito de atravessar um cruzamento. Em computação, um semáforo nada mais é do que uma variável ou objeto que controla o acesso a um determinado recurso. Com isso, estamos fazendo sincronização do acesso de várias threads, analogamente à sincronização dos vários semáforos de um cruzamento.

Em Java, podemos sincronizar o acesso a um método ou bloco de código. Veja o exemplo:

int contador = 0;
public synchronized int incrementa() {
    contador++;
    return contador;
}

Neste novo exemplo, as duas threads não iriam executar o método incrementa() ao mesmo tempo e no mesmo objeto. Somente uma de cada vez adentraria o bloco com o incremento, evitando o efeito colateral descrito no tópico anterior.

Não se engane! (Ressalvas importantes)

É importante notar que a sincronização no método é equivalente a um bloco synchronized(this), o que significa que o bloqueio é feito na instância do objeto que contém o método. Se houverem duas instâncias, não haverá sincronização.

Um erro muito comum é o desenvolvedor achar que o synchronized irá resolver os problemas de concorrência dos sistemas. Isso não é verdade.

Alguns frameworks web, por exemplo, podem criar várias instâncias das classes para tratar requisições, dependendo de seu escopo, então a sincronização não iria ocorrer como esperado. Além disso, aplicações distribuídas em clusters (dois ou mais servidores) também não seriam sincronizadas mesmo usando singletons.

Mesmo que synchronized fizesse o que alguns esperam, ainda incorreríamos em um grande problema.

Gargalos!

gargalo-e1353923696197

A sincronização resolve alguns problemas, mas pode gerar outros. Se houverem muitos métodos sincronizados, gargalos começarão a surgir no sistema.

Gargalos são pontos do sistema que geram lentidão, pois várias threads precisam ficar esperando sua vez. Quanto mais sincronização fizermos, mais gargalos.

Sincronizando somente o necessário

Para resolver isso, podemos usar uma forma mais adequada de sincronização. No exemplo anterior, usamos um semáforo “global”, isto é, o próprio objeto. Isso faz com que todo acesso ao objeto seja sincronizado por um único semáforo!

A solução para isso é usar semáforos mais específicos de forma que sincronizemos somente o que é necessário. Veja o exemplo:

Integer contador = 0;
Object semaforo = new Object();
public Integer incrementa() {
    synchronized (semaforo) {
        contador++;
        return contador;
    }
}

Este simples exemplo não traz muitas vantagens em si mesmo. Mas considere um cenário onde existam outros métodos nesse objeto que não modificam a mesma variável. Poderíamos criar um semáforo para cada variável, limitando ao máximo o escopo da sincronização.

A boa prática diz que o bloqueio deve ser referente aos dados acessados e não ao objeto que os contém. Isso evita muitos bloqueios desnecessários e pode fazer toda a diferença em cenários um pouco mais complexos.

E se não houver modificações?

Objetos imutáveis são, por natureza, seguros para uso em múltiplas threads, pois não há risco de efeitos colaterais. Eles não precisam ser sincronizados para serem seguramente usados em multithreading e podem ajudar muito na melhoria do desempenho da aplicação.

Este é um dos motivos pelos quais as classes básicas do Java, como String e Integer, são imutáveis. Observe que eu estou falando que não é possível modificar o valor de uma instância dessas classes, não que você não pode atribuir outra instância a uma variável.

Então nunca haverá problemas com objetos imutáveis?

Sim e não! Não é por acaso que a definição de thread-safe fala sobre o contexto de uso.

Apenas para citar um exemplo, suponha que um mapa imutável contenha elementos do tipo ArrayList mutáveis. Se threads diferentes acessarem e modificarem essas listas, estaremos sujeitos novamente a efeitos colaterais indesejados. Então, mesmo uma coleção imutável ou adequadamente sincronizada não cobre todos os casos, pois se ela contém um elemento mutável, ainda que seja um simples POJO, o resultado final não será thread-safe.

É preciso também tomar cuidado com classes do Java que, apesar de aparentarem inocência, na realidade são inerentemente thread-unsafe. Um exemplo é classe SimpleDateFormat, que estende a classe DateFormat, a qual possui um atributo do tipo Calendar, que é mutável. Aliás, ter atributos mutáveis como o Calendar é uma fonte de problemas em potencial.

Em resumo, devemos analisar todos os objetos compartilhados por threads, incluindo seu conteúdo.

Um pouquinho da API do Java

Para tentar ajudar, a API Collections do Java nos mune com várias implementações imutáveis e thread-safe para todos os gostos.

As versões sincronizadas de classes de coleções são encontradas no pacote java.util.concurrent. Eis alguns substitutos para as classes convencionais:

A classe ConcurrentHashMap é uma versão melhorada de mapa sincronizado em relação ao HashTable, suportando manipulação por várias threads, mas sem bloquear as operações de leitura.

O método utilitário Collections.synchronizedMap(map) retorna um wrapper sincronizado de um mapa qualquer. Entretanto, mesmo métodos de leitura são sincronizados, resultando em mais gargalos com relação ao ConcurrentHashMap.

Alguns dos wrappers sincronizados disponíveis são:

public static <T> Collection<T> synchronizedCollection(Collection<T> c);
public static <T> Set<T> synchronizedSet(Set<T> s);
public static <T> List<T> synchronizedList(List<T> list);
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m);
public static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s);
public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m);

Além disso, também existe uma lista de wrappers imutáveis, isto é, que retornam uma versão imutável do objeto:

public static <T> Collection<T> unmodifiableCollection(Collection<? extends T> c);
public static <T> Set<T> unmodifiableSet(Set<? extends T> s);
public static <T> List<T> unmodifiableList(List<? extends T> list);
public static <K,V> Map<K, V> unmodifiableMap(Map<? extends K, ? extends V> m);
public static <T> SortedSet<T> unmodifiableSortedSet(SortedSet<? extends T> s);
public static <K,V> SortedMap<K, V> unmodifiableSortedMap(SortedMap<K, ? extends V> m);

A referência oficial dos métodos que retornam wrappers imutáveis e sincronizados está aqui.


Este artigo foi baseado na minha resposta no StackOverflow em Português!

Página 4 de 8

Creative Commons O blog State of the Art de Luiz Ricardo é licenciado sob uma Licença Creative Commons. Copie, compartihe e modifique, apenas cite a fonte.