
Compartilhar:
Sina é um ex-membro da equipe da Vonage. Ele atuou como Java Developer Advocate na Vonage. Com formação acadêmica, ele tem uma curiosidade geral por tudo o que se relaciona a carros, computadores, programação, tecnologia e natureza humana. Em seu tempo livre, ele gosta de caminhar ou jogar videogames competitivos.
Aumente sua produtividade com a engenharia orientada a modelos (Parte 3)
Introdução
Bem-vindos de volta ao episódio final desta série sobre engenharia orientada a modelos. Na Parte 2, descrevi como usei essas tecnologias para economizar muito tempo ao adicionar suporte a novas APIs ao SDK Java da Vonage. Neste artigo, destacarei alguns princípios-chave com base nas lições aprendidas, para que você possa aproveitar ao máximo essas tecnologias e essa abordagem.
Conheça o seu “porquê”
A primeira coisa a ter em mente antes de prosseguir é certificar-se de que essa solução seja adequada ao seu caso de uso. Como diz o ditado: “Quando tudo o que você tem é um martelo, tudo parece um prego”. É natural que as tecnologias passem por modismos — isso é conhecido como o “ciclo de hype do Gartner”. Embora certas tecnologias e metodologias de desenvolvimento pareçam atraentes, no fim das contas elas são apenas ferramentas. Ter uma caixa de ferramentas diversificada é ótimo, pois significa que podemos escolher a ferramenta ou abordagem certa para cada tarefa. O que é adequado para um caso de uso pode não ser para outro. Então, quando o MDE é a escolha certa e quando não é? Aqui estão algumas perguntas a serem consideradas.
É fácil modelar esse domínio?
Nem todos os desafios se encaixam perfeitamente em um metamodelo facilmente definível. Embora, em muitos casos, seja possível adotar uma visão orientada a objetos de um domínio com relações previsíveis entre os Concepts desse domínio, pode haver ocasiões em que a natureza desses Concepts e até mesmo a forma como eles se relacionam entre si seja dinâmica demais. Para aproveitar ao máximo uma abordagem orientada a modelos, o metamodelo do domínio deve, no mínimo, ser definível com um alto grau de certeza. Os conceitos devem ser mapeados para classes com atributos e relações definíveis. Se houver muitos elementos do domínio a serem definidos e as relações forem muito complexas, seu metamodelo refletirá isso. Nesses casos, definir o metamodelo e os modelos será uma tarefa muito árdua e pode superar os benefícios em comparação com a implementação direta.
Qual será o nível de complexidade do gerador?
Se o seu domínio puder ser definido com clareza por meio de um metamodelo, você também precisa considerar como esses conceitos se relacionam com o código e a lógica de negócios que deseja gerar. Há argumentos a favor de ambos os extremos: se a lógica for densa (ou seja, altamente dependente do modelo), a geração automática reduz a chance de erros. Por outro lado, isso pode tornar o gerador mais difícil de manter e de se trabalhar com ele. Também vale a pena questionar quantos modelos você precisará desenvolver e manter, bem como a complexidade geral da lógica de coordenação para invocar esses modelos, especialmente quando comparada ao código escrito manualmente.
Quanta codificação manual isso vai economizar?
Uma das questões mais importantes — e talvez também a mais direta — é a proporção entre código padrão e lógica. Uma abordagem de geração de código orientada a modelos e baseada em modelos funciona muito bem quando o código gerado é, em sua maior parte, código padrão, de modo que o resultado possa ser colado em sua base de código com modificações mínimas ou até mesmo nenhuma modificação. No entanto, se o código gerado exigir um trabalho manual significativo de codificação adicional — especialmente se for necessário modificar o código gerado, em vez de, por exemplo, adicionar novos métodos —, o valor do gerador diminui. Isso ocorre porque a sobrecarga de desenvolver e manter o gerador, bem como refatorar ou modificar manualmente o código gerado, pode ser um fardo para alguns desenvolvedores em comparação com escrever essas seções do código do zero.
Com que frequência você vai usá-lo?
Mesmo que uma abordagem orientada a modelos seja viável — ou seja, se o seu domínio se encaixar perfeitamente em um metamodelo e a geração de código for vantajosa —, talvez não valha a pena o esforço se você planeja usá-la apenas uma vez. É claro que isso depende de outros fatores, como a quantidade de código que você está gerando — se for um projeto grande, pode muito bem valer a pena o investimento, mesmo que seja uma vez só. Em outros casos, a “economia” pode ser modesta, mas vai se acumulando com o tempo por meio do uso frequente. Outra maneira de pensar sobre essa questão é: “Quantos modelos teremos?” — é provável que, quanto mais modelos você criar (e quanto mais frequentemente você os alterar), mais você aproveitará o gerador.
É fácil atualizar seu(s) modelo(s)?
No ponto anterior, mencionei como ter mais modelos ou alterá-los com frequência permite aproveitar melhor os benefícios de usar uma abordagem de geração de código. No entanto, se seus modelos forem criados exclusivamente para esse exercício e precisarem ser constantemente atualizados para refletir mudanças nos requisitos de negócios ou no ambiente, isso pode gerar atritos. No meu caso, por exemplo, qualquer alteração na especificação da API exige que o modelo seja atualizado. O trabalho envolvido na atualização do modelo é diretamente proporcional à extensão das alterações na especificação. Se os modelos precisarem evoluir rapidamente, isso significa que qualquer código gerado anteriormente agora está inválido e precisa ser regenerado. Considere isso juntamente com o fato de que parte do código gerado pode ter sido (ou precisa ser) modificada manualmente, conforme discutido em um ponto anterior.
O metamodelo precisará ser alterado com frequência?
Como os modelos são dados estruturados, é razoável esperar que eles mudem — às vezes, até mesmo rapidamente. Isso geralmente não representa problema e causa relativamente pouco atrito, desde que as atualizações do modelo sejam automatizadas. O que não deve mudar com frequência, no entanto, é o esquema (metamodelo). Isso é um pouco semelhante ao primeiro ponto: se os conceitos em seu domínio estão mudando com frequência e são difíceis de definir em um metamodelo estável, isso torna muito mais difícil seguir uma abordagem orientada a modelos. Idealmente, o metamodelo deve ser revisado principalmente durante a fase de prototipagem e raramente depois de se tornar estável e “em produção” (ou seja, quando o resultado já é utilizável). Adicionar campos ao metamodelo geralmente não é uma alteração que causa incompatibilidade, já que eles podem ser opcionais, mas remover classes, relações e atributos pode (e geralmente vai) comprometer seus modelos e fluxo de trabalho existentes, o que significa que, em alguns casos, você terá que recomeçar do zero.
Além das correções de bugs, com que frequência o gerador será atualizado?
Durante o desenvolvimento inicial dos modelos do gerador de código e da lógica de coordenação, é natural que haja muitos bugs ou omissões. Quando esses problemas forem, em grande parte, resolvidos e o resultado gerado estiver conforme o desejado, o gerador pode ser considerado estável. No entanto, mesmo depois disso, haverá inevitavelmente mudanças devido a novos requisitos de negócios, guias de estilo ou estrutura de código. Seja qual for o motivo, vale a pena considerar que quaisquer alterações em um gerador estável correm o risco de introduzir novos bugs, que, mais uma vez, precisarão ser corrigidos. Como esses erros geralmente só podem ser detectados ao inspecionar o código gerado, o ideal seria que o gerador não precisasse passar por alterações frequentes. Isso porque o gerador é (ou pode ser) mais difícil de testar do que o próprio código gerado; portanto, mudanças rápidas nos requisitos de negócios que precisam ser refletidas na lógica dos modelos introduzem atritos adicionais.
Sua equipe está de acordo com isso?
Por último, mas não menos importante, mesmo que uma abordagem orientada a modelos pareça ser a opção perfeita para o seu caso de uso, é improvável que ela seja bem-sucedida se seu gerente e seus colegas não estiverem dispostos a se adaptar a esse fluxo de trabalho. Afinal, construir o metamodelo, o gerador e os modelos, bem como mantê-los, é uma carga adicional que aumenta a complexidade e exige que as pessoas compreendam as tecnologias utilizadas. Fatores organizacionais também têm seu peso — como a origem dos modelos, quem e como eles serão criados ou atualizados, quem é responsável pelo gerador, que tipo de processo será usado para extrair a saída gerada e adicioná-la à base de código existente etc. Há muitos desafios logísticos a serem considerados em relação a essa abordagem, sem falar em conformidade e Audit! Se você estiver usando essa abordagem para ajudar a gerar código padrão, mas todos que a utilizam se comprometam a inspecionar e revisar manualmente o código gerado antes de enviá-lo para a base de código principal, deve haver relativamente pouco atrito. Por outro lado, se for uma parte integrante e obrigatória do processo de desenvolvimento com automação, é necessário um pensamento mais aprofundado e a devida diligência. De qualquer forma, as pessoas que utilizam os modelos, o gerador ou o resultado final devem estar cientes e de acordo com a abordagem.
Desenvolvimento iterativo
Suponha que você tenha refletido sobre todas as questões acima e concluído que essa abordagem é adequada para o seu caso de uso. Como você deve proceder para implementá-la? Com base no que apresentei até agora, você pode ter ficado com a impressão de que o desenvolvimento orientado a modelos é um processo do tipo “cascata”. Embora seja verdade que o metamodelo deva ser estável e seja, naturalmente, o ponto de partida, na prática há mais flexibilidade do que a literatura e as ferramentas levariam a crer.
No meu caso, certamente não comecei com o metamodelo perfeito e, mesmo agora, ele ainda tem muito espaço para melhorias. Por exemplo, uma coisa que percebi ao criar modelos é que preciso criar tipos Java embutidos para poder referenciá-los, mas esses tipos poderiam fazer parte do metamodelo. Muitos dos atributos do modelo — especialmente atributos booleanos como isRequest, isResponse, isHal, isQueryParams etc.— surgiram quando percebi que seriam necessários para o gerador. Alterações adicionais não causam incompatibilidades — geralmente é possível estender seu metamodelo com novos tipos e atributos, desde que sejam opcionais ou tenham um valor padrão razoável.
O desenvolvimento orientado por modelos continua sendo desenvolvimento de software; portanto, um processo iterativo permite que você obtenha feedback mais rapidamente, em vez de investir excessivamente na fase inicial, quando ainda há poucas informações disponíveis. Portanto, na prática, recomendo que você desenvolva o metamodelo, o gerador e um modelo de exemplo em paralelo, para que possa visualizar o fluxo de trabalho de ponta a ponta e identificar antecipadamente quaisquer parâmetros ausentes no metamodelo, bem como erros no texto gerado, em vez de só perceber isso depois de ter criado o que, no papel, parece estar “completo”.
MDE “One-Shot” ou “Pure”?
O caso de uso que apresentei na Parte 2 foi, em grande parte, uma abordagem do tipo “única”. Ou seja, uma vez que o código tenha sido gerado, não tenho mais utilidade para o gerador ou para o modelo. Por exemplo, se a especificação da API for atualizada ou se eu detectar alguns bugs no código gerado — a menos que haja problemas graves ou revisões significativas —, é provável que eu não atualize o modelo nem gere o código novamente, pois já refatorei e evoluí o código gerado. Afinal, o que importa para mim é o resultado final, e é ele que será mantido. Nesse sentido, eu não estava realmente praticando o MDE da maneira como “deveria” ser feito. O código gerado é um ponto de partida para se trabalhar, não o produto final. Portanto, alguns podem argumentar que isso não segue o espírito do modelo orientado. Isso não significa, porém, que não seja útil.
Em uma abordagem tradicional orientada a modelos, o código seria, idealmente, regenerado a partir do modelo sempre que este fosse atualizado, de modo que a base de código e o modelo estivessem sempre sincronizados. Pense da seguinte maneira: quando você escreve um programa em uma linguagem da JVM (Java, Kotlin, Groovy, Scala etc.), o que você envia para o controle de versão? O que você mantém? Com o que você se preocupa? É o código-fonte. Mas esse código-fonte não é o que importa no momento da execução; o que importa é o bytecode (ou seja, os .class arquivos). Por que nos importamos tanto com nosso código se, no fim das contas, tudo é compilado para bytecode Java? Porque o bytecode é derivado deterministicamente do código-fonte. Pode-se argumentar que o principal recurso nessas linguagens é o compilador. Bem, um compilador é essencialmente um transformador: seu código-fonte é o modelo (em conformidade com o metamodelo da linguagem), o compilador é o gerador e o bytecode é a saída do gerador (já escrevi sobre isso antes, caso você esteja curioso). Da mesma forma, o código gerado em “MDE puro” deve ser tratado da mesma maneira que o código compilado: necessário, mas não algo que precisemos examinar ou com o qual precisemos nos preocupar muito. Os modelos do gerador são, essencialmente, o “código-fonte” no MDE.
É importante refletir sobre isso juntamente com as questões que levantei anteriormente, pois manter manualmente a sincronização entre o modelo e o código-fonte é uma fonte de atrito e de possíveis erros. Se você adotar uma abordagem mais pragmática, na qual deseja obter um código inicial e não planeja reutilizar o gerador com o mesmo modelo, então isso se torna uma preocupação menor. Além disso, suponha que o código gerado não precise de muitas (ou nenhuma) modificações manuais. Nesse caso, você pode atualizar o modelo ou o gerador ao identificar erros no código de saída e regenerá-lo com o mínimo de esforço manual. Por outro lado, se o código de saída precisar ser modificado substancialmente à mão após a geração, provavelmente não vale a pena atualizar o modelo e regenerá-lo, pois você terá que conciliar as alterações manuais com o código gerado novamente.
Se a entrada for ruim, a saída também será ruim
Isso me leva ao próximo ponto. Você só obtém dessa abordagem o que investir nela. Isso vale tanto para o(s) modelo(s) quanto para o gerador. Se você economizar esforços ao construir seu modelo, deixando campos não essenciais em branco, o código gerado não poderá ter o resultado desejado. No meu exemplo, às vezes fui preguiçoso com a documentação e os valores usados para testes, pois achei que precisaria ajustá-los de qualquer maneira. Mas quando dediquei mais esforço, descobri que precisava fazer menos modificações. Como mencionado anteriormente, você sempre pode complementar ou ajustar seus modelos do gerador caso perceba que são necessárias funcionalidades adicionais ou haja erros de formatação. Dedicar tempo para garantir que seu modelo esteja correto e o mais completo possível em relação às suas necessidades reduzirá os erros e a necessidade de executar o gerador novamente. Isso é especialmente importante em abordagens pontuais ou quando o código gerado é editado manualmente após a geração inicial.
O Princípio de Pareto se aplica
A regra 80/20 se aplica perfeitamente à geração de código orientada por modelos, na minha experiência, e reforça a importância do desenvolvimento iterativo. É possível obter cerca de 80% dos benefícios com 20% do esforço. Se você começar com modelos simples, nos quais gera até mesmo apenas o esqueleto básico das classes de que precisará, já terá escrito boa parte do código. Você tem as classes com os nomes corretos no pacote certo, com o cabeçalho de direitos autorais, importações típicas, métodos, declarações de campos, construtores etc. Você pode então, gradualmente, transferir a lógica mais complexa escrita manualmente — caso a considere repetitiva — para o gerador.
Comece sempre pelas tarefas mais fáceis e não tente fazer tudo de uma vez. Descobri que isso vale especialmente para a geração de código de teste: escrever um gerador para criar apenas as classes de teste e os stubs de métodos com as asserções já preparadas economizou muito tempo. Assim, pude me concentrar em escrever manualmente a lógica essencial dos testes, sem me preocupar com o código padrão. Isso também adiciona estrutura ao seu fluxo de trabalho, de modo que você sabe exatamente o que precisa ser feito, sem se sentir sobrecarregado pela necessidade de lidar com todos os pequenos detalhes. Você ficaria surpreso com o quanto isso, por si só, pode aumentar sua motivação e produtividade!
Por outro lado, é importante equilibrar isso com os aspectos práticos da manutenção e evolução da base de código. Se sua prioridade for manter o código gerado, em vez do gerador e dos modelos, não complique demais o gerador, pois você acabará tornando-o exponencialmente mais complexo em troca de benefícios muito marginais. Isso torna o gerador mais intimidador e difícil de trabalhar, o que dificulta a integração de novas pessoas. Como regra geral, tente fazer com que cada modelo do gerador tenha uma aparência semelhante à saída gerada. Suponha que seus modelos consistam mais em seções dinâmicas com lógica de ramificação complexa, em vez de texto estático. Nesse caso, provavelmente vale a pena separar essa lógica em métodos utilitários ou simplificar o modelo para torná-lo mais legível.
De qualquer forma, vale a pena ter em mente que não há nenhuma “mágica” nisso: criar e manter o fluxo de trabalho do MDE, juntamente com suas ferramentas, representa uma sobrecarga adicional. Esteja ciente de quanto tempo você está investindo nisso em relação à economia que está obtendo. E isso nos leva de volta ao ponto de partida: justificar a razão por trás disso.
Conclusão
Com isso, chegamos ao fim desta minissérie de posts sobre engenharia orientada a modelos pragmática. Espero que vocês tenham aprendido não apenas algumas novas ferramentas e uma abordagem de desenvolvimento, mas também estejam cientes de suas limitações e de como aproveitar ao máximo essas tecnologias. Em última análise, espero que esta introdução à engenharia orientada a modelos e ao estudo de caso sirva como um exemplo útil e ajude vocês a tomar melhores decisões em seus projetos de software, caso decidam seguir esse caminho.
Se você tiver algum comentário ou sugestão, sinta-se à vontade para entrar em contato conosco no X, anteriormente conhecido como Twitter ou dê uma passada no nosso Slack da Comunidade. Espero que este artigo tenha sido útil e agradeço quaisquer comentários ou opiniões. Se você gostou, confira meus outros artigos sobre Java.
Compartilhar:
Sina é um ex-membro da equipe da Vonage. Ele atuou como Java Developer Advocate na Vonage. Com formação acadêmica, ele tem uma curiosidade geral por tudo o que se relaciona a carros, computadores, programação, tecnologia e natureza humana. Em seu tempo livre, ele gosta de caminhar ou jogar videogames competitivos.