Faturando o débito técnico

Um tempo atrás, o homem, o mito, a lenda Fabrício Buzeto fez esta esta interessante pergunta:

Out of curiosity. Does your team keep a list of technical debt? Does it make you feel joy?
Só por curiosidade. Seu time mantém uma lista de débitos técnicos? Isso faz vocês felizes?

Isso me fez lembrar algumas coisas. Por alguns anos, fui responsável pelos portlets Liferay Calendar e Kaleo Designer. Eram aplicativos complexos, construídos em ritmo acelerado quando o conceito de SPAs ainda estava em evolução: muitas escolhas exigiam uma revisão.

Então comecei a escrever tíquetes do JIRA para débitos técnicos. Quando um desses problemas tornava uma demanda mais difícil, eu convertia esse tíquete em uma subtarefa da demanda. Como gosto de dizer, eu estava “faturando o débito na feature“.

Comentei isso e então ele me fez uma pergunta crucial:

Why not treat them like any other card in the backlog then?

De fato, por quê?

Bem, a princípio, nós tentamos! Eu apresentava as issues de débito técnico em nossas reuniões de priorização. Ter os problemas escritos ajudou muito a chamar a atenção dos gestores, aliás.

Débito técnicos são um peixe difćil de vender, no entanto. As pessoas são compreensivelmente cautelosas ao investir em algo cujo valor não é evidente. Ainda assim, as alterações demoravam cada vez mais para serem entregues e os bugs de regressão continuavam aparecendo. Precisávamos corrigir a raiz dos problemas.

É por isso que comecei a trabalhar em débitos como parte das tarefas que agregam valor. Quando consertávamos um débito para facilitar uma demanda, ficava evidente de que o trabalho extra valia a pena. Aquela refatoração não foi apenas uma ideia aleatória: ela nos trouxe valor.

Essa é a primeira razão para lidar com débito técnico como subtarefas de outras demandas: ao vincular a dívida a uma tarefa que entrega valor, é mais fácil justificar o esforço extra para as partes interessadas.

No início, esse “faturamento de débito” era apenas um dispositivo de comunicação. Mas houve um efeito colateral interessante: os problemas mais gritantes eram naturalmente resolvidos primeiro. Faz sentido: como trabalhávamos neles quando causam problemas, os que causassem mais problemas eram resolvidos primeiro. Como priorização é sempre um desafio (e priorizar débito técnico é ainda mais difícil), isso foi muito útil!

Ainda tínhamos uma pilha de tarefas de débito, mas muitas já não eram relevantes. Algumas, já haviam sido resolvidas. Outras eram ideias elegantes no passado, mas não faziam mais sentido. Em retrospectiva, boa parte do “débito” eram preferências pessoais, ou suposições que não eram mais verdadeiras após alguma evolução do produto.

Esta é a segunda razão para o faturamento da dívida: trabalhar na “saúde do código” como parte de demandas é uma maneira eficaz de priorizar em qual débito merece o esforço.

Veja que ótimo! Tivéssemos resolvido o débito técnico sozinho — por exemplo, em uma força-tarefa —, talvez fizéssemos mudanças que poderiam, de fato, dificultar a evolução futura. A cobrança de dívidas permitiu-nos confirmar que solicitações se adequavam aos nossos objetivos. E houve uma consequência mais sutil e importante.

Nós, desenvolvedores, temos opiniões fortes, e isso é bom. Geralmente tentamos transformar essas opiniões em um objetivo. Mas é difícil saber se um objetivo é o correto. Uma vez que usamos essas ideias para ajudar em algo mais claramente relevante, esse objetivo se transforma em uma ferramenta. Ferramentas são muito mais fáceis de avaliar!

Esse é um terceiro motivo para faturar o débito: quando o débito técnico está atrelada à entrega de valor, a força criativa da equipe se alinha com os objetivos da organização.

Nossa experiência com essa estratégia foi bastante eficaz. Todos sabiam que suas sugestões seriam avaliadas: as tarefas de saúde não seriam mais uma obrigação a priorizar, mas um conjunto de ferramentas que nossos colegas buscariam para ajudar em seus desafios O backlog de débito técnico não era mais apenas um poço dos desejos.

Os aplicativos também ficaram melhores. Quando comecei a trabalhar no Calendário, por exemplo, ele era visto como um portlet especialmente problemático. O primeiro lançamento não podia agendar eventos! Quando saí daquele time, o Calendário não tinha bug de prioridade 3 ou superior (níveis que temos que corrigir). E entregamos uma boa quantidade de recursos, mesmo alguns ausentes nos líderes da concorrência. Nada mal para um produto que fora um exemplo de como não funcionar!

Por tudo isso, sempre me pareceu bom pagar o débito técnico como parte das demandas, mas nunca pensei muito sobre por que. Então, obrigado pela pergunta, Fabricio! Foi uma prazer pensar nisso.

PS: Acabei de lembrar que Ron Jeffries escreveu um excelente post sobre quando refatorar. Ele na verdade é contra algo que defendi aqui, mas vi bastante semelhanças, e naturalmente ele explica muito melhor. Vale muito a leitura!

(Esta é uma tradução de Billing the Technical Debt, um post em Suspension of Disbelief.)

Importando Módulos ES6 em CommonJS

Aqui na Liferay, alguns dias atrás, necessitávamos utilizar o pacote p-map. Só tinha um problema: esta aplicação em específico utilizava módulos no padrão CommonJS, e p-map utiliza módulos ES6. Até algumas das melhores referências que encontrei (por exemplo, este post) deixavam claro que não seria possível importar módulos ES6 a partir de CommonJS.

A boa notícia é que isto não é mais verdade! Usando import dinâmico, podemos carregar módulos ES6 a partir de CommonJS. Vejamos um exemplo.

Neste projeto, o arquivo importer.js tenta utilizar require() para importar um módulo ES6:

const pmap = require('p-map');

exports.importer = () => {
  console.log('Yes, I could import p-map:', pmap);
}Code language: JavaScript (javascript)

Naturalmente, isto não funciona:

$ node index.js 
internal/modules/cjs/loader.js:1102
      throw new ERR_REQUIRE_ESM(filename, parentPath, packageJsonPath);
      ^

Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: /home/adam/software/es6commonjs/node_modules/p-map/index.js
require() of ES modules is not supported.
require() of /home/adam/software/es6commonjs/node_modules/p-map/index.js from /home/adam/software/es6commonjs/importer.js is an ES module file as it is a .js file whose nearest parent package.json contains "type": "module" which defines all .js files in that package scope as ES modules.
Instead rename index.js to end in .cjs, change the requiring code to use import(), or remove "type": "module" from /home/adam/software/es6commonjs/node_modules/p-map/package.json.

    at new NodeError (internal/errors.js:322:7)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1102:13)
    at Module.load (internal/modules/cjs/loader.js:950:32)
    at Function.Module._load (internal/modules/cjs/loader.js:790:12)
    at Module.require (internal/modules/cjs/loader.js:974:19)
    at require (internal/modules/cjs/helpers.js:101:18)
    at Object.<anonymous> (/home/adam/software/es6commonjs/importer.js:1:14)
    at Module._compile (internal/modules/cjs/loader.js:1085:14)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1114:10)
    at Module.load (internal/modules/cjs/loader.js:950:32) {
  code: 'ERR_REQUIRE_ESM'
}
Code language: Bash (bash)

A solução é converter o require() em um import dinâmico. Mas tem um detalhe: import dinâmicos retornam uma Promise. Há várias maneiras de lidar com isso; a mais simples provavelmente é tornar nossa função assíncrona, como nessa versão:

exports.importer = async () => {
  const pmap = await import('p-map');
  console.log('Yes, I could import p-map:', pmap);
}Code language: JavaScript (javascript)

Agora nossa pequena aplicação funciona!

$ node index.js 
ok
Yes, I could import p-map: [Module: null prototype] {
  AbortError: [class AbortError extends Error],
  default: [AsyncFunction: pMap],
  pMapSkip: Symbol(skip)
}Code language: Bash (bash)

Outros ajustes podem ser necessários; eu mesmo precisei ajustar as configurações do eslint. O importante é que isto é possível. E não é uma gambiarra: a própria documentação de Node recomenda essa abordagem.

Então, não se assuste com informações desatualizadas: você não vai precisar reescrever sua aplicação toda como módulos ES 6, ao menos por enquanto.

Bandeiras de mobilização e botões vermelhos

É melhor trabalhar numa organização hierarquizada, top-down, ou numa mais solta, bottom-up? Depois de alguns anos, aprendi que ambos os estilos têm vantagens e desvantagens, funcionam e podem ser igualmente agradáveis.

Naturalmente, em ambos os estilos, a boa liderança depende de uma miríade de fatores. Como contribuidor individual, porém, notei duas atitudes de meus superiores que fazem uma diferença especial.

Nas organizações bottom-up é bem comum contratar pessoas competentes e deixá-las fazer sua mágica. Nesses cenários, sempre me ajudou muito o líder desenhar bem a missão. Não é definir o que vai ser feito, mas sim objetivos concretos com o qual podemos nos conectar. Sem isso, o subordinado pode se perder em tarefas irrelevantes. Já aconteceu muito comigo: dediquei muito esforço a bugs mínimos.

Isso é algo que tende a não acontecer em ambientes hierarquizados. Mesmo que nos dediquemos a algo irrelevante, a “culpa” não será nossa. Por isso mesmo, porém, frequentemente recebemos instruções francamente perigosas. Eu lembro de uma funcionalidade que me solicitaram. Eu sabia claramente como fazer — já tinha feito muitas similares — mas meu chefe me ordenou outra abordagem. Soube que, anos depois, após perdas de dados e duas reescritas, fizeram exatamente o que eu faria no primeiro dia.

Enfim, se você for dar liberdade, dê também uma missão clara, uma bandeira de mobilização que nos guie e nos inspire a tentar. Se for dar instruções rigidamente, disponibilize um botão vermelho para alertas que venham de baixo. Liderança vai além disso, mas essas duas atitudes já ajudam bastante.

Dois problemas num Jenkinsfile

Ah, as sutilezas do shell script…

Já usei vários servidores Jenkins aqui na Liferay, sempre como desenvolvedor. Na Liferay Cloud, porém, configurar e rodar Jenkins é uma tarefa rotineira. Resolvi então reforçar meu conhecimento seguindo os passos de Guided Tour to Jenkins. Foi quando aconteceu uma coisa curiosa.

O processo de build no Jenkins é definidos por arquivos Jenkinsfile. Um Jenkinsfile contém, entre outras coisas, vários estágios do processo de build. Cada estágio possui um ou mais passos (isto é, comandos a serem executados).

A sessão sobre passos múltiplos me apresentou o retry (que executa um um passo até ele ser bem-sucedido) e timeout (que limita o tempo que um passo pode tomar). Para testá-los, escrevi o seguinte no meu Jenkinsfile:

pipeline {
  agent any
  stages {
    stage('build') {
      steps {
        retry(3) {
          sh '''
            R=$(od -An -N1 -i /dev/random)
            echo trying flakey... 
            [ $((R % 3)) -eq 0 ] && exit 1
          '''
        }
        timeout(time: 5, unit: 'SECONDS') {
          sh '''
            R=$(od -An -N1 -i /dev/random)
            sh 'echo trying slow...
                      [ $((R % 3)) -eq 0 ] && sleep 10
          '''
         }
       }
    }
  }
}Code language: PHP (php)

Mesmo que você não conheça Jenkinsfiles, é possível ter uma ideia do que se passa:

  • No retry(3), coloquei um script shell que, a cada três execuções, falha. Assim, poderia ver Jenkins fazendo novas tentativas de executá-lo.
  • No passo de timeout(5, SECONDS) há um script que, a cada três execuções, demora dez segundos para terminar. Como há um tempo-limite de 5 segundos, essas execuções demoradas vão falhar.

Mandei o aquivo para GitHub configurei Jenkins para “buildar” o repositório e… todos os builds falharam! Isto não estava certo, eu esperava que o build falhasse pouco mais de um terço das vezes. O que estava acontecendo?

O problema é que, na minha cabeça, a condição do teste [ $((R % 3)) -eq 0 ] && exit 1 seria verdadeira um terço das vezes, fazendo com que o script encerrasse com status 1. (Todo status não-nulo é considerado falha.) Só que, quando a condição era falsa, o próprio teste falhava. Era o status do teste que Jenkins verificava!

Entendido o problema, foi fácil resolver: adicionei, ao final do script, um operador OU. Por quê? Porque se o teste passar, o exit 1 é executado e o script falha; se o teste não passar, porém, o sh vai executar o comando depois do ||, que sempre vai retornar zero.

pipeline {
  agent any
  stages {
    stage('build') {
      steps {
        retry(3) {
          sh '''
            R=$(od -An -N1 -i /dev/random)
            echo trying flakey... 
            [ $((R % 3)) -eq 0 ] && exit 1 || echo 'flakey ok'
          '''
        }
        timeout(time: 5, unit: 'SECONDS') {
          sh '''
            R=$(od -An -N1 -i /dev/random)
            sh 'echo trying slow...
            [ $((R % 3)) -eq 0 ] && sleep 10 || echo 'slow ok'
          '''
        }
      }
    }
  }
}Code language: PHP (php)

Funcionou! Exceto que agora o build raramente falhava, e nunca por tempo excedido. Por que será?

É que concatenei || echo 'slow ok' também na frente do passo do timeout. O script desse passo de fato levava 10 segundos para executar um terço das vezes, e Jenkins de fato cancelava o script.

O problema é que Jenkins “mata” o comando sleep 10, que então retorna um status de erro. Só que o echo 'slow ok' também “come” esse status de falha quando o build é cancelado. Por isso, mesmo quando abortado, o build aparecia como bem-sucedido. Por outro lado, eu não poderia simplesmente deixar o teste falhar, como antes.

A solução é aquela tão comum: parar de usar truquezinhos engenhosos e escrever código claro. Troquei os && e || por um condicional if. Convenhamos que até mais legível ficou, né?

pipeline {
  agent any
  stages {
    stage('build') {
      steps {
        retry(3) {
          sh '''
            R=$(od -An -N1 -i /dev/random)
            echo trying flakey... 
            [ $((R % 3)) -eq 0 ] && exit 1 || echo 'flakey ok'
          '''
        }
        timeout(time: 5, unit: 'SECONDS') {
          sh '''
            R=$(od -An -N1 -i /dev/random)
            sh 'echo trying slow...
            if [ $((R % 3)) -eq 0 ] ; then 
              sleep 10
            fi
          '''
        }
      }
    }
  }
}Code language: PHP (php)

Às vezes sinto que estamos na era CVS das ferramentas de CI, aguardando um Subversion que simplifique nossa vida. Mas confesso estar feliz que tenhamos, hoje essas ferramentas disponíveis. Por ora, vou focando em aprender com esses incidentes.

Busca textual no Liferay Portal com Elasticsearch – Parte 2: Arquitetura

Quando migramos de Lucene para Elasticsearch, Liferay manteve o mesmo processo: ao salvar uma entidade, indexamos seus campos. Só que, agora, no Elasticsearch.

Contudo, Elasticsearch não é uma biblioteca: é um sistema servidor altamente distribuído, rodando em clusters. Isto nos forçou a tomar novas decisões, e nos presenteou com algumas lições valiosas. Para entendê-las, convém revisar como Elasticsearch pode ser usado.

O modelo de programação de Elasticsearch

No Elasticsearch, os dados são salvos como pares de chave-valor, agrupados em documentos. Os documentos representam as instâncias das entidades.

Um índice contem vários documentos. Para efeitos práticos, todos os documentos de um índice são mesmo tipo (ao contrário de SGBDs, que aceitam várias tabelas em um banco de dados). O tipo define que campos são encontrados nos documentos e é usualmente dinâmico, permitindo que campos novos sejam adicionados por mera inserção.

Vários índices para vários portais

É muito comum trabalhar, no Elasticsearch, com vários índices diferentes em um mesmo cluster, para o mesmo formato de dados. Como seriam os índices do Liferay. Para responder isso, é necessário ter em mente que Liferay é multi-tenant.

Sabe quando você entra, digamos, em wordpress.com ou blogspot.com e cria um blog? Seu blog é totalmente independente de qualquer outro no mesmo site. O mesmo se aplica ao Liferay Portal, que permite criar instâncias independentes em uma mesma instalação.

Os engenheiros decidiram então que cada instância teria seu próprio índice no Elasticsearch. Separar os dados desta maneira nos permitiu controlar melhor o acesso de uma instância, assim como nos deu flexibilidade para otimizar Elasticsearch.

Por outro lado, embora houvesse vários índices, todos teriam o mesmo formato. Como seria esse formato?

Tudo junto e misturado

Seguindo as recomendações da Elastic, cada índice teria um só tipo. Neste tipo, indexamos as mais diversas entidades.

Na hora em que uma entidade é salva no banco de dados, os valores de suas colunas são indexados em um só tipo de documento. Assim, o post de blog, o artigo do CMS, o comentário no fórum… todos serão representados pelo mesmo formato. O título do blog, do evento, da wiki etc. se tornarão o campo title de vários documentos. O mesmo se passa com campos como content, summary, description

Estes campos representam características comuns a várias entidades. Quando todas elas são indexadas em um só índice, com um schema uniforme, a busca se torna mais simples: basca procurar pela string nestes documentos.

Os documentos Elasticsearch também contêm bastante metadados, sendo alguns dos mais importantes o campo entryClassName (o tipo da entidade que o documento representa, geralmente o nome de uma classe Java) e entryClassPK (identificador da entidade na tabela SQL). Desta forma, podemos encontrar o que o usuário procura e, a partir daí, recuperar a entidade correta do bando de dados.

Esta arquitetura é uma herança da busca com Lucene mas, nada surpreendentemente, funcionou perfeitamente com Elasticsearch. Esta foi uma de nossas primeiras lições:

Para implementar busca texutal, junte vários tipos de unidade em um só tipo de documento.

Decidida a arquitetura dos dados, a próxima questão a resolver era: como indexar e buscar as entidades no índice da instância. Este será o tema da parte 3 desta série.

Protected: Por que eu provavelmente não usaria Haskell em um projeto profissional

This content is password protected. To view it please enter your password below:

Busca textual no Liferay Portal com Elasticsearch – Parte 1: História

Qual a aplicação indispensável de um portal web?

Liferay Portal é uma plataforma de portais web, referência de mercado, open source e em Java. Um portal web é um software que suporta várias aplicações, chamadas portlets; portlets podem inclusive estar na mesma página web. O Liferay Portal, por exemplo, inclui CMS, blog, wiki, fóruns, calendário, formulários, workflow, e-commerce e muito mais, por padrão. Ainda é possível criar suas próprias aplicações e acrescentá-las ao portal.

Por que é difícil fazer buscas em um portal

Uma aplicação obrigatória em todo portal é a pesquisa global. Usuários querem uma barra de busca simples, que procure por dados de todas as diferentes aplicações.

Isto é desafiador em um portal baseado em SQL como Liferay. Cada aplicação salva seus dados em tabelas próprias. SQL “tradicional” não é uma solução razoável para estes casos: as queries seriam imensas e difíceis de construir.

Além disso, faltam a SQL funcionalidades básicas de busca textual. Alguns dialetos de SQL possuem capacidades extras, mas Liferay Portal pode rodar em mais de quarenta SGBDs diferentes. Não poderíamos utilizar extensões de vendors.

Outra solução era necessária.

Bibliotecas como solução

Logotipo da Lucene, em que a palavra "lucene" aparece escrita em letras minúsculas cursivas, e o "l" do começo possui algumas "franjas" apontando para trás, dando a sensação de velocidade, movimento para frente.
Logotipo de Lucene

Essa outra solução era Apache Lucene, a biblioteca Java de indexação e busca. Lucene tornou-se praticamente a escolha natural quando se trabalha com pesquisa textual. Mesmo soluções mais robustas como Solr e Elasticsearch são construídos ao redor dele.

Como utilizávamos a biblioteca? Quando o Liferay salvava uma entidade buscável no banco SQL, passava seus campos para o Lucene. Lucene indexava o conteúdo (em um formato eficiente para busca) e o salvava no disco.

Havia porém um desafio. Em produção, portais Liferay sempre rodam em clusters. Como o Lucene salvava os dados no disco local, o conteúdo era indexado apenas no nó do cluster Liferay que recebeu a requisição. Para superar esta limitação, sincronizávamos os índices de Lucene com JGroups, uma biblioteca de mensageria que enviava mensagens entre os vários nós.

Utilizamos esta solução por vários anos… até adotarmos Elasticsearch.

O que é Elasticsearch?

Logotipo atual do Elasticsearch, em que três figuras geométricas formam um "E" meio arredondado.
Logotipo atual do Elasticsearch. (Confesso que achava o antigo mais fofinho.)

Resumidamente, Elasticsearch é banco de dados NoSQL criado com ênfase em para busca textual.

Baseado em Lucene, Elasticsearch é extremamente escalável, distribuído e simples. Hoje, além de ser quase um padrão de facto para busca textual, Elasticsearch é popular para análise de dados, monitoramento e segurança, especialmente em combinação com o resto da Elastic Stack. Atualmente utilizamos para muitas destas outras funções na Liferay, mas hoje focaremos aqui na busca textual.

As razões da migração

Por que abandonamos nossa solução e adotamos Elasticsearch? Por várias razões.

  1. Lucene é uma biblioteca de baixo nível, o que tornava a manutenção do código mais trabalhosa e cara. Elasticsearch é mais poderoso, simples e flexível.
  2. Elasticsearch roda e suas próprias VMs e cluster, enquanto Lucene rodava na JVM de Liferay. Mover a busca para fora das VMs do Liferay reduziu os recursos necessários nos servidores Liferay.
  3. As JVMs separadas do Elasticsearch também permite otimizá-las para busca sem conflitos com as configurações ideais para portal das VMs do Liferay.
  4. Clusterização e sincronização já eram partes de Elasticsearch. Pudemos deletar milhares de linhas de código!

Além de tudo, Elasticsearch se tornava padrão de mercado. Foi uma escolha natural.

Neste processo, aprendemos muito sobre Elasticsearch. Veremos um pouco do que descobrimos na próxima parte.

Ajustando a Cartela de Kanban em Validação

Em Kanban, não se pode voltar uma tarefa pra fases anteriores. Como toda regra, há situações em que não se aplica. Quais? Veja nesse excelente post do grande Eduardo Zoby!

Um exemplo em especial me fez pensar.

Uma pulga atrás da orelha

O que fazer quando bugs críticos são encontrados em uma tarefa que satisfaz os critérios para revisão? Zoby explica que o time deixa a cartela em validação. Enquanto isso, subtarefas são criadas e executadas.

É uma maneira correta e funcional de modelar o processo. Como desenvolvedor, porém, sinto certo incômodo com este cenário. Por razões bobas, confesso, mas interessantes:

  1. Trabalhar em algo que está “em validação” soa impreciso. Nós estamos desenvolvendo quando corrigimos falhas.
  2. Pessoalmente, fico ansioso quando tenho de esperar pelo resultado de uma validação. Não há diferença prática, mas há uma psicológica. Temos agência sobre o trabalho que estamos desenvolvendo, mas não sobre o trabalho nosso que estão revisando.
  3. Falta clareza na posse da tarefa. Quem é o responsável, o desenvolvedor ou o validador? O tíquete define a quem está atribuído, mas a própria coluna acaba sendo dividida entre dois papeis.

Mas… como seria uma alternativa?

Imaginando uma fábrica

Sendo Kanban inspirado por processos de manufatura, talvez um retorno às origens pudesse oferecer alguma alternativa.

Me imaginei (porque não sei nada de engenharia de produção) em uma fábrica de carros. O que acontece com um automóvel em montagem se um de seus componentes se provou defeituoso? O carro não volta na esteira de produção, mas tampouco vai ser testado. Mais crucialmente, o componente também não volta para ser consertado: ele é descartado.

Não dá para mapear um processo industrial para engenharia de software, mas a ideia de descartar o componente é instigante. Descartar os “componentes” da nossa história é um bom caminho.

Não se conserta o amortecedor enquanto se testa o carro

Como seriam os “componentes” de nossa cartela. Seriam subtarefas, mas apenas se satisfizerem duas condições:

  1. são bem definidas o suficiente para serem avaliáveis;
  2. são pequenas o suficiente para que possam ser descartadas.

Durante o desenvolvimento, subtarefas seriam criadas prodigamente, inclusive e especialmente se a tarefa estiver “pronta” (naquele sentido obscuro que nós programadores damos ao termo). Uma vez terminada, cada subtarefa é enviada para validação; caso falhe, é fechada e outra subtarefa é aberta. Enquanto isso, a história continua em desenvolvimento.

Imagino várias vantagens neste processo:

  1. as alterações são revisadas e mergeadas gradativamente — bem à moda do trunk-based development.
  2. regressões seriam descobertas mais perto da mudança que as causou.
  3. estatísticas seriam provavelmente mais precisas: trabalho em bugs descobertos não seria mais computado como validação.

Certamente há pontos em aberto. O que fazer, por exemplo, se ao final a história não for aprovada? Há que se pensar. Eu tornaria a própria a revisão em uma subtarefa, mas nunca fiz isso. Como não sou engenheiro de produção, não sei o que fazem com os carros que falham ao final — talvez haja alguma inspiração la!

Um caso de uso peculiar do passado remoto

Isso tudo é um exercício mental, mas à medida que pensava, lembrei que já fiz algo similar.

Cuidei do Calendário da Liferay por anos, como uma “euquipe.” Comunicar-me com todos os stakeholders e codificar ao mesmo tempo era um desafio. Resolvi isto criando muitas subtarefas — alguns tíquetes chegaram a ter mais de trinta. Enquanto isso, os bugs e histórias permaneciam em desenvolvimento.

Às vantagens acima, somaram-se outras:

  1. os pull requests menores que eram revistos rapidamente.
  2. os stakeholders podiam ver o que eu estava fazendo, se quisessem.

Para um componente não-estratégico, estes detalhes foram fundamentais!

Vale a pena?

Deixar em validação ou em desenvolvimento a tarefa a corrigir é um detalhe semântico. Ainda assim, acho que pensar no assunto pode ser produtivo. Como não sou agilista, não tenho tanto know-how em definir workflows de times, mas o ponto de vista dos contribuidores individuais podem ser úteis. Quem sabe este aqui não é?

A Evolução da Carreira Remota

Como o profissional remoto pode crescer na sua carreira? Sendo o trabalho remoto uma revolução recente, esta é uma das questões complexas que se apresentam. No geral, os empregados de colarinho branco tendem a crescer mais quando mudam de empresa, e na minha experiência isto é até mais comum em ambientes remotos. Contudo, é possível crescer na mesma carreira como trabalhador remoto — se a empresa fizer seu dever de casa. Desde que comecei a comunidade Hipótese Remota, conheci várias destas empresas remote-first que trabalham pesado para evoluir seus colaboradores, e resolvi buscar um pouco de seu conhecimento.

Tudo azul na carreira

Convidei meu velho amigo da UnB, Fabricio Buzeto, co-founder da bxblue, uma startup fintech daqui de Brasília que cresce a olhos vistos, para um café no dia 5 de outubro de 2020. Ele me contou como funciona o plano de carreira da bxblue: “Não fazemos nada diferente das empresas presenciais. Temos avaliações periódicas e um calendário de promoção.”

No caso deles, há duas partes no plano de carreiras: um plano de cargos e salários, que reconhece como o profissional cresceu, e um plano de competências, que irá ajudá-lo ainda mais. Cada departamento tem critérios de avaliação. Por exemplo o call center da área de atendimento tem métricas de fechamentos, a engenharia de software não tem.

É importante que os critérios sejam claros, objetivos e, notavelmente, coletivos. “Na bx, a métrica é a média do time,” me contou Fabrício. “A nossa área de atendimento, hoje, é dez vezes mais produtiva que o melhor atendente do passado.” Essa estratégia, que foca na equipe e não no indivíduo, facilita encontrar os reais problemas por trás das métricas. “Se os atendentes estão convertendo pouco, que habilidade falta para converter mais? O software pode estar apresentando uma proposta inviável. Ou o atendente pode demorar a ligar, ou não consegue ligação e não busca por outros meios.”

Planejamento distribuído para carreiras distribuídas

Instigado por conhecer o plano de carreira da bxblue, resolvi conversar com outras empresas. Lembrei da minha querida amiga Karina Varela da Red Hat — lembram das dicas genias dela sobre trabalho em casa com família? Ela havia me contato como, sendo uma corporação que cresceu do free software dos anos 90 e 2000, a Red Hat sempre foi internacional, distribuída e, portanto, remote-first. Marquei outro cafezinho com ela e sua superior, Glauce Santos, gerente de aquisições da América Latinha da Red Hat, para o dia 8 de outubro de 2020. Perguntei: como é o plano de carreira na RH?

Para minha surpresa, eles não tinham!

Glauce me explicou que o desenvolvimento na Red Hat é muito mais localizado. “Nós não temos um plano de carreira, como numa Big Four. Nós temos uma cultura aberta e avaliação de desempenho individualizada, com o gestor.” Neste caso, o principal responsável é o próprio colaborador. “A responsabilidade fica na mão do funcionário” informa Glauce. Para isso, o suporte do gestor é fundamental, conforme nos conta Karina: “O gerente ajuda o colaborador a chegar onde ele quer.”

Embora seja uma abordagem bem diferente da bxblue, há uma semelhança: critérios são definidos pelas áreas e times. “O consultor é avaliado pela satisfação do cliente, ou talvez pelas horas trabalhadas. No suporte se vê quantos chamados foram atendidos, e quantos SLAs foram cumpridos. Os times de venda têm metas“, Karina me disse. Glauce complementa: “Empregados são avaliados por responsabilidades principais, goals, metas e objetivos. E há um plano de desenvolvimento para cada um, desenvolvido em conjunto com o gestor.”

Crescendo para os lados

Um dos pontos mais interessantes da conversa foi sobre algo que também é incentivado aqui na Liferay: mudar de funções e times. Eu mesmo, por exemplo, já mudei de time várias vezes. Isto ocorre tanto na bxblue quanto na Red Hat.

“Nós somos incentivados a mudar de times através de processos seletivos internos,” Karina me contou. O lado bom disso é que, quando não há vagas ou orçamento para promoções, o empregado pode se desenvolver expandindo os horizontes. Complementa Glauce: “Na RH, sempre tem oportunidade. Às vezes não temos o budget ou o ‘próximo passo’, mas sempre tem mais responsabilidades. Há carreiras verticais, horizontais e em Y, é possível mudar de área, virar especialista, etc.”

Movimentações laterais são uma solução para crescimento de carreira? Em minha opinião, podem ser uma boa ferramenta complementar, mas não substituem, naturalmente, as promoções. Tanto o colaborador quanto o RH precisam ter ciência de que isto não substitui crescimento. Por outro lado, acredito que pode ajudar bastante. Eu mesmo já resolvi, com mudanças de time ou área, problemas que acreditava necessitar de uma promoção. Ainda fui atrás da promoção, mas a mudança foi uma lufada de ar fresco.

Sumário

Hoje, até mais que à época, as empresas têm de se esforçar mais para manter seus colaboradores. Com cada vez mais empresas adotando o remote-first, o desafio é ainda maior. Planos de carreira bem definidos, como os da bxblue, são um grande diferencial para manter profissionais. Não são obrigatórios, porém, como o modelo distribuído da Red Hat provou. Mudanças de time e área também são úteis mas, pessoalmente, creio ser necessário estar atento para evitar estagnação.

E você, o que acha? Deixe seu comentário aí!

Não me Interprete Mal: Improvisando Testes para um Interpretador

Estou amando ler o livro Crafting Interpreters. Nele, Bob Nystrom nos ensina como escrever um interpretador implementando uma pequena linguagem de programação chamada Lox. Há muito tempo não me divertia tanto programando! Além de bem escrito, o livro é engraçado e ensina bem mais coisas do que eu esperava. Mas estou tendo um problema.

Os snipptes no livro são escritos para copiar e colar. Contudo, o livro tem desafios ao final de cada capítulo, estes desafios não têm código-fonte e por vezes nos forçam a alterar muito o interpretador. Eu faço todos estes exercícios, e como resultado meu interpretador é diferente demais do código mostrado no livro. Como consequência, várias vezes quebro alguma parte do interpretador.

Como resolver isso?

Testes unitários seriam frágeis, já que a estrutura do código muda frequentemente. Testes de fim a fim parecem mais práticos nesse caso. Assim, para cada nova funcionalidade da linguagem, escrevi um programinha. Por exemplo, meu interpretador deve criar clausuras, e para garantir isso copiei o programa em Lox abaixo para o arquivo counter.lox:

fun makeCounter() {
  var i = 0;
  fun count() {
    i = i + 1;
    print i;
  }

  return count;
}

var counter = makeCounter();
counter(); // "1".
counter(); // "2".

O resultado deste programa devem ser os números 1 e 2 impressos em linhas separadas. Então coloquei esses valores em um arquivo chamado counter.lox.out. O programa também não pode falhar, então criei um arquivo vazio chamado counter.lox.err. (Em alguns casos é preciso garantir que o programa Lox falhará. Nesses casos, o arquivo .lox.err deve ter conteúdo.)

Pois bem, escrevi programas e arquivos de saída para vários exemplos. Agora preciso comparar os resultados dos programas com as saídas esperadas. Resolvi então usar a ferramenta que mais me ajuda nos momentos de urgência: shell script. Fiz um script Bash com um laço for iterando sobre todos os exemplos:

for l in examples/*.lox
do

done

Para cada exemplo, executei o programa Lox, redirecionando as saídas para arquivos temporários:

for l in examples/*.lox
do
  out=$(mktemp)
  err=$(mktemp)
  java -classpath target/classes/ br.com.brandizzi.adam.myjlox.Lox $l > $out 2> $err
done

Agora, comparamos a saída real com a saída esperada com diff. Quando compara dois arquivos, diff returna 0 se não há diferença, 1 se existe alguma diferença, ou 2 em caso de erro. Como em Bash o condicional if considera 0 como verdadeiro, basta checar a negação do retorno de diff.

Se o programa imprimir algo na saída-padrão diferente do que está no arquivo .lox.out, temos uma falha:

for l in examples/*.lox
do
  out=$(mktemp)
  err=$(mktemp)
  java -classpath target/classes/ br.com.brandizzi.adam.myjlox.Lox $l > $out 2> $err

  if ! diff $l.out $out
  then
    FAIL=1
  fi
done

Também comparamos a saída de erro com o arquivo .lox.err:

for l in examples/*.lox
do
  out=$(mktemp)
  err=$(mktemp)
  java -classpath target/classes/ br.com.brandizzi.adam.myjlox.Lox $l > $out 2> $err

  if ! diff $l.out $out
  then
    FAIL=1
  fi

  if ! diff $l.err $err
  then
    FAIL=1
  fi
done

Por fim, verifico se houve alguma falha e reporto o resultado:

for l in examples/*.lox
do
  out=$(mktemp)
  err=$(mktemp)
  java -classpath target/classes/ br.com.brandizzi.adam.myjlox.Lox $l > $out 2> $err

  if ! diff $l.out $out
  then
    FAIL=1
  fi

  if ! diff $l.err $err
  then
    FAIL=1
  fi

  if [ "$FAIL" = "1" ]
  then
    echo "FAIL" $l
  else
    echo "PASS" $l
  fi
done

Nem todos os meus programas em Lox podem ser checados, porém. Por exemplo, há um programa que cronometra a execução de loops, é impossível prever o valor que ele vai imprimir. Por isso, adicionei a possibilidade de pular alguns programas: basta criar um arquivo com a extensão .lox.skip:

for l in examples/*.lox
do
  if [ -e $l.skip ]
  then
    echo SKIP $l
    continue
  fi

  out=$(mktemp)
  err=$(mktemp)
  java -classpath target/classes/ br.com.brandizzi.adam.myjlox.Lox $l > $out 2> $err

  if ! diff $l.out $out
  then
    FAIL=1
  fi

  if ! diff $l.err $err
  then
    FAIL=1
  fi

  if [ "$FAIL" = "1" ]
  then
    echo "FAIL" $l
  else
    echo "PASS" $l
  fi
done

Se, porém, tenho um exemplo em Lox e ele não possui os arquivos de saída esperada (nem o arquivo .lox.skip) então tenho um problema e o script inteiro falha:

for l in examples/*.lox
do
  if [ -e $l.skip ]
  then
    echo SKIP $l
    continue
  elif [ ! -e $l.out ] || [ ! -e $l.err ]
  then
    echo missing $l.out or $l.err
    exit 1
  fi

  out=$(mktemp)
  err=$(mktemp)
  java -classpath target/classes/ br.com.brandizzi.adam.myjlox.Lox $l > $out 2> $err

  if ! diff $l.out $out
  then
    FAIL=1
  fi

  if ! diff $l.err $err
  then
    FAIL=1
  fi

  if [ "$FAIL" = "1" ]
  then
    echo "FAIL" $l
  else
    echo "PASS" $l
  fi
done

Com isto, meu script testador está pronto. Vejamos como se comporta:

$ ./lcheck.sh
PASS examples/attr.lox
PASS examples/bacon.lox
PASS examples/badfun.lox
PASS examples/badret.lox
PASS examples/bagel.lox
PASS examples/bostoncream.lox
PASS examples/cake.lox
PASS examples/checkuse.lox
PASS examples/circle2.lox
PASS examples/circle.lox
1d0
< 3 1c1 < --- > [line 1] Error at ',': Expect ')' after expression.
FAIL examples/comma.lox
PASS examples/counter.lox
PASS examples/devonshinecream.lox
PASS examples/eclair.lox
PASS examples/fibonacci2.lox
PASS examples/fibonacci.lox
PASS examples/func.lox
PASS examples/funexprstmt.lox
PASS examples/hello2.lox
PASS examples/hello3.lox
PASS examples/hello.lox
PASS examples/math.lox
PASS examples/notaclass.lox
PASS examples/noteveninaclass.lox
PASS examples/point.lox
PASS examples/retthis.lox
PASS examples/scope1.lox
PASS examples/scope.lox
PASS examples/supersuper.lox
PASS examples/thisout.lox
PASS examples/thrice.lox
SKIP examples/timeit.lox
PASS examples/twovars.lox
PASS examples/usethis.lox
PASS examples/varparam.lox

Opa, aparentemente removi o suporte ao operador vírgula por acidente. Ainda bem que fiz este script, não é?

Espero que este post tenha sido minimamente interessante! Agora, vou consertar meu operador vírgula e seguir lendo este livro maravilhoso.