Brian Goetz, Arquiteto Java na Oracle, trabalha constantemente em busca de produtividade e desempenho da linguagem Java e, com isso, introduziu um conceito experimental sobre classes de dados com potencial para, algum dia, ser integrado a linguagem. Sua pesquisa demonstra um ajuste natural das classes de dados com recursos promissores, como tipos de valores e pattern matching. Ainda há muito trabalho a ser realizado, antes do conceito estar pronto para se tornar parte efetiva da linguagem Java. Goetz explorou os problemas e as compensações das classes de dados com base na premissa de que, as vezes "dados são apenas dados".
Motivação
As classes Java geralmente exigem muitos códigos redundantes, independentemente do quão simples ou complexas essas classes possam ser. Isso levou a reputação do Java em ser "muito verboso". Goetz explica:
Para escrever uma simples classe responsável por transportar dados, é necessário escrever muito código repetitivo de baixo valor: construtores, métodos de acesso, equals(), hashCode(), toString(), etc. Os desenvolvedores são tentados a "cortar caminho" omitindo métodos importantes, levando a um comportamento surpreendente ou a um debug precário, ou insistindo em uma solução alternativa, que não é inteiramente apropriada para o serviço, mas ela tem a "forma correta" e o desenvolvedor não quer criar mais uma classe.
As IDEs ajudam a escrever a maior parte do código, mas a codificação é a menor parte do problema. As IDE's não fazem nada para ajudar o leitor a conhecer a intenção de design do "plano para transportar dados os x, y e z", por meio das dezenas de linhas de códigos repetidos. O código repetitivo é um bom lugar para os bugs se esconderem; se pudermos, é melhor eliminar seus refúgios imediatamente.
Semelhante as declarações das classes definidas em Scala (case), Kotlin (data) e C# (record) que foram projetados para serem compactos, o mesmo poderia ocorrer com uma classe Java sendo portadora de dados simples e com um mínimo de sobrecarga. Sem uma definição formal do suporte aos dados ficaria difícil seu reconhecimento pela maioria dos desenvolvedores Java. E, embora a comunidade Java realmente aceite um mecanismo nas classes de dados, existiriam diversas interpretações individuais sobre um transporte de dados. Goetz usou a parábola dos cegos e um elefante para explicar:
Como dirá Algebraic Annie, "uma classe de dados é apenas um tipo de produto algébrico". Como as classes case no Scala, que vem unidas com pattern matching, e melhores servidas de modo imutável (e, de sobremesa, Annie pediria interfaces sealed).
Boilerplate Billy dirá "uma classe de dados é apenas uma classe comum, mas com uma sintaxe melhor", e provavelmente se arrepia com as restrições de mutabilidade, extensão ou encapsulamento (o irmão de Billy, JavaBeans Jerry, dirá "eles devem ser JavaBeans - é claro que tenho getters e setters também". E sua irmã, POJO Patty, destaca que está se afogando em POJOs empresariais, e lembra que gostaria que eles fossem procurados por frameworks como o Hibernate).
Tuple Tommy dirá "uma classe de dados é apenas uma tupla nominal" - e nem pode esperar que tenham outros métodos além dos principais métodos de Object - que são mais simples que os agregados (ele pode até esperar seus nomes sejam apagados, para que duas classes de dados da mesma "forma" possam ser livremente convertidas).
Values Victor dirá "uma classe de dados é realmente apenas um tipo de valor de maior transparência".
Todas essas pessoas estão unidas em favor das "classes de dados", mas possuem ideias divergentes sobre como são as classes de dados, e pode não haver nenhuma solução que os agrade.
Entendendo o problema
O conceito das classes de dados vai além da redução do código repetitivo, Goetz afirma que isso é "apenas um sintoma de um problema muito mais profundidade", no qual o custo do encapsulamento é compartilhado por meio das classes Java. Os princípios da abstração e encapsulamento orientados a objeto permitem que os desenvolvedores Java escrevam códigos robustos e seguros em diversos limites:
- Limites de manutenção;
- Limites de segurança e confiança;
- Limites de integridade;
- Limites de versionamento.
Para classes como SocketInputStream, esses limites são essenciais devido a sua complexidade inerente. Mas em uma classe que é portadora de dados simples como dois valores inteiros (como o exemplo a seguir), é realmente preciso preocupar-se com tais limites?
record Point(int x,int y) { ... }
Goetz explica:
O custo de estabelecer e sustentar esses limites (como os argumentos do construtor que mapeiam o estado, como definir a igualdade do estado, etc) é constante entre as classes, mas o benefício não é. O custo e benefícios, às vezes, pode ultrapassar os limites. Esse é um dos motivos pelos quais os desenvolvedores Java dizem fazer "muita cerimônia" - não que a cerimônia não tenha valor, mas que são forçados a invocá-la mesmo quando ela não oferece um valor significativo.
O modelo de encapsulamento que o Java fornece - em que a representação é inteiramente desacoplada da construção, acesso de estado e igualdade - é mais do que muitas classes necessitam. As classes que possuem um relacionamento simples com seus limites podem obter um benefício em um modelo mais simples, no qual podemos definir uma classe como um wrapper fino em torno de seu estado e derivar a relação entre: estado, construção, igualdade e acesso de estado.
Além disso, os custos de desacoplamento da representação da API vão além da sobrecarga em declarar os membros repetitivos; o encapsulamento, por sua natureza, é destruidor de informações.
Requisitos para classes de dados
Usando a declaração do Point a seguir, considere sua definição como um transportador de dados simples:
final class Point extends java.lang.DataClass { public final int x; public final int y; public Point(int x,int y) { this.x = x; this.y = y; } // padrões para desestruturar o Point(int x,int y) // implementações com base no estado de equals(), hashCode() e toString() // acesso de leitura pública para x() e y() }
Para estudar sobre o design no planejamento dos dados, Goetz definiu um conjunto de requisitos (ou restrições) para "gerar de forma segura e mecânica os construtores, padrões de extração, métodos de acesso, equals(), hashCode() e toString() - e mais". Ele descreve:
Dizem que uma classe C possui um conversão transparente para um vetor de estado S se:
- Existe uma função ctor: S -> C que mapeia uma instância do vetor de estado para uma instância de C (o construtor pode rejeitar alguns vetores com estado inválido, como números racionais cujo denominador é zero);
- Existe uma função total dtor: C -> S que mapeia uma instância de C para um vetor de estado S, domínio do ctor;
- Para qualquer instância c de C, ctor(dtor(c)) é igual a c, de acordo com o equals() do C;
- Para dois vetores de estado s1 e s2, se cada um de seus componentes é igual ao componente correspondente do outro (de acordo com o equals() do componente), então ctor(s1) e ctor(s2) são ambos indefinidos, ou eles são iguais de acordo com o equals() do C;
- Para instâncias equivalentes de c e d, invocar a mesma operação produz resultados equivalentes: c.m() é igual a d.m(). Além disso, após a operação, c e d ainda devem ser equivalentes.
Essas invariantes são uma tentativa de capturar os requisitos; essa mudança é transparente, pois existe uma relação simples e previsível entre a representação das classes, sua construção e sua desestruturação - na qual a API é a representação.
Classes de dados e Pattern Matching
Um simples transporte de dados tem suas vantagens, como afirma Goetz, como "converter livremente uma instância de classe de dados de trás para frente entre sua forma agregada e seu estado explodido". Isso funcionaria muito bem com o pattern matching. Como demonstrado em seu trabalho sobre pattern matching, Goetz mostrou a desestruturação e melhorias na utilização do switch. Com isso em mente, pode ser possível escrever o seguinte código:
interface Shape { ... } record Point (int x,int y) { ... } record Rect (Point p1, Point p2) implements Shape { ... } record Circle (Point center, int radius) implements Shape { ... } ... switch(shape) { case Rect (Point(var x1, var y1),Point(var x2,var y2)) : ... case Circle (Point(var x, var y), int radius): ... }
Qualquer instância concreta de Shape pode ser facilmente desestruturada dentro de uma instrução switch. Isso também pode ser útil para a externalização, como serialização, conversão de/para JSON e XML, e mapeamento de banco de dados.
Refinando o Design Space
Goetz mostrou que os requisitos para ser um portador de dados simples vem com algo em troca. Ele explica:
O modelo mais simples - e mais rígido - para as classes de dados é dizer que uma classe de dados é uma classe final com os campos finais e públicos, e para cada um dos estados do componente, um construtor público e um padrão de desconstrução cuja assinatura corresponde à descrição do estado e o estado com base em implementações dos métodos do objeto central, além disso, nenhum outro membro (ou implementações explícitas dos membros implícitos) é permitido. Esta é essencialmente a interpretação mais rigorosa de uma tupla nominal.
Este ponto de partida é simples e estável - e quase todo mundo encontrará algo para se opor a isso. Então, o quanto podemos moderar nas restrições sem desistir dos benefícios semânticos que queremos? Vejamos algumas direções nas quais o ponto de partida rígido poderia ser estendido e suas interações.
Essas instruções abrangem uma grande variedade de elementos de design e problemas relacionados:
- Interfaces e métodos adicionais
- Riscos violam a regra "nada além do estado".
- Sobrescrevendo membros implícitos
- Risco de violar os requisitos de um simples transporte de dados.
- Construtores adicionais
- Assegurar-se de que o estado do objeto e a descrição do estado sejam equivalentes.
- Campos adicionais
- Risco de violar "o estado, o estado inteiro, e nada além do estado".
- Extensão
- Problemas relacionados a extensão entre classes de dados e classes regulares.
- Mutabilidade
- Questionar a lógica de permitir que as classes de dados sejam mutáveis.
- Encapsulamento dos atributos e métodos de acesso
- Certificar-se de que os atributos encapsulados devem ser legíveis.
- Matrizes e cópias defensivas
- Cópias defensivas violam a invariante de desestruturação e reconstrução de uma matriz para garantir uma instância igual.
- Segurança de Thread
- Questionamento de como a mutabilidade das classes de dados pode ser thread-safe.
Resumo
Java teve um excelente ano em 2017 e teve muitas novidades na linguagem esse ano. No entanto, como Goetz disse ao InfoQ, as classes de dados ainda são consideradas uma idéia "meio cruas" que requerem muito trabalho para entender como esse conceito pode algum dia se tornar uma realidade.
Em resumo, Goetz explica:
A questão-chave para projetar uma facilidade para "dados agregados simples" em Java é identificar quais graus de liberdade estamos dispostos a abandonar. Se tentarmos modelar todos os níveis de liberdade das classes, apenas movemos a complexidade; para obter algum benefício, devemos aceitar algumas restrições. Pensamos que as restrições sensatas para se aceitar, estão negando o uso do encapsulamento para dissociar a representação da API e para mediar o acesso de leitura ao estado; por sua vez, isso fornece benefícios sintáticos e semânticos significativos para classes que podem aceitar essas restrições.
Vicente Romero, principal membro da equipe técnica da Oracle, fez recentemente um "push inicial público" sobre o desenvolvimento de classes de dados que podem ser encontradas na ramificação do repositório do Projeto Âmbar.
Goetz falou ao InfoQ sobre sua pesquisa com classes de dados:
InfoQ: Qual tipo de resposta foi recebida desde a publicação do seu artigo?
Brian Goetz: A resposta esperada: alguns comentários altamente positivos sobre a ideia e uma variedade de sugestões (na maior parte mutuamente inconsistentes) de como poderia ser "melhorada". Ou seja, as pessoas gostam da ideia, mas, como esperado, muitas pessoas gostariam que seguissemos o design central em uma ou outra direção, para atender as suas preferências pessoais. Como uma característica altamente subjetiva, isso era de se esperar.
InfoQ: Você prevê um mecanismo de classe de dados para algum dia ser integrado na linguagem de programação Java? Se sim, que tipo de esforço será necessário para abordar todas as preocupações discutidas no seu artigo?
Goetz: Isso irá exigir um "tempo de cozimento". Com o design da linguagem, sua primeira idéia, não importa o quão cuidadosamente seja pensada, será errada. Assim como a segunda. Muitos recursos da linguagem exigem meia dúzia ou mais interações antes que finalmente se descubra o lugar certo para "pousar". Então iremos experimentar, prototipar, reunir feedback, iterar e iterar novamente. Até sentirmos que chegamos ao lugar certo.
InfoQ: É um objetivo ou não promover o rebasing das implementações de classes não-Java, de classes compactas (por exemplo, classes de case do Scala) sobre classes de dados?
Goetz: Toda linguagem tem sua própria sintaxe de superfície. No entanto, as classes de dados se conectam a recursos de outras linguagens, como pattern matching, e esperamos (como aconteceu com o Lambda) que outras linguagens direcionem o suporte em tempo de execução desses recursos e ganhem os benefícios da interoperabilidade.
InfoQ: Até onde se sabe, os arquitetos de Scala, Kotlin e C# enfrentaram desafios semelhantes ao implementar uma declaração compacta de classe?
Goetz: De fato, embora tanto o Kotlin quanto o Scala conseguissem levar isso muito mais perto do início de seus projetos do que o C#, também tinham menos restrições de navegação. E cada um se estabeleceu em um ponto ligeiramente diferente no design de espaço.
InfoQ: Qual é a mensagem individual mais importante que gostaria que os leitores soubessem sobre as classes de dados?
Goetz: Essas classes de dados são sobre dados, não sobre concisão sintática. Trata-se de fornecer um meio natural para modelar dados puros no modelo de objeto. E nem todas as classes são portadoras de dados puros, mesmo que gostem dos benefícios da concisão que as classes de dados oferecem.
InfoQ: Qual sua visão para a pesquisa de classes de dados?
Goetz: Quebrar os recursos que as classes de dados necessitam em recursos mais refinados, que possam ser usados em todas as classes. Por exemplo, mesmo as classes que não sejam portadoras de dados, os construtores estão repletos de repetições propensos a erros, que poderiam ser substituídos fazendo uso de uma correspondência de nível mais alto entre os parâmetros do construtor e sua representação. Dessa forma, as classes de dados se tornam mais simples, e mais classes podem obter o benefício do recurso sem tentar encaixá-las em classes de dados.
Referências
- Brian Goetz Speaks to InfoQ on Pattern Matching for Java por InfoQ (27 de Setembro, 2017)
- Java Language Features - All Aboard Project Amber por Brian Goetz at Devoxx Belgium (10 de Novembro, 2017)
- Design of Java Value Types Makes Progress por InfoQ (30 de Novembro, 2017)
- Looking Forward to Java in 2018 por InfoQ (30 de Dezembro, 2017)