Arquivo

Arquivo do autor

Usando AspectJ para injetar comportamento em um Enum

29, abril, 2010

Nota: Isso pode não ser viável na maioria dos casos, mas acho interessante pelo fator curiosidade.

Já me deparei algumas vezes em uma situação onde eu tinha uma série de enums que implementavam uma interface. Enums em Java (ainda) não podem herdar métodos, o que faz com que os métodos devam ser implementados em todos os enums, mesmo que o que a gente queira seja a mesma implementação pra todos os casos.

Um exemplo disso é o seguinte: para internacionalizar uma aplicação, por motivos que não vem ao caso, decidimos encapsular as mensagens em enums. Ou seja, teríamos uma interface Message, que todos os enums vão implementar, e cada contexto da aplicação teria seu próprio enum. Declaramos dois métodos em uma mensagem: um get(), que retorna a mensagem diretamente; um get(Object… args), que usa um MessageFormat para colocar os valores dos argumentos dentro da mensagem.

public interface Message {
    String get();
    String get(Object... args);
}

O método para recuperar as strings é o mesmo para todos os enums. Um ResourceBundle (que contém as mensagens carregadas de um arquivo properties) é acessado, e o valor correspondente ao valor do enum é retornado. Um enum típico seria assim:

public enum PersistenceMessage implements Message {
    OPEN("persistence.open"),
    SAVE("persistence.save");

    private String key;

    private PersistenceMessage(String key) {
        this.key = key;
    }

    @Override
    public String get() {
        return Messages.get(key);
    }

    @Override
    public String get(Object... args) {
        return Messages.get(key, args);
    }
}

O problema aqui é que os métodos em todos os enums vão estar duplicados.

Como seria bom se existisse um método de injetar comportamento nestes enums… talvez como os mixins do Ruby

Acontece que em Java existe um jeito de fazer algo parecido com os mixins de Ruby. Com técnicas de AOP (aspect oriented programming), mas especificamente com ajuda do framework AspectJ, é possível injentar comportamento em classes com as chamados inter-type declarations.

Eu não vou explicar aqui o que é AOP ou como funciona o AspectJ, mas abaixo vai o exemplo de como fazer isso.

O primeiro passo é criar um aspecto para a interface Message, fornecendo os métodos desejados.

public aspect MessageGetter {

    public String Message.get() {
        return Messages.getString(this.getKey());
    }

    public String Message.get(Object... args) {
        return Messages.getString(this.getKey(), args);
    }
}

Como vocês devem ter percebido, eu adicionei na interface de Message o método getKey(), para ter um ponto de acesso à chave. Eu nunca usei AspectJ na prática, então talvez exista uma forma mais elegante de fazer isso. Essa foi a única alteração que fiz na interface, então não vou mostrá-la novamente.

Cada enum agora segue o seguinte padrão:

public enum PersistenceMessage implements Message  {
    OPEN("persistence.open"),
    SAVE("persistence.save");

    private String key;

    private PersistenceMessage(String key) {
        this.key = key;
    }

    @Override
    public String getKey() {
        return key;
    }
}

O método getKey() passou a ser duplicado, mas acredito que seja muito menos grave do que a situação anterior.

Este foi um exemplo extremamente simples, e com certeza não é uma opção para todos depender do AspectJ só para esse comportamento em um projeto, mas acredito que só o fato de ter o conceito de mixins na cabeça seja útil para abrir alguns horizontes na hora de bolar a arquitetura de uma aplicação.

Renato Besen Arquitetura de Software

Heurísticas de Usabilidade aplicadas ao Código

24, março, 2010

Nos dias 12 e 13 de Março a OnCast promoveu um curso de teste de usabilidade, com o Ezequiel Blasco. O que mais me chamou a atenção no curso foram algumas heurísticas para avaliar a usabilidade de um sistema.

Como não trabalho diretamente com design de interfaces, resolvi refletir e aplicar as heurísticas no que interessa para um desenvolvedor de software.

  1. Visibilidade do estado do sistema - quando lemos um código fonte, é importante que saibamos o que está acontecendo. Alguns exemplos de coisas que podem ferir esta heurística: métodos onde o nível da abstração é muito variado (e.g. manipulação de uma string e acesso ao servidor no mesmo método); magia negra, ou seja, pequenos truques que parecem funcionar mas ninguém sabe o porquê;  muitas suposições, o programador pode considerar certas coisas implicitamente, mas isso pode não estar claro para todos; variáveis/classes/pacotes com nomes não auto-explicativos, e etc.
    Em resumo, eu considero sempre o bom conselho da PEP 20 que diz “Explicit is better than implicit“. Se alguma coisa prejudica a visibilidade do sistema, ela deve ser bem documentada e justificada. Se muitas dessas coisas acontecem em um mesmo software, está na hora de considerar uma nova arquitetura para ele.
  2. Simetria entre sistema e mundo real - este ponto se refere as metáforas e modelagem utilizadas em um software. Se espera que um software reflita pelo menos superficialmente o domínio, o que dá origem a técnicas como o DDD. Considere por exemplo um software de gerenciamento de projetos Scrum onde não se tem um conceito de Sprint. Alguma coisa deve estar errada, não?
  3. Controle e liberdade do usuário - Se uma classe é muito fechada, a reutilização dela é prejudicada. Obviamente é importante ter um balanço entre nenhuma configuração e muita configuração. É interessante fazer com que o sistema tenha um fallback para uma configuração padrão, mas devemos tomar cuidado para não limitar demais as coisas, pois isso gera duplicação de código.
  4. Consistência e padrões - Se uns métodos tem nomes em cammelCase e outros separados com underscore, nunca se sabe o que esperar do sistema. Não ter um padrão de código e arquitetura induz o programador a erros, já que o nosso cérebro é basicamente uma máquina de identificar padrões.
  5. Prevenção de erros - existem aqui duas possibilidades. Primeiro, se o erro pode tratado, faça, mas não esqueça da heurística 1: o erro deve ser comunicado, bem como a sua solução. Caso não seja possível tratar, quebre cedo. Não existe nada mais frustrante do que entrar no fluxo de trabalho, e descobrir que ele não vai poder ser concluído por que ocorreu um erro aconteceu lá no início.
  6. Reconhecimento ao invés de lembrança - esta heurística se refere a intuitividade do código. Um bom código parece natural de ser utilizado, nada é forçado, e você é capaz de utilizar mesmo se ficar 3 meses sem encostar nele. Se toda vez que você precisa usar algo (uma biblioteca, um framework, etc.) é necessário ler o tutorial/ajuda, bem… é um mau sinal.
  7. Flexibilidade e eficiência de uso - para exemplificar esta heurística, considere o framework Ruby on Rails. Ele é extremamente eficiente de se usar, pois otimiza a codificação para os casos mais comuns. Digamos que 80% das pessoas vai utilizar o sistema de template X, com o banco de dados Y e o framework de teste Z. Nesse cenário, é muito mais prático ter estes três componentes pré-configurados, e o 20% que precisar utilizar algo diferente mergulha mais fundo no sistema, e faz as alterações necessárias. Assim, além de eficiente, o framework é flexível.
  8. Estética e design minimalista - esta heurística vai ao encontro da filosofia Unix de desenvolvimento, que diz: “faça apenas uma coisa, e faça bem”. Outro conceito que descreve a heurística é o “Single Responsibility Principle“.
  9. Recuperação de erros - o programador deve ser capaz de identificar qual o erro e ter informações para poder tratar o mesmo. Sair do programa com uma mensagem “exit with code -1” não é muito útil para ninguém.
  10. Ajuda  e documentação - este talvez seja um ponto polêmico, mas depende muito de situação para situação. Muitos consideram que documentação de código é igual a código duplicado. Bob Martin no livro Clean Code sugere que devemos evitar o mínimo de mistura de linguagens em um arquivo (e.g. um arquivo com Java, Html, JavaScript e Inglês é terrível de manter, pois temos que mudar a sintaxe em que o cérebro está trabalhando em um curto espaço de tempo). Por outro lado, se estamos criando uma biblioteca que vai ser usada por terceiros, é importante ter a documentação dos métodos com exemplos de uso, edge cases, etc. Só não podemos esquecer que a documentação possui um custo para ser mantida, assim como o código.

Lembrando que essas são apenas as minhas reflexões sobre o assunto, e assim como no design de interfaces são apenas heurísticas, ou seja, não necessariamente um código precisa atender a todas elas para ser bom. A medida dessas heurísticas é fuzzy, e temos que encontrar um balanço para entregar o software funcionando, que afinal é o objetivo primordial de todos os desenvolvedores.

Eu gostaria muito de conhecer a ótica de outras pessoas sobre os pontos aqui tratados, portanto sintam-se livras para colocar suas opiniões nos comentários.

Renato Besen Arquitetura de Software

A Importância da Mudança

18, fevereiro, 2010

No segundo grau todos aprendemos sobre o conceito de “entropia” na física e química. Entropia neste contexto está associada com a capacidade de um sistema mudar espontaneamente de estado. Quanto menor a entropia de um sistema, mais chance o mesmo tem de mudar; quanto maior a entropia, menos chances de mudar, ou seja, o sistema é mais estável.

Um fato importante sobre a entropia é que, em um sistema isolado, ela nunca decresce. Isso significa que o sistema sempre tende para o estado mais estável. Para exemplificar, considere uma mistura de água e açúcar: o açúcar tende a se dissolver na água (uma mudança espontânea), mas o contrario é improvável de acontecer, pois a entropia do sistema já dissolvido é maior do que a entropia inicial.

Mas o que isso tudo tem a ver com desenvolvimento de software? Encarando o software desenvolvido como um sistema físico, a conclusão lógica é que, se não houver um gasto de energia para manter a entropia baixa, o sistema tende à estagnação. Quanto maior a entropia do software, mais difícil ter mudanças no mesmo.

Aproximando a idéia de entropia para a metodologia Scrum, podemos fazer uma associação com as máquinas de movimento perpétuo. A idéia da máquina é que ela gere uma quantidade de energia igual ou maior do que a energia necessária para a mesma funcionar, formando assim um ciclo de retroalimentação infinito. Ora, como a entropia de um sistema isolado sempre cresce, uma máquina com este funcionamento é teoricamente impossível. E é por isso que no ciclo do Scrum existe o conceito de melhoria continua.

A melhoria continua representa uma injeção externa de energia que torna possível a existência de um processo como o Scrum. Essa energia é necessária para que a equipe de desenvolvimento consiga se adaptar e abraçar as mudanças (quem trabalha com desenvolvimento sabe que essas mudanças acontecem mais do que muitos desejariam).

Para deixar claro, imagine uma equipe X já estabelecida (com conhecimento no domínio e na tecnologia) que quer aumentar a velocidade de desenvolvimento. A velocidade sempre tende a se estabilizar; as pessoas não vão programar mais rápido por que um cliente pediu, ou por que um gerente fez uma ameaça. Se a velocidade mudou, alguma outra variável mudou no sistema. O exemplo mais óbvio é a perda de qualidade do código: testes deixam de ser escritos, a comunicação com o cliente é prejudicada (desenvolvedores fazem mais suposições sobre o domínio do que deveriam). Outras possibilidades são a mudança da tecnologia usada (frameworks, ou mesmo a linguagem), melhoria no conhecimento dos desenvolvedores, ou mudanças relacionadas ao processo de desenvolvimento.

As mudanças no processo são o que fazem o Scrum funcionar para equipes tão heterogêneas (e que geram reclamações relacionadas às certificações). O Scrum deve ser modificado para se adaptar as equipes, e não só no início de um projeto, mas durante cada iteração. O projeto deve servir como um laboratório: quando a equipe se sentir confiante, mudanças no processo devem ser inseridas (de preferência uma de cada vez) para gerar oscilações na entropia. Como os sprints no Scrum são relativamente pequenos, se a mudança for negativa ela pode ser rapidamente revertida. Se a mudança for positiva, ótimo! Em ambos os casos, a mudança serviu como experiência para a equipe, e ela agora tem mais conhecimento sobre o processo e sobre as suas próprias limitações. No pior dos casos, a mudança serve para que os membros da equipe não fiquem acomodados.

Para identificar possíveis mudanças pode-se usar as retrospectivas, identificando pontos fracos a serem remediados e também os pontos fortes a serem reforçados. Retrospectivas, porém, são um assunto para outro post.

Renato Besen Auto-gestão, Metáforas

Sobre DTOs e programação estruturada

7, janeiro, 2010

Com a minha própria autorização, replico aqui um post do meu blog:

Os DTOs (Data Transfer Object) tem como principal uso a transferência de dados entre diferentes camadas de uma aplicação. A principal característica desse tipo de objeto é falta de comportamento.

O problema: DTOs as vezes são usados além do seu propósito inicial, o que leva o desenvolvedor à programação estruturada. Considere por exemplo os seguintes exemplos, primeiro em Java:

public class User {
  private String login;
  private String passwortHash;

  [getters e setters]
}

public class UserUtil {
  public static boolean checkLogin(User user) {
    ...
  }
} 

Agora em C:

struct User {
  char* login;
  char* passwordHash;
};

int checkLogin(User user) {
...
} 

Nota: não compilei os códigos, mas são suficientes para objetivos didáticos.

Perceberam a semelhança? Apesar da keyword “class” estar presente na versão Java, isso NÃO é orientação a objetos.

DTOs têm usos justificados, porém muitas vezes acabam se espalhando pela aplicação. A consequência dessa difusão é que o comportamento que utiliza os dados de um DTO tem dois “destinos” principais:

  • Uma “classe” com vários métodos estáticos;
  • no pior caso, o comportamento é duplicado em cada lugar que é usado.

A duplicação do comportamento pode ser bem problemática. Muitas vezes um mesmo comportamento está replicado com pequenas variações (e.g. uma comparação de string case-sensitive e outra case-insensitive). Esse acoplamento com o DTO dentro da aplicação também dificulta a mudança do mesmo, pois dependendo da escala da aplicação os pontos de mudança são muitos, e como não há uma centralização dos comportamentos, refactorings automatizados estão fora de questão.

Outro problema que ocorre nos dois casos é que esse estilo de programação dificulta a criação de testes da aplicação. Ao invés de criar um mock do objeto (estendendo o objeto/interface ou com algum framework), é necessário criar um objeto real com os dados necessários para o comportamento desejado. Com isso, o teste que deveria ter um ponto da aplicação sendo testado possui uma dependência com o comportamento associado ao DTO (ou seja, os testes falham se o “comportamento do DTO” for alterado).

Muitas vezes os DTOs são uma dependência externa do projeto (e.g. equipes distribuídas, cada uma trabalhando em um módulo da aplicação). Neste caso, se não for possível colocar o comportamento junto com os dados, é interessante ter uma classe que encapsule o DTO, fornecendo os comportamentos do mesmo. Desta forma evita-se que toda a aplicação conheça o funcionamento do DTO (flags ou outros atributos internos), fornece um ponto centralizado com o comportamento e melhora muito a legibilidade do código, pois os dados estão juntos com o comportamento associado.

Renato Besen Arquitetura de Software

Testes parametrizados com JUnit

9, dezembro, 2009

Muitas vezes temos a necessidade de executar um mesmo teste repetidas vezes mudando apenas alguns parâmetros. A solução mais simples caso sejam poucos testes é replicar o método de testes e mudar as variáveis, ou então criar um método com as asserções desejadas e chamá-lo com os diferentes parâmetros.

Ambas as possibilidades apresentadas pecam, pois não respeitam o princípio DRY. Uma solução muito mais elegante é usar os testes parametrizados, uma possibilidade que para quem usa JUnit existe a partir da versão 4.

O funcionamento é simples: um método no teste, anotado com Parameters, deve retornar uma coleção de arrays, que serão os parâmetros de execução:

@Parameters
public static Collection<Object[]> parameters() {
    List<Object[]> list = new ArrayList<Object[]>();
    list.add(new Object[] {"asshole!", "jerk!"});
    list.add(new Object[] {"what the hell?", "what the heck?"});
    return list;
}

Cada array desta coleção fornecerá os argumentos para o construtor do teste, possibilitando assim que estes valores sejam salvos em campos da classe, e utilizados por quem precisar dos mesmos:

public ProfanityFilterTest(String input, String output) {
    this.input = input;
    this.output = output;
    filter = new ProfanityFilter();
}

A classe de testes deve ser configurada para rodar com um runner específico, o Parameterized:

@RunWith(Parameterized.class)
public class ProfanityFilterTest {
[...]

Com estes passos básicos, ao rodar a classe testes, cada teste será chamado uma vez com os parâmetros fornecidos.

Resultado dos Testes Parametrizados

Para conferir o código completo baixe ProfanityFilter.java e ProfanityFilterTest.java.

Renato Besen Arquitetura de Software, Auto-gestão