“A programação orientada a objetos torna o código mais compreensível por meio da contenção de partes móveis. A programação funcional torna o código mais compreensível por meio da minimização de partes móveis”. – Michael Feathers, autor de Working with Legacy Code, no Twitter
Neste artigo, eu discuto um dos blocos de construção da programação funcional: a imutabilidade. O estado de um objeto imutável não pode mudar após a construção. Em outras palavras, o construtor é a única maneira de mudar o estado do objeto. Não é possível alterar um objeto imutável – em vez disso, criamos um novo objeto com o valor alterado e indicamos a referência para ele. String é um exemplo clássico de uma classe imutável desenvolvida no núcleo da linguagem Java. A imutabilidade é essencial para a programação funcional, porque corresponde às metas de minimizar as partes que mudam. Isso facilita pensar sobre essas partes.
Implementando classes imutáveis em Java
Linguagens orientadas a objeto modernas, como Java, Ruby, Perl, Groovy e C#, criaram mecanismos convenientes para facilitar a alteração de estado de maneiras controladas. No entanto, o estado é tão fundamental para cálculo que não se pode prever sua movimentação. Por exemplo, criar um código correto multiencadeado de alto desempenho é difícil em linguagens orientadas para objeto devido aos infinitos mecanismos de mutabilidade. Como Java é otimizado para manipular estado, é preciso se adaptar a alguns desses mecanismos para obter os benefícios da imutabilidade. Mas ao aprender a evitar algumas armadilhas, desenvolver classes imutáveis em Java se tornará mais fácil.
Definindo classes imutáveis
Para tornar uma classe Java imutável, é preciso:
- Tornar todos os campos final. Quando se definem campos como final em Java, é preciso inicializá-los no momento da declaração ou no construtor. Não entre em pânico se o IDE reclamar que você não inicializou no local da declaração. Ele perceberá que isso está correto quando você escrever o código adequado no construtor.
- Tornar a classe final para que não possa ser substituída. Se a classe puder ser substituída, os comportamentos de seus métodos podem ser substituídos também, portanto, o mais seguro é cancelar a aprovação de subclasses. Observe que essa é a estratégia usada pela classe String de Java.
- Não fornecer um construtor sem argumento.Quando se tem um objeto imutável, é preciso definir todos os estados que ele contém no construtor. Se não há estado para definir, para que ter um objeto? Métodos estáticos em uma classe stateless funcionariam da mesma forma. Por isso, nunca se deve ter um construtor sem argumento para uma classe imutável. Se, por algum motivo, você estiver usando uma estrutura que exija isso, veja se é possível satisfazê-la fornecendo um construtor sem argumento privado (que é visível por reflexo). Observe que a ausência de um construtor sem argumento viola o padrão JavaBeans, que insiste em um construtor padrão. Mas JavaBeans não pode ser imutável devido à maneira como os métodos setXXX funcionam.
- Fornecer ao menos um construtor. Se você não forneceu um construtor sem argumento, esta é sua última chance de incluir algum estado no objeto!
- Não fornecer outros métodos mutáveis exceto o construtor. Além de evitar métodos setXXX típicos inspirados em JavaBeans, é preciso ter cuidado para não retornar referências de objeto mutáveis. O fato de que a referência do objeto é final não significa que seja impossível alterar o objeto para o que ela aponta. Por isso, é preciso copiar defensivamente qualquer referência de objeto retornada a partir de métodos getXXX.
Classe imutável “tradicional”
A Listagem 1 contém uma classe imutável que satisfaz os requisitos anteriores:
public final class Address { private final String name; private final List<String> streets; private final String city; private final String state; private final String zip; public Address(String name, List<String> streets, String city, String state, String zip) { this.name = name; this.streets = streets; this.city = city; this.state = state; this.zip = zip; } public String getName() { return name; } public List<String> getStreets() { return Collections.unmodifiableList(streets); } public String getCity() { return city; } public String getState() { return state; } public String getZip() { return zip; } }
Observe o uso do método Collections.unmodifiableList() na Listagem 1 para fazer uma cópia defensiva da lista de ruas. É necessário sempre usar coleções para criar listas imutáveis em vez de arrays. Embora seja possível copiar arrays defensivamente, isso traz alguns efeitos colaterais indesejáveis. Considere o código na Listagem 2:
public class Customer { public final String name; private final Address[] address; public Customer(String name, Address[] address) { this.name = name; this.address = address; } public Address[] getAddress() { return address.clone(); } }
O problema no código da Listagem 2 se manifesta quando se tenta fazer alguma coisa com o array clonado que retorna a partir da chamada para o método getAddress(), como mostra a Listagem 3:
public static List<String> streets(String... streets) { return asList(streets); } public static Address address(List<String> streets, String city, String state, String zip) { return new Address(streets, city, state, zip); } @Test public void immutability_of_array_references_issue() { Address [] addresses = new Address[] { address(streets("201 E Washington Ave", "Ste 600"), "Chicago", "IL", "60601")}; Customer c = new Customer("ACME", addresses); assertEquals(c.getAddress()[0].city, addresses[0].city); Address newAddress = new Address( streets("HackerzRulz Ln"), "Hackerville", "LA", "00000"); // doesn't work, but fails invisibly c.getAddress()[0] = newAddress; // illustration that the above unable to change to Customer's address assertNotSame(c.getAddress()[0].city, newAddress.city); assertSame(c.getAddress()[0].city, addresses[0].city); assertEquals(c.getAddress()[0].city, addresses[0].city); }
Quando você retorna um array clonado, você protege o array subjacente – mas está devolvendo um array que se parece com um array ordinário -, o que significa que é possível alterar o conteúdo dele. Ainda que a variável que contém o array seja final, isso se aplica apenas à referência do array, e não ao seu conteúdo. Usando Collections.unmodifiableList() (e a família de métodos Collections para outros tipos), você recebe uma referência de objeto que não tem métodos mutáveis disponíveis.
Classes imutáveis mais limpas
Frequentemente, ouvimos dizer que campos imutáveis também devem ser transformados em privados. Eu discordo dessa ideia, após ouvir alguém que tem uma visão diferente, mas clara, esclarecer as considerações entranhadas. Em uma entrevista com o criador do Clojure, Rich Hickey, conduzida por Michael Fogus (consulte Recursos), Hickey fala sobre a ausência de encapsulamento para ocultar dados em diversas partes centrais do Clojure. Esse aspecto do Clojure sempre me incomodou, pois estou habituado ao pensamento com base em estado. Mas depois eu percebi que não é preciso se preocupar em expor campos se eles forem imutáveis. A maioria das proteções que usamos para encapsulamento só existe para evitar a mutação. Se separarmos esses dois conceitos, o resultado é uma implementação Java mais limpa.
Considere a versão da classe Address na Listagem 4:
public final class Address { private final List<String> streets; public final String city; public final String state; public final String zip; public Address(List<String> streets, String city, String state, String zip) { this.streets = streets; this.city = city; this.state = state; this.zip = zip; } public final List<String> getStreets() { return Collections.unmodifiableList(streets); } }
Declarar métodos getXXX() públicos para campos imutáveis é benéfico apenas quando queremos ocultar a representação subjacente. Mas esse benefício é duvidoso em um momento que IDEs de refatoração podem encontrar tais mudanças facilmente. Ao tornar os campos públicos e imutáveis, é possível acessá-los diretamente em código sem se preocupar em alterá-los acidentalmente.
@Test (expected = UnsupportedOperationException.class) public void address_access_to_fields_but_enforces_immutability() { Address a = new Address( streets("201 E Randolph St", "Ste 25"), "Chicago", "IL", "60601"); assertEquals("Chicago", a.city); assertEquals("IL", a.state); assertEquals("60601", a.zip); assertEquals("201 E Randolph St", a.getStreets().get(0)); assertEquals("Ste 25", a.getStreets().get(1)); // compiler disallows //a.city = "New York"; a.getStreets().clear(); }
A princípio, usar campos públicos imutáveis parece natural se você escutar os macacos furiosos¹Acessar os campos públicos e imutáveis evita a sobrecarga de uma série de chamadas getXXX(). Observe também que o compilador não permite designar uma das primitivas, e se você tentar chamar um método mutável na coleção street, o resultado será UnsupportedOperationException (como recebido na parte superior do teste). O uso desse estilo de código indica fortemente que se trata de uma classe imutável.
Desvantagens
Uma possível desvantagem da sintaxe mais limpa é o esforço para aprender esse novo idioma. Mas eu acho que vale a pena: encoraja a pensar sobre imutabilidade ao criar classes, devido a uma diferença estilística óbvia, e reduz código padrão desnecessário. Mas há algumas desvantagens desse estilo de código em Java (que, para falar a verdade, não foi projetado para acomodar imutabilidade diretamente):, mas a diferenciação deles é um benefício: você não está acostumado a lidar com tipos imutáveis em Java, mas isso parece um novo tipo, como ilustra a Listagem 5:
- Como Glenn Vanderburg comentou, a maior desvantagem é que o estilo viola o que Bertrand Meyer (criador a linguagem de programação Eiffel) chamou de Princípio do Acesso Uniforme: “Todos os serviços fornecidos por um módulo devem estar disponíveis por meio de uma notação uniforme, que não indica se são implementados por armazenamento ou por cálculo”. Em outras palavras, o acesso a um campo não deve dizer se o valor é retornado por um campo ou por um método. O método getStreets() da classe Address não é uniforme com os demais campos. Esse problema não pode ser realmente resolvido em Java, ele é resolvido em algumas das outras linguagens de JVM, de uma forma que permite imutabilidade.
- Algumas estruturas que dependem muito de reflexo não funcionam com esse idioma, pois exigem um construtor padrão.
- Como estamos criando objetos novos em vez de alterar os antigos, sistemas com muitas atualizações podem causar ineficiência com a coleta de lixo. Linguagens como Clojure possuem recursos integrados para tornar isso mais eficiente com referências imutáveis. Isso é o padrão nessas linguagens.
Imutabilidade no Groovy
Desenvolver a versão de campo público e imutável da classe Address em Groovy resulta em uma implementação limpa, como mostra a Listagem 6:
class Address { def public final List<String> streets; def public final city; def public final state; def public final zip; def Address(streets, city, state, zip) { this.streets = streets; this.city = city; this.state = state; this.zip = zip; } def getStreets() { Collections.unmodifiableList(streets); } }
Como de costume, Groovy exige menos código padrão que Java – e também possui outros benefícios. Como Groovy permite criar propriedades usando a sintaxe get/set familiar, é possível criar uma propriedade realmente protegida para a referência do objeto. Considere os testes de unidade mostrados na Listagem 7:
class AddressTest { @Test (expected = ReadOnlyPropertyException.class) void address_primitives_immutability() { Address a = new Address( ["201 E Randolph St", "25th Floor"], "Chicago", "IL", "60601") assertEquals "Chicago", a.city a.city = "New York" } @Test (expected=UnsupportedOperationException.class) void address_list_references() { Address a = new Address( ["201 E Randolph St", "25th Floor"], "Chicago", "IL", "60601") assertEquals "201 E Randolph St", a.streets[0] assertEquals "25th Floor", a.streets[1] a.streets[0] = "404 W Randoph St" } }
Observe que, em ambos os casos, o teste termina quando uma exceção é causada devido a uma violação do contrato de imutabilidade. Na Listagem 7, no entanto, a propriedade streets é semelhante às primitivas, mas está protegida através do método getStreets().
Anotação @Immutable de Groovy
Um dos princípios subjacentes desta série é que as linguagens funcionais devem cuidar de mais detalhes com baixo nível. Um bom exemplo é a anotação @Immutable incluída em Groovy versão 1.7, que torna desnecessário todo o código na Listagem 6. A Listagem 8 mostra uma classe Client que usa esse tipo de anotação:
@Immutable class Client { String name, city, state, zip String[] streets }
Por usar a anotação @Immutable, essa classe possui as seguintes características:
- É final.
- Propriedades têm automaticamente campos de suporte privados com métodos get sintetizados.
- Qualquer tentativa de atualizar propriedades resulta em ReadOnlyPropertyException.
- Groovy cria construtores ordinais e baseados em mapa.
- Classes de coleção são colocadas entre aspas apropriadas, e arrays (e outros objetos clonáveis) são clonados.
- Métodos padrão equals, hashcode e toString são gerados automaticamente.
Essa anotação é extremamente eficiente! Ela também funciona de acordo com a expectativa, como mostra a Listagem 9:
@Test (expected = ReadOnlyPropertyException) void client_object_references_protected() { def c = new Client([streets: ["201 E Randolph St", "Ste 25"]]) c.streets = new ArrayList(); } @Test (expected = UnsupportedOperationException) void client_reference_contents_protected() { def c = new Client ([streets: ["201 E Randolph St", "Ste 25"]]) c.streets[0] = "525 Broadway St" } @Test void equality() { def d = new Client( [name: "ACME", city:"Chicago", state:"IL", zip:"60601", streets: ["201 E Randolph St", "Ste 25"]]) def c = new Client( [name: "ACME", city:"Chicago", state:"IL", zip:"60601", streets: ["201 E Randolph St", "Ste 25"]]) assertEquals(c, d) assertEquals(c.hashCode(), d.hashCode()) assertFalse(c.is(d)) }
Tentar substituir a referência de objeto gera um ReadOnlyPropertyException. E tentar mudar o destino para onde apontam as referências de um objeto encapsulado gera um UnsupportedOperationException. Isso também gera métodos equals e hashcode apropriados, como mostra o último teste – o conteúdo do objeto é o mesmo, mas eles não apontam para a mesma referência.
Obviamente, tanto Scala como Clojure suportam e encorajam a imutabilidade e possuem sintaxe limpa para ela, e as implicações disso serão abordadas em artigos futuros da série.
Benefícios da imutabilidade
Adotar a imutabilidade é essencial para pensar como um programador funcional. Embora o desenvolvimento de objetos imutáveis em Java exija um pouco mais de complexidade inicial, a simplificação forçada por essa abstração na parte posterior dos projetos compensa facilmente o esforço.
Classes imutáveis eliminam diversas coisas geralmente preocupantes em Java. Um dos benefícios de utilizar um pensamento funcional é perceber que existem testes para verificar se as mudanças ocorreram com sucesso no código. Em outras palavras, o propósito real do teste é validar a mutação – e quanto mais mutação houver, mais testes são necessários para garantir que esteja correta. Se isolar os lugares nos quais as mudanças ocorrem restringindo bastante a mutação, você cria um espaço muito menor para erros e tem menos lugares para testar. Como mudanças ocorrem apenas na construção, as classes imutáveis fazem com que seja trivial criar testes de unidade. Não é necessário copiar um construtor, e não é preciso se preocupar com os detalhes internos da implementação do método clone(). Os objetos imutáveis são bons candidatos para chaves em Maps ou Sets. Chaves em coleções de dicionários em Java não podem mudar de valor enquanto são usadas como chave, por isso objetos imutáveis são ótimas chaves.
Objetos imutáveis também são automaticamente thread-safe e não possuem problemas de sincronização. Também não podem jamais existir em estado desconhecido ou indesejado devido a uma exceção. Como toda a inicialização ocorre no momento da construção, qualquer exceção ocorre antes de haver uma instância de objeto. Joshua Bloch chama isso de atomicidade de falha: sucesso ou falha que depende da mutabilidade é resolvido quando o objeto é desenvolvido (consulte Recursos).
Por fim, um dos melhores recursos das classes imutáveis é como elas se encaixam na abstração de composição. No próximo artigo, eu começarei a investigar a composição e por que ela é tão importante no mundo do pensamento funcional.
¹ Quem me contou essa história pela primeira vez foi Dave Thomas e eu a usei em meu livro The Productive Programmer (consulte Recursos). Eu não sei se essa é uma história verdadeira (apesar de ter pesquisado bastante), mas isso não é o importante. Ela ilustra muito bem esse argumento.
Nos anos 1960, cientistas comportamentais realizaram um experimento no qual colocaram cinco macacos em uma sala, com uma escada e um cacho de bananas pendurado no teto. Os macacos rapidamente descobriram que podiam subir na escada e comer bananas. Então, sempre que um dos macacos se aproximava da escada, os cientistas esguichavam água gelada na sala inteira. Logo nenhum dos macacos se aproximava da escada. Em seguida, os cientistas substituíram um desses macacos por um novo macaco, que ainda não tinha participado do experimento. Quando ele foi direto para a escada, todos os outros macacos o agrediram. Ele não sabia por que estava sendo agredido, mas ele logo aprendeu que não deveria chegar perto da escada. Gradualmente, os cientistas substituíram todos os macacos do início da experiência, até que tinham um grupo de macacos que nunca tinham sido molhados com água gelada, mas eles ainda assim atacavam qualquer um que se aproximasse da escada.
A moral? Em projetos de software, muitas práticas existem porque “sempre fizemos assim”.
***
Faça o download de uma versão experimental gratuita do IBM Cognos Express V9, a primeira e única solução de inteligência de negócios (BI) e planejamento integrada, construída com o propósito de atender as necessidades de empresas de médio porte. Ela fornece os recursos básicos de relatório, análise, painel, pontuação, planejamento, orçamento e previsão que empresas de médio porte necessitam, por um preço acessível. Tudo é incluído em uma solução pré-configurada que é fácil de instalar, usar e comprar.
Recursos
Aprender
- The Productive Programmer (Neal Ford, O’Reilly Media, 2008): O livro mais recente de Neal Ford trata de ferramentas e práticas que ajudam a melhorar sua eficiência em codificação.
- Clojure: Clojure é um Lisp moderno e funcional que é executado na JVM.
- Rich Hickey Q&A: Michael Fogus entrevista Rich Hickey, criador do Clojure.
- Stuart Halloway sobre Clojure: Saiba mais sobre o Clojure com esse podcast do developerWorks.
- Scala: é uma linguagem moderna e funcional em JVM.
- Guia do Scala para desenvolvedores de Java atarefados: aprofunde-se em Scala nessa série do developerWorks escrita por Ted Neward.
- Effective Java, 2.ª ed. (Joshua Bloch, Addison Wesley, 2008): Leia mais sobre atomicidade de falha.
- Navegue na livraria de tecnologia para ver livros sobre este e outros tópicos técnicos.
- Zona tecnologia Java do developerWorks: Encontre centenas de artigos sobre quase todos os aspectos da programação Java.
Obter produtos e tecnologias
- Faça o download de Versões de avaliação de produto IBM ou explore as versões de teste on-line no IBM SOA Sandbox e entre em contato com as ferramentas de desenvolvimento de aplicativos e produtos de middleware do DB2®, Lotus®, Rational®, Tivoli® e WebSphere®.
Discutir
Participe da comunidade do developerWorks. Entre em contato com outros usuários do developerWorks, enquanto explora os blogs, fóruns, grupos e wikis orientados ao desenvolvedor.
***
Sobre o autor: Neal Ford é um arquiteto de software e Meme Wrangler, na ThoughtWorks, uma consultoria global de TI. Projeta e desenvolve aplicativos, materiais de instrução, artigos para revistas, treinamentos e apresentações em vídeo/DVD, e é autor ou editor de livros que abordam uma variedade de tecnologias, inclusive The Productive Programmer Seu enfoque é o projeto e construção de aplicativos corporativos de grande porte. Também é orador internacionalmente aclamado nas conferências de desenvolvedores ao redor do mundo. Conheça seu Web site.
***
Artigo original disponível em: http://www.ibm.com/developerworks/br/library/j-ft4/index.html