A Orientação a Objetos é constantemente mal utilizada. Sem o devido treino, temos a tendência de raciocinar de forma estruturada.
Esse problema está tão enraizado que muitas vezes encontramos dificuldades nos pontos mais fundamentais da programação, tal como a construção de instâncias de objetos um tanto mais complexos que beans simples. Como consequência, nosso código fica difícil de manter e mais propenso a erros.
Portanto, todo desenvolvedor deve estudar Padrões de Projetos (Design Patterns), já que estes ajudam a resolver os problemas mais comumente enfrentados e nos dão o treino necessário para pensarmos sobre os problemas mais complexos. Além disso, existem outras técnicas, como as Interfaces Fluentes (Fluent Interfaces), que ajudam a criarmos código mais legível e, dessa forma, aumentam a produtividade e diminuem a propensão a erros.
Criando um objeto (the dummy way)
Suponha que precisamos criar um objeto com diversos atributos “opcionais”. Vamos usar uma pizza como exemplo (um dos exemplos clássicos para ilustrar padrões de projetos). Já vi muitos construtores de “pizza” por aí da seguinte forma:
public class Pizza { private int tamanho; private boolean queijo; private boolean tomate; private boolean bacon; Pizza(int tamanho) { this.tamanho = tamanho; } Pizza(int tamanho, boolean queijo) { this(tamanho); this.queijo = queijo; } Pizza(int tamanho, boolean queijo, boolean tomate) { this(tamanho, queijo); this.tomate = tomate; } Pizza(int tamanho, boolean queijo, boolean tomate, boolean bacon) { this(tamanho, queijo, tomate); this.bacon = bacon; } }
Diga a verdade, você já faz isso em algum momento? E pode ficar pior se acrescentarmos construtores com combinações e ordenação diferentes de parâmetros!
A sobrecarga é interessante quando temos algumas poucas variações de parâmetros e há poucas mudanças no conjunto de atributos. Porém, chega uma hora que nem sabemos mais o que está acontecendo. Quanto tempo você já perdeu inspecionando conteúdo de classes de terceiros para entender que valores deveria usar? Exemplo:
new Pizza(10, true, false, true, false, true, false, true, false...)
Que tipo de pizza é essa mesmo? 😯
Criando um objeto com Builder Pattern
Alerta: antes que alguém fique agitado porque este exemplo diverge do Builder Pattern originalmente publicado no GoF, leia o livro com atenção, pois os próprios autores propõem implementações alternativas dos padrões de projeto e afirmam categoricamente que estes devem ser adaptados de acordo com o contexto. É sério, está escrito lá, acredite ou não.
public class Pizza { private int tamanho; private boolean queijo; private boolean tomate; private boolean bacon; public static class Builder { // requerido private final int tamanho; // opcional private boolean queijo = false; private boolean tomate = false; private boolean bacon = false; public Builder(int tamanho) { this.tamanho = tamanho; } public Builder queijo() { queijo = true; return this; } public Builder tomate() { tomate = true; return this; } public Builder bacon() { bacon = true; return this; } public Pizza build() { return new Pizza(this); } } private Pizza(Builder builder) { tamanho = builder.tamanho; queijo = builder.queijo; tomate = builder.tomate; bacon = builder.bacon; } }
Aplicando o padrão de projeto Builder, temos agora um objeto “construtor” para o objeto Pizza. A classe Pizza está um pouco mais complexa, mas confira como ficou elegante a forma de temperarmos:
Pizza pizza = new Pizza.Builder(10) .queijo() .tomate() .bacon() .build();
É muito mais fácil de codificar com essa API e entender o que está acontecendo.
O Builder Pattern é muito utilizado em boas bibliotecas que disponibilizam APIs intuitivas e fáceis de aprender, como construtores de XML e o Response do JAX-RS, por exemplo.
Composição de Objetos
Outro ponto onde temos dificuldades encontra-se na criação de objetos que são compostos por vários objetos. Como exemplo, vamos montar um pedido com alguns itens para um cliente:
public class Pedido { List<Item> itens; Cliente cliente; void adicionarItem(Item item) { itens.add(item); } void setCliente(Cliente cliente) { this.cliente = cliente; } void fechar() { // ... } }
O uso fica assim:
Pedido p = new Pedido(); p.setCliente(new Cliente("José")); p.adicionarItem(new Item("Motocicleta", 1)); p.adicionarItem(new Item("Capacete", 2)); p.fechar();
Embora, neste exemplo, nosso código esteja relativamente simples, um objeto mais complexo dificultaria ao desenvolvedor entender o código. Mas, estou certo, podemos fazer muito melhor que isso!
Interface Fluente
A “mágica” das Interfaces Fluentes está em encadear métodos que retornam a instância do próprio objeto, evitando repetições desnecessárias no código “cliente”. Abaixo, o exemplo anterior foi ligeiramente modificado:
public class Pedido { List<Item> itens; Cliente cliente; Pedido com(int quantidade, String nome) { itens.add(new Item(nome, quantidade)); return this; } Pedido para(String nome) { cliente = new Cliente(nome); return this; } void fechar() { // ... } }
O uso fica assim:
new Pedido() .para("José") .com(1, "Motocicleta") .com(2, "Capacete") .fechar();
Muito mais limpo, com maior clareza e intuitivo, não é mesmo?
Classes Utilitárias
Temos ainda classes utilitárias com seus métodos estáticos, usadas nos mais diversos pontos da arquitetura de um sistema. Elas causam problemas quando mal planejadas, pois a tendência é acumularmos muitos métodos sobrecarregados com diferentes objetivos que infectam todo o código. Então, sem perceber, perdemos o controle, já que agora todo o sistema está acoplado a tais classes e alterações acabam impactando onde não esperamos.
Vejamos um exemplo de uma típica classe com rotinas de tratamento de datas:
public class Data { public static Date converteTextoParaData(String dataStr) { try { return new SimpleDateFormat("dd/MM/yyyy").parse(dataStr); } catch (ParseException e) { return null; } } public static String converteDataParaTexto(Date data) { return new SimpleDateFormat("dd/MM/yyyy").format(data); } public static Date avancarDiasCorridos(Date dataInicial, int dias) { Calendar c = Calendar.getInstance(); c.setTime(dataInicial); c.add(Calendar.DATE, dias); return c.getTime(); } }
Eis como ficaria uma manipulação simples de data usando essa classe:
String inputDateStr = "28/02/2013"; Date inputDate = Data.converteTextoParaData(inputDateStr); Date resultDate = Data.avancarDiasCorridos(inputDate, 30); String resultDateStr = Data.converteDataParaTexto(resultDate);
Entediante. Vamos refatorar a classe com o conceito de Interface Fluente:
public class Data { private Date data; public Data(String dataStr) { try { data = new SimpleDateFormat("dd/MM/yyyy").parse(dataStr); } catch (ParseException e) { throw new IllegalArgumentException(e); } } public String toString() { return new SimpleDateFormat("dd/MM/yyyy").format(data); } public Data avancarDiasCorridos(int dias) { Calendar c = Calendar.getInstance(); c.setTime(data); c.add(Calendar.DATE, dias); data = c.getTime(); return this; } }
E o uso fica assim:
String resultDateStr = new Data("28/02/2013").avancarDiasCorridos(30).toString();
Simples, não?
Nota: considere este como um exemplo de estudo. O uso do new para instanciar objetos é discutível, assim como o uso da classe java.util.Date.
Conclusões
Simplificar o código não é luxo. Trata-se de uma necessidade na luta contra a crescente complexidade dos sistemas de software.
A utilização de padrões de projetos como o Builder Pattern e técnicas como a Fluent Language nos auxiliam nessa empreitada, além de tornar a codificação mais simples e agradável.
Outro ponto positivo é que, ao utilizar objetos específicos para tratar problemas comuns, como no exemplo de Fluent Language, fica mais fácil manter o foco da responsabilidade das classes. Com métodos estáticos, a tendência é acrescentar novos métodos indiscriminadamente, enquanto com um objeto “inteligente” temos uma melhor percepção dos limites.
Leitura Obrigatória
Para ler mais sobre Fluent Interfaces, acesse http://martinfowler.com/bliki/FluentInterface.html.
Além disso, todo desenvolvedor deve ler o livro Design Patterns: Elements of Reusable Object-Oriented Software. Infelizmente, o livro é um tanto antigo e os exemplos podem ser um pouco “chatos” para desenvolvedores web, pois muitos aplicam-se à construção de GUI de aplicativos desktop.
Especificamente para quem está iniciando em Java, uma leitura mais leve é o Head First Design Patterns. É mais fácil de entender, porém incompleto. Não espere encontrar todos os padrões em detalhes. Muitos deles só constam no final como uma espécie de resumo.