Implementando malloc() e free() — reutilizando blocos de memória

Este post faz parte de uma série sobre como implementar as funções malloc() e free(). No artigo anterior, alteramos nossas funções para liberar alguns blocos de memória. Contudo, isto apenas ocorria se os blocos liberados fossem desalocados do mais novo ao mais antigo.

Isto não faria muita diferença. Raramente a memória alocada dinamicamente se comporta com uma pilha, onde o bloco mais novo é sempre desalocado primeiro. A grande vantagem da alocação de memória dinâmica, afinal, é que ela não funciona como uma pilha.

Para entender as limitações de nossa implementação, considere o código abaixo

void *ptr1 = abmalloc(8);
void *ptr2 = abmalloc(8);
abfree(ptr1);
void *ptr3 = abmalloc(8);

Na primeira linha, alocamos oito bytes, e os liberamos na terceira linha. Na última linha, alocamos oito bytes de novo. Contudo, não podemos reaproveitar a memória liberada, pois novos blocos de memória só são alocados no topo do heap. Para economizar memória de verdade, precisamos de uma solução mais sofisticada.

Uma opção é reutilizar blocos livres. Quando desalocarmos um bloco de memória, vamos marcá-lo como disponível para reuso. Quando uma solicitação de memória de tamanho menor ou igual a algum bloco disponível ocorrer, podemos apenas retornar um ponteiro para o bloco disponível e marcá-lo como em uso.

Para isto, acrescentamos ao cabeçalho dos blocos um campo booleano, chamado available, que indicará se o bloco está livre. Como um bloco só pode ser reusado se a memória solicitada por abmalloc() for menor ou igual à disponível no bloco, precisamos também de um campo no cabeçalho indicando o tamanho do bloco, que chamaremos de size.

typedef struct Header {
  struct Header *previous;
  size_t size;
  bool available;
} Header;

Quando o bloco é alocado, o valor do campo available deve ser false (já que o bloco não está disponível). Também registramos o tamanho do bloco no campo size:

void *abmalloc(size_t size) {
  Header *header = sbrk(sizeof(Header) + size);
  header->previous = last;
  header->size = size;
  header->available = false;
  last = header;
  return last + 1;
}

Temos as informações no cabeçalho mas ainda não estamos reusando memória desalocada. Para reaproveitar os blocos disponíveis, precisamos encontrá-los! O algoritmo para isto é bem simples: logo no começo abmalloc() irá iterar sobre os blocos, a partir do último até chegar ao primeiro. Como o ponteiro previous do primeiro bloco é sempre NULL, paramos quando o ponteiro for NULL:

void *abmalloc(size_t size) {
Header *header = last;
while (header != NULL) {
header = header->previous;
}

Em cada iteração, verificamos se o bloco está disponível e tem tamanho aceitável. Se no meio desse processo encontrarmos um bloco disponível maior ou igual ao que precisamos, estamos com sorte! Marcamos o bloco como indisponível, e o retornamos.

void *abmalloc(size_t size) {
Header *header = last;
while (header != NULL) {
if (header->available && (header->size >= size)) {
header->available = false;
return header + 1;
}
header = header->previous;
}

E se não encontrarmos um bloco que satisfaça essas condições? Neste caso a função abmalloc() aumenta o heap, como já fazia antes:

void *abmalloc(size_t size) {
  Header *header = last;
  while (header != NULL) {
    if (header->available && (header->size >= size)) {
      header->available = false;
      return header + 1;
    }
    header = header->previous;
  }
  header = sbrk(sizeof(Header) + size);
  header->previous = last;
  header->size = size;
  header->available = false;
  last = header;
  return last + 1;
}

Na hora de desalocar, temos duas situações possíveis. Se o bloco desalocado por abfree() for o último, nada muda: movemos o topo do heap para o começo do bloco, alteramos o ponteiro last. Mas se o bloco não estiver no topo do heap? Simplesmente marcamos ele como disponível, como pode ser visto na cláusula else da função abaixo:

void abfree(void *ptr) {
   Header *header = (Header*) ptr - 1;
   if (header == last) {
     last = header->previous;
     brk(header);
   } else {
     header->available = true;
   }
 }

Reutilizar blocos de memória é um enorme avanço. Contudo, podemos ser ainda mais eficientes no uso de memória. Por exemplo, só reduzimos o tamanho do heap se desalocamos o último bloco. Se há mais blocos inutilizados logo antes dele, poderíamos liberá-los também. Veremos como fazer isto no próximo post.

Implementando malloc() e free() — adicionando metadados aos blocos de memória

Este post faz parte de uma série sobre como implementar as funções malloc() e free(). Anteriormente, fizemos uma implementação são simplória que praticamente não libera memória nenhuma: um ponteiro aponta para o último bloco alocado, o que permite que abfree() o desaloque, mas somente ele.

Uma opção melhor é fazer o último bloco apontar para o penúltimo, o penúltimo para o antepenúltimo e assim por diante, formando uma lista ligada. Para isso, criamos uma struct que servirá como cabeçalho dos blocos, contendo um ponteiro para o bloco anterior:

typedef struct Header {
  struct Header *previous;
} Header;

Além disso, o ponteiro para o último bloco, que era do tipo void*, agora é do tipo Header*:

Header *last = NULL;

Para usar esses cabeçalhos, abmalloc() reserva memória suficiente para armazenar tanto o cabeçalho quanto o tamanho solicitado:

void *abmalloc(size_t size) {
Header *header = sbrk(sizeof(Header) + size);

Deste modo, utilizamos o início do bloco para armazenar informações necessárias, como um ponteiro para o último bloco alocado antes do novo:

  header->previous = last;

Em seguida, atualizamos last para apontar para o novo bloco:

   last = header;

Por fim, retornamos um ponteiro para a memória que o usuário pode utilizar. Como header aponta para os metadados, não podemos simplesmente retorná-lo. Caso contrário, toda informação do cabeçalho seria sobrescrita quando o usuário usasse o ponteiro! Ao invés disso, retornamos um ponteiro para logo depois do cabeçalho. Este ponteiro é fácil de calcular: é o endereço de memória do cabeçalho mais o tamanho do cabeçalho:

  return header + 1;
}

Note como incrementamos o ponteiro header em 1. Como o tipo do ponteiro é Header*, o incremento será, na verdade, o número de bytes da struct Header, não um byte apenas. Por isso, o tipo do ponteiro incrementado é muito relevante na aritmética de ponteiros.

Agora que nossos blocos de memória possuem metadados no início, é preciso levar isso em conta na hora de desalocar. free() recebe um ponteiro não para o começo do bloco, mas para a memória disponibilizada ao usuário. Logo, precisamos encontrar o começo do bloco a partir do ponteiro que o usuário passou. Nada que uma pequena aritmética de ponteiros não resolva:

void abfree(void *ptr) {
  Header *header = (Header*) ptr - 1;

Se header aponta para o último bloco alocado, o bloco anterior passará a ser o último. Neste caso, podemos retornar memória do heap para o sistema operacional através de brk():

  if (header == last) {
last = header->previous;
brk(header);
}

Eis a nova versão de nossas funções malloc() e free():

typedef struct Header {
   struct Header *previous;
 } Header;

 Header *last = NULL;

 void *abmalloc(size_t size) {
   Header *header = sbrk(sizeof(Header) + size);
   header->previous = last;
   last = header;
   return header + 1;
 }

 void abfree(void *ptr) {
   Header *header = (Header*) ptr - 1;
   if (header == last) {
     last = header->previous;
     brk(header);
   }
 }

abmalloc() e abfree() podem ser ligeiramente mais econômicos em memória agora, mas não muito. Raramente a memória alocada dinamicamente se comporta com uma pilha, onde o bloco mais velho é sempre desalocado primeiro. No próximo post, veremos como utilizar a memória de blocos mais antigos que não estão mais em uso.

Pequenos tipos de tíquetes

Tickets no Jira tendem a acumular campos redundantes e opcionais, ficando complexos e confusos. Gosto do Jira, mas compreendo a frustração que isso causa. Por isso, inspirado pelas , sugiro aplicar a abordagem de criar tíquetes menores.

Apenas três estados

Uma maneira de limitar o tamanho dos tickets é simplificar o workflow restringindo o número de estados. Por exemplo, podemos definir que cada tipo de tíquete teria, no máximo, três estados:

  • A fazer
  • Em progresso
  • Concluído

Para representar outros estágios, podemos criar de novos tipos de tíquetes, como subtarefas.

Um tipo de tíquete medianamente complexo

Vejamos um exemplo. Considere o tíquete abaixo:

Chave: XYZ-1234. Status: Em teste. Título: Demônios nasais. Descrição: Chamar free() em ponteiro previamente desalocado resulta em demônios saindo do nariz. Análise técnica: A causa-raiz é um comportamento indefinido. Resultado de teste: O patch não funciona. Agora fantasmas saem pelas orelhas. Data de lançamento: 2023-12-22.

Ele seguiria por este workflow:

Aberto ⇨ A fazer ⇨ Em análise ⇨ Fazendo ⇨ Em teste ⇨ A lançar ⇨ Feito

Como poderíamos reduzir o número de fases?

Podemos começar removendo estágio “Em análise. No seu lugar, criamos um novo tipo de tíquete, chamado “Análise Técnica”. Assim, a tarefa original ficará em execução (“Fazendo”) enquanto a análise técnica estiver em andamento.

Menos campos em um tíquete

Uma vantagem disto seria transferência de campos para subtarefas. Campos que se misturariam no tíquete original apareceriam apenas nas tarefas em que são relevantes.

Considere o campo “Data de lançamento”, que só faz sentido na fase “A Lançar”. Se desenvolvedores, testadores etc. não são responsáveis pelo lançamento, este campo é confuso e polui a tarefa original. Com um novo tipo de tarefa chamado “Release“, esse campo estaria no lugar mais apropriado, mantendo o tíquete original sucinto.

Repetindo estágios sem regredir

Outra vantagem é que o ticket original pode passar pelo mesmo “estágio” várias vezes. É comum um tíquete ter uma fase de desenvolvimento, seguida por testes de qualidade, por exemplo. Só que, se surgir um problema na avaliação, não é recomendável retroceder à fase de desenvolvimento. Como lidar com isso?

Ao trabalhar com subtarefas, podemos marcar a validação como concluída e criar um novo tíquete de implementação. No nosso tíquete, por exemplo, podemos remover a fase “Em teste” e criar uma subtarefa do tipo “Testagem”, assim como uma outra chamada “Desenvolvimento”. Cada vez que o teste falhar, fechamos a testagem e abrimos uma nova tarefa de desenvovimento.

Resultado

Seguindo esta estratégia, nosso tíquete ficaria assim:

Chave: XYZ-1234. Status: Em teste. Título: Demônios nasais. Descrição: Chamar free() em ponteiro previamente desalocado resulta em demônios saindo do nariz. Links: XYZ-1235 Análise técnica; XYZ-2345 Remover trecho em latim; XYZ-2345 Usar função medium(); XYZ-3456 Testar função medium(); XYZ-4444 Plano de release

E o workflow ficaria bem mais simples:

Naturalmente, esta estratégia é flexível. No nosso caso, por exemplo, não removemos a fase “A fazer” ainda. Restringir a cinco (incluindo backlog e validação) é outra possibilidade, mas não muito mais que isso.

Conclusões

Em programação, é comum encontrar os chamados “God objects“, objetos enormes que são responsáveis por várias funções diferentes. Quebrá-los é uma maneira segura de obter qualidade de código. Por isso, suspeito que o mesmo princípio pode se aplicar a tíquetes no Jira.

Não sou o gerente de projeto, mas como programador, acredito que limitar o tamanho e os passos dos tickets pode ser uma ideia eficaz. Fico curioso para saber se alguém já experimentou isso e como foi.

Implementando malloc() e free() — primeiros passos

Seguindo a maravilhosa jornada que é ler  Crafting Interpreters, cheguei no ponto onde implementamos um interpretador em C! Como sempre, Bob Nystrom impiedosamente nos propõe desafios interessantíssimos que nos mantém ocupados por longos períodos. Por exemplo, neste capítulo ele nos sugere implementar nosso próprio alocador de memória, sem a menor necessidade! Inevitavelmente, caí na armadilha.

O desafio nos permite alocar uma grande região de memória e gerenciá-la, mas decidi implementar a função malloc() do zero. Como uso Ubuntu, foi necessário primeiramente entender melhor o layout da memória de um processo no Linux.

Considere o diagrama abaixo, que representa o layout da memória de um processo.

Layout da memória alocada para um processo no Linux: primeiro, temos um bloco com o código executável, depois blocos para as variáveis globais e estáticas (inicializadas e não inicializadas). Segue-se um largo bloco de memória não utilizado no início do programa, depois os argumentos e variaveis de ambiente, e um block reservado ao kernel. O bloco não utilizado será, com o tempo, utilizado para variáveis locais (stack) e blocos dinamicamente alocados (heap).

Na memória alocada para o processo, há várias seções. Quando o programa inicia sua execução, a parte acinzentada não está em uso ainda. Ao longo de sua execução, o programa declara variáveis locais, fazendo a stack (pilha) crescer para trás.

Já a memória dinamicamente alocada é obtida do heap, que cresce na direção oposta. A maneira bem popular de expandir o heap é aumentando o tamanho do segmento de data (i.e. a seção que contém variáveis globais e estáticas) com a chamada de sistema sbrk().

O diagrama acima ilustra como esta chamada de sistema funcional funciona. sbrk() recebe como parâmetro um número inteiro que irá ser somado ao ponteiro que indica o fim do segmento de data. Depois disso, sbrk() retorna o valor do ponteiro antes do incremento.

De certo modo, o comportamento de sbrk() já é suficiente para alocar memória. Nossa funçaõ malloc() pode simplesmente invocar sbrk() e retornar para o usuário o ponteiro para o início do bloco de memória alocada:

void *abmalloc(size_t size) {
return sbrk(size);
}

Em princípio, free() não precisa fazer nada: como nesta implementação sempre usamos a memória do topo do heap, não há nada que possamos fazer para reutilizar blocos de memória mais antigos. Nesse sentido, free() pode perfeitamente ser um no-op:

void abfree(void *ptr) {
}

Uma operação útil pode ser feita, porém, se o bloco a ser liberado tiver sido o último a ser alocado. Isto significa que ele está no topo da pilha, então basta mover o ponteiro do topo da pilha para o início do bloco desalocado. Para isto, primeiramente devemos saber onde começa o último bloco de memória alocado, o que é fácil com uma variável global:

void *last_block = NULL;

void *abmalloc(size_t size) {
last_block = sbrk(size);
return last_block;
}

Depois, na função free(), verificamos se o ponteiro passado aponta para o último bloco. Neste caso, movemos o topo da pilha para trás com a chamada de sistema brk(). Esta syscall recebe como parâmetro um ponteiro e, se este ponteiro é um valor “razoável” (não é nulo, não aponta para dentro da stack, não aponta para antes do heap), usa o valor do ponteiro como novo topo do heap. O resultado seria algo assim.

void abfree(void *ptr) {
  if (ptr == last_block) {
      brk(last_block);
  }
}

Esta desalocação, porém, é inútil na prática. Considere o exemplo abaixo:

void *ptr1 = abmalloc(8);
void *ptr2 = abmalloc(8);
abfree(ptr2);
abfree(ptr1);

Com a versão atual de abfree(), conseguimos liberar a memória apontada por ptr1, mas não a apontada por ptr2. Para poder liberar ptr2, seria preciso saber que, uma vez que ptr1 foi desalocada, o próximo último bloco é ptr2. Poderíamos criar uma variável second_last_block? Não adiantaria: teríamos o mesmo problema com o antepenúltimo bloco, e assim por diante.

Precisamos de uma estrutura de dados mais poderosa aqui, e é isto que veremos no nosso próximo post.

Substituindo métodos de set-up por utilitários de teste

(Este é um post que publiquei no antigo blog da SEA Tecnologia. Como ainda é relevante, resolvi republicá-lo.)

Uma das muitas aprendizagens que adquiri na antiga SEA Tecnologia é a criação de utilitários de teste.

Utilitários de teste são uma maneira de reaproveitar código em testes unitários. Usualmente, isso é feito utilizando os métodos setUp ou @Before dos casos de teste, mas isso tem algumas desvantagens. Por exemplo, em um caso de teste, podemos ter a seguinte inicialização:

private Address address;
private AddressDAO addressDAO;

@Before
public void setUp() {
    address = new Address();
    address.setStreet("Rua fulano");
    address.setNumber("123/A");
    addressDAO = new AddressDAO();
}Code language: PHP (php)

Essa inicialização funciona bem no teste abaixo…

@Test
public void testGetAllAddresses(){
    addressDAO.addAddress(address);

    List<Address> addresses = addressDAO.getAllAddresses();

    assertEquals(1, addresses.size());
    assertEquals("Rua fulano", addresses.get(0).getStreet());
    assertEquals("123/A", addresses.get(0).getNumber());
} Code language: PHP (php)

Agora, se tivermos o teste a seguir, o objeto criado é desperdiçado:

@Test
public void testGetNoAddress() {
    List<Address> addresses = addressDAO.getAllAddresses();

    assertEquals(0, addresses.size());
}Code language: PHP (php)

Se o código for como o seguinte, teremos redundância de código. também temos de decidir SE o outro objeto deve ser criado no @Before também ou no método.

@Test
public void testGetAllAddressesMoreThanOne() {
    addressDAO.addAddress(address);
    Address address2 = new Address();
    address2.setStreet("Outra rua");
    address2.setNumber("111");
    addressDAO.addAddress(address2);
    List<Address> addresses = addressDAO.getAllAddresses(); 
    assertEquals(1, addresses.size());
    assertEquals("Rua fulano", addresses.get(0).getStreet());
    assertEquals("123/A", addresses.get(0).getNumber()); 
}Code language: PHP (php)

Esses inconvenientes são menores quando comparados à tarefa de criar uma rede de dependências. Por exemplo, para testar uma classe Person que agrega um Address em um outro caso de teste, teremos de ter um @Before semelhante a esse:

private Person person;
private Address address;
private PersonDAO personDAO;

@Before     
public void setUp() {
    address = new Address();
    address.setStreet("Rua fulano");
    address.setNumber("123/A");
    person = new Person();
    person.setName("João");
    person.setAddress(address);
    personDAO = new PersonDAO();
} Code language: PHP (php)

O código para a criação de endereços foi duplicado, e é difícil criar as dependências. Nesses exemplos, vemos casos simples, mas é fácil visualizar como a situação irá se complicar.

Nós solucionamos esse problema criando uma classe para criar esses objetos. Essa classe seria algo como isso:

public class TestUtil {
    public static Address utilCreateAddress(String street, String number) {
        Address address = new Address();
        address.setStreet("Rua fulano");
        address.setNumber("123/A");
        return address;     
    }

    public static Person utilCreatePerson(String name, Address address) {
        Person person = new Person();
        person.setName(name);
        person.setAddress(address);
        return person;
    }
}Code language: JavaScript (javascript)

Nossos casos de teste estendiam a TestUtil, facilitando a criação de objetos:

public class TestAddress2 extends TestUtil {
    private AddressDAO addressDAO = new AddressDAO();

    @Test
    public void testGetAllAddresses() {
        Address address = utilCreateAddress("Rua fulano", "123/A");
        addressDAO.addAddress(address);

        List<Address> addresses = addressDAO.getAllAddresses();

        assertEquals(1, addresses.size());
        assertEquals("Rua fulano", addresses.get(0).getStreet());
        assertEquals("123/A", addresses.get(0).getNumber());
    }

    @Test
    public void testGetNoAddress() {
        List<Address> addresses = addressDAO.getAllAddresses();

        assertEquals(0, addresses.size());
    }

    @Test
    public void testGetAllAddressesMoreThanOne() {
        Address address = utilCreateAddress("Rua fulano", "123/A");
        Address address2 = utilCreateAddress("Outra rua", "111");
        addressDAO.addAddress(address);
        addressDAO.addAddress(address2);

        List<Address> addresses = addressDAO.getAllAddresses();

        assertEquals(2, addresses.size());
        assertEquals("Rua fulano", addresses.get(0).getStreet());
        assertEquals("123/A", addresses.get(0).getNumber());
    } 
} Code language: PHP (php)

Como também precisávamos frequentemente de um objeto qualquer, ou que apenas um ou outro parâmetro fosse definido, criávamos variantes dos métodos:

public static Address utilCreateAddress() {
    return utilCreateAddress("Qualquer", "Qualquer");
}

public static Person utilCreatePerson() {
    return utilCreatePerson("José", utilCreateAddress());
} Code language: PHP (php)

Aprendemos isto em um projeto um tanto complexo, com grandes redes de dependências de objetos. O uso desses utilitários de teste viabilizou a prática de TDD no sistema. Era emocionante descobrir que, para criar aquele documento que dependia de sete outros documentos e uns cinco ou seis usuários, bastava chamar um método.

Naturalmente, há mais sobre nossos utilitários de teste do que foi escrito aqui, e pode haver mais ainda que sequer fizemos. (Por exemplo, pode ser interessante produzir utilitários de teste para classes específicas, ao invés de um gigantesco utilitário) Entretanto, como a ideia é bem simples, esperamos que esse pontapé inicial lhe motive a pensar sobre o tema. Até mais!

Sem Comentários. E agora?

Tradicionalmente, considera-se uma boa prática comentar o código. Há algum tempo, tem-se revisto este conceito. Na Liferay, por exemplo, seguimos uma política de não comentar código. Pessoalmente, sou um entusiasta desta filosofia. Mas não quero apresentar ou defender esta estratégia: há muito material bom sobre isto. Quero discutir uma questão em aberto.

Quem comenta quer transmitir alguma informação importante. Que informação é essa? E, mais importante ainda, onde podemos registrá-la? Vejamos algumas alternativas.

O que essas linhas fazem?

Os nomes de funções são excelentes para explicar o que o código faz. Se um bloco de código precisa de um comentário, considere extraí-lo para uma função ou classe. O nome da entidade já esclarecerá seu propósito.

Observe, por exemplo, as linhas abaixo, retiradas desta classe de testes:

Assert.assertNotNull(recurrence);
Assert.assertNull(recurrence.getUntilJCalendar());
Assert.assertEquals(0, recurrence.getCount());Code language: CSS (css)

Essas linhas verificam se a RRule de um evento tem certas propriedades: ela deve existir, ter um “untilCalendar nulo e uma contagem de zero.

Os conceitos são complexos; eu mesmo me confundiria ao reler estes asserts. Um comentário poderia explicá-los. Mas este commit já esclareceu tudo ao mover essas linhas para um método e invocá-lo:

assertRepeatsForever(recurrence);

Aquelas asserções verificavam se o evento se repete eternamente! Nenhum comentário foi necessário — felizmente, pois estes asserts estavam em vários testes.

O que está acontecendo?

Se o comentário iria explicar algo relevante em tempo de execução, considere transformá-lo em uma mensagem de log! Note o exempo abaixo.

if (Validator.isBlank(serviceAccountKey)) {
	// If no credentials are set for GCS Store, the library will
	// use Application Default Credentials.
	_googleCredentials =
		ServiceAccountCredentials.getApplicationDefault();
}
else {
	_googleCredentials = ServiceAccountCredentials.fromStream(
		new ByteArrayInputStream(serviceAccountKey.getBytes()));
}Code language: JavaScript (javascript)

Este comentário pode ser relevante para quem lê o código. Contudo, seria crucial para alguém investigando um problema de autenticação. Por isso, na prática, escolhi logar uma mensagem:

if (Validator.isBlank(serviceAccountKey)) {
	if (_log.isInfoEnabled()) {
		_log.info(
			"No credentials set for GCS Store. Library will use " +
				"Application Default Credentials.");
	}

	_googleCredentials =
		ServiceAccountCredentials.getApplicationDefault();
}
else {
	_googleCredentials = ServiceAccountCredentials.fromStream(
		new ByteArrayInputStream(serviceAccountKey.getBytes()));
}Code language: JavaScript (javascript)

Por que este código está aqui?

Comentários para explicar por que algumas linhas estão ali também são comuns. Um local melhor para compartilhar essas informações são as mensagens de commits.

Estes dias, por exemplo, me pediram para ajudar com um código em que trabalhei anos atrás. Lendo uma JSP — lembre-se, anos atrás — eu encontrei essas linhas:

<liferay-portlet:renderURL portletName="<%= KaleoDesignerPortletKeys.KALEO_DESIGNER %>" var="viewURL">
	<portlet:param name="mvcPath" value="/designer/view_kaleo_definition_version.jsp" />
	<portlet:param name="redirect" value="<%= currentURL %>" />
	<portlet:param name="name" value="<%= kaleoDefinitionVersion.getName() %>" />
	<portlet:param name="draftVersion" value="<%= kaleoDefinitionVersion.getVersion() %>" />
</liferay-portlet:renderURL>Code language: HTML, XML (xml)

Esta tag está gerando uma URL para ser utilizada em outro lugar. Mas meus olhos treinados acharam estranho aquele parâmetro portletName. Este valor costumar ser definido automaticamente.

Um git blame esclareceu tudo, quando encontrei este commit. A mensagem é clara:

LPS-74977 / LPS-73731 By making the render URL explicitly use the Kaleo Designer name, it will be valid when rendered in another portlet.

Entendi! Este código provavelmente vai ser invocado por algum outro portlet. Neste caso, o valor seria automaticamente setado pela outra aplicação, e por alguma razão queremos evitar isso.

(Por esta razão, aliás, prefiro commits pequenos: eles facilitam descobrir a razão de trechos de código bem específicos. É como se todas as linhas de código tivessem um comentário! Não é uma posição unânime, porém: há quem prefira commits maiores.)

A razão da linha foi esclarecida. Mas por que ela pode ser invocada de outra aplicação? Isto não é usual…

Por que esta mudança foi feita?

Um código bem escrito explica como algo foi implementado. A mensagem de commit esclarece o porquê, mas em um contexto local. Como explicar a motivação mais ampla por trás de um código sem recorrer a comentários?

Os tíquetes do issue tracker são excelentes para isto. Normalmente escritos para guiar o desenvolvimento, esses documentos ajudam demais na interpretação do código. Se adicionarmos a chave do tíquete à mensagem de commit, podemos rastrear as razões.

Voltando ao exemplo acima. Descobrimos que uma linha permite usar o mesmo código em vários portlets. Mas isso raramente é necessário. Por que precisamos reutilizar o código neste caso? Por sorte, a mensagem menciona dois tíquetes. Fui verificar o mais antigo; cheguei a LPSA-64324:

[Information Architecture] EE – As a portal admin, I would like to access all workflow portlets from the control panel section under the same tab.

O título já ajuda, e o texto esclarece de vez. Por razões de usabilidade, três aplicações diferentes passaram a aparecer em abas de um mesmo portlet. Faz todo sentido!

Os comentários que a gente gosta

É importante destacar que tentamos evitar comentários desorganizados, que se entrelaçam no código e tentam explicar trechos difíceis de entender. Há vários comentários, frequentemente com formatos padronizados, que não atrapalham a leitura. Um exemplo óbvio são os cabeçalhos de copyright.

Outra maneira de usar comentários efetivamente é a programação letrada. Neste estilo de programação, os comentários são a estrela do show: o código-fonte contem mais prosa do que código executável. Isto é útil quando explicar o algoritmo é mais importante do que lê-lo, como em pesquisas acadêmicas e análise de dados. Não por acaso, é o paradigma de ferramentas populares como Jupyter Notebook e Quarto.

Mais relevante ainda, ferramentas como Javadoc, JSDoc, Doxygen etc. leem comentários em um formato específico para gerar documentação. Estes comentários não afetam a legibilidade. Pelo contrário: javadocs são ótimos para explicar como usar estas entidades. Combinados com ferramentas como meu querido Doctest, temos até garantias de acurácia e atualidade!

Um mundo de possibilidades

Esses são apenas alguns exemplos de alternativas aos comentários. Há muitas outras opções, como wikis, blogs. Já encontrei a explicação para um código que escrevi no Stack Overflow! Podemos pensar em ainda mais soluções para atender a diferentes necessidades. O ponto principal é que, tendo estas ferramentas à nossa disposição, adicionar comentários diretamente ao código torna-se desnecessário.

Naturalmente, evitar comentários é apenas uma das formas para se escrever código legível. Comentários não são proibidos; de fato, há estratégias que podem torná-los eficazes. No entanto, na minha experiência, comentar indisciplinadamente leva a piores resultados, e essas técnicas ajudam a documentar informações importantes que não cabem diretamente no código.

Você é um adepto da estratégia “sem comentários”? Se sim, que outros meios você usa para transmitir infirmações? Se não, como você faz para ter comentários efetivos? Que tipo de comentário você não vê sendo substituído por essas abordagens? Adoraria escutar suas opiniões.

Dez anos de Liferay

Dias atrás, recebemos aqui em casa um pacote inesperado. O que encontramos dentro dele foi ainda mais surpreendente! O que estaria acontecendo?

Uma caixa de iPad com um cartão sobre ela. O cartão tem o número "10" escrito em cor dourada.

Bem, acontece que há alguns meses atrás, eu completei incríveis dez anos trabalhando na Liferay! Isso não é apenas um long período, mas também uma jornada que me proporcionou muito crescimento. Morei em duas cidades, viajei para algumas outras ao redor do mundo, aprendi a trabalhar remotamente, lidei com inúmeras tecnologias e testemunhei o crescimento da filial da LATAM, que saiu de um punhado de pessoas para centenas.

Um cartão com um cabeçalho escrito: "Happy Liferay Anniversary."

Abaixo, escrito à mão:

"Adam,
É uma honra escrever este cartão para vocÊ comemorar seus 10 anos de Liferay.
Um trabalho feito com comprometimento e dedicação sempre gera bons frutos.
Tenho muitok orgulho de ter feito parte da sua história. Que você continue sendo inspiração para todos nós.
Feliz 10 anos de LIferay!! Que venham muito mais..."

Hoje em dia, é raro permanecer tanto tempo no mesmo lugar, especialmente em uma carreira na área de tecnologia. No entanto, a Liferay é realmente um local agradável para trabalhar, onde sempre há coisas novas para aprender e desafios, sejam da tecnologia, do trabalho em equipe ou do cuidado com o cliente. Sem dúvida, cresci muito e, ao que parece, ainda tenho espaço aqui para evoluir ainda mais!

Portanto, agradeço a todos pelo presente, mas, o que é mais importante, agradeço pelo ótimo momento, pelo crescimento e pelos desafios. E preparem-se, pois pretendo ser uma “incomodação” encantadora entre todos vocês por muitos e frutíferos anos que estão por vir! 😄🎉

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.