Visual density

Escopo: transversal. Aplica-se a qualquer linguagem ou stack do projeto.

Código é lido muitas vezes mais do que é escrito. A visual density (densidade visual) trata de agrupar o que pertence junto e separar o que é distinto, usando uma ferramenta só: a linha em branco. Ela guia o olho pelo código e dispensa o comentário de seção. Esta página é o guia transversal; cada linguagem tem a sua versão com exemplos idiomáticos, linkada no fim.

Conceitos fundamentais

ConceitoO que é
visual density (densidade visual)Quantidade de informação por bloco de código; o alvo é pouca por bloco e muita por arquivo
semantic group (grupo semântico)Poucas linhas que executam uma etapa coesa, por exemplo validar, calcular ou salvar
blank line (linha em branco)Separa dois grupos; faz o papel que antes cabia a um comentário de seção
boundary (limite)Linha que separa camadas, por exemplo do handler para o service; pede uma linha em branco antes
multi-line block (bloco de várias linhas)Objeto, array ou comando quebrado em várias linhas; pede um respiro depois de si
column alignment (alinhamento em colunas)Espaços extras para alinhar = ou : na vertical; antipadrão, quebra a cada renomeação

Referência rápida

RegraDescrição
O grupo natural tem 2 linhasLinhas relacionadas ficam juntas; a separação natural é por par
Três linhas quando dividir criaria uma soltaTrês declarações simples do mesmo tipo (const, let) podem ficar juntas
Quatro ou mais quebram em 2+2A partir de quatro linhas relacionadas, sempre dividir para evitar a muralha
A const que nomeia o retorno cola no returnconst X = …; return X; ficam juntas, não importa quantos passos venham acima
return separadoSó vai linha em branco antes do return se a linha acima for de várias linhas, for efeito colateral ou se o valor nasceu passos antes
A linha que usa o valor recém-declarado cola neleQuando a última linha depende só da penúltima (ex: label = f(cityLine)), as duas ficam juntas
Montar o resultado é fase separadaA linha que costura vários fragmentos ganha respiro antes
A variável e o if que a valida são um grupoO respiro vem depois do par, não no meio dele
Bloco de várias linhas pede respiro depoisObjeto, array ou comando quebrado em várias linhas exige linha em branco depois
if com chaves seguidos pedem respiro entre siExceção: guardas de uma linha ficam juntas
Sem alinhamento em colunasUm espaço único ao redor de = ou :
Nunca duas linhas em brancoUma separa; duas viram ruído

A regra central

Grupos pequenos, separados por uma linha em branco. Dois é o tamanho natural do grupo. Três é aceitável quando dividir deixaria uma linha sozinha. A partir de quatro, sempre quebre. Use exatamente uma linha em branco entre grupos: a segunda vira ruído.

O return fica junto da linha que nomeia o valor

Uma const nomeada logo acima do return explica o que está sendo devolvido. Quando essa const ocupa uma única linha e o return devolve exatamente ela, as duas se leem como uma frase e não aceitam linha em branco no meio. Quantos passos existem acima não importa: o respiro separa o par do que veio antes, nunca parte o par.

❌ Ruim: linha em branco fragmenta o par
function mapErrorToStatus(error) {
  const status = errorStatusByCode[error.code] ?? 500;

  return status;
}

A linha em branco isola o return como se ele encerrasse um parágrafo. Só que não há parágrafo nenhum antes dele, há uma linha. O olho procura o bloco que terminou e não acha.

✅ Bom: as duas linhas juntas
function mapErrorToStatus(error) {
  const status = errorStatusByCode[error.code] ?? 500;
  return status;
}

Lê-se como uma frase só: o status vem da tabela, devolva.

Quando o return cola na linha acima e quando ganha um respiro

O return gruda na linha imediatamente acima em um caso só: quando essa linha é a const que nomeia o valor devolvido e cabe em uma única linha.

Em todos os outros casos vai uma linha em branco antes dele:

  • a linha acima ocupa várias linhas (um objeto ou um comando quebrado);
  • a linha acima é efeito colateral (await, função que não devolve nada) e portanto não nomeia o valor;
  • o valor devolvido nasceu vários passos antes, sem par direto com a linha de cima.
❌ Ruim: o respiro caiu no lugar errado
function formatOrderDate(isoString, locale = "pt-BR") {
  const parsedDate = new Date(isoString);
  const formatter = new Intl.DateTimeFormat(locale, {
    day: "2-digit",
    month: "2-digit",
    year: "numeric",
  });
  const formattedDate = formatter.format(parsedDate);

  return formattedDate;
}

formatter ocupa várias linhas e pede o respiro depois de si. Ele foi parar antes do return, e o efeito é duplo: o bloco grande fica junto do que vem depois, e o par formattedDate + return formattedDate acaba partido.

✅ Bom: bloco grande isolado, retorno junto do seu nome
function formatOrderDate(isoString, locale = "pt-BR") {
  const parsedDate = new Date(isoString);
  const formatter = new Intl.DateTimeFormat(locale, {
    day: "2-digit",
    month: "2-digit",
    year: "numeric",
  });

  const formattedDate = formatter.format(parsedDate);
  return formattedDate;
}
✅ Bom: respiro antes do `return` quando o valor é um objeto de várias linhas
function buildOrderResponse(order, requestId) {
  const data = {
    id: order.id,
    total: order.total,
    items: order.items,
  };

  return { data, requestId };
}

data ocupa várias linhas. O respiro antes do return separa esse bloco do envelope que fecha a função.

✅ Bom: respiro antes do `return` quando o valor nasceu passos antes
async function registerUser(input) {
  const user = await userRepository.create(input);

  const token = generateToken(user.id);
  await sendWelcomeEmail(input.email, token);

  return user;
}

return user não forma par com await sendWelcomeEmail, que é efeito colateral e não nomeia nada. O user foi criado lá em cima. A linha em branco marca o encerramento.

Não deixe uma linha sozinha entre espaços

Três declarações simples e seguidas formam um grupo coeso. Parti-las em 2+1 deixa a última boiando entre duas linhas em branco, e o leitor para para descobrir o que aquele destaque significa. Descobre que era só mais uma constante. Mantenha as três juntas e só divida em 2+2 quando forem quatro ou mais.

❌ Ruim: a terceira linha boiando sozinha
const MINIMUM_DRIVING_AGE = 18;
const ORDER_STATUS_APPROVED = 2;

const ONE_DAY_MS = 86_400_000;

A linha solta parece um passo à parte, mas é só mais uma constante. O leitor pausa procurando o motivo e não encontra.

✅ Bom: as três juntas
const MINIMUM_DRIVING_AGE = 18;
const ORDER_STATUS_APPROVED = 2;
const ONE_DAY_MS = 86_400_000;
✅ Bom: quatro viram 2+2
const MINIMUM_DRIVING_AGE = 18;
const ORDER_STATUS_APPROVED = 2;

const ONE_DAY_MS = 86_400_000;
const MAX_RETRY_ATTEMPTS = 3;

Duas linhas onde a segunda usa o valor da primeira

Quando a linha final consome o valor que a linha anterior acabou de declarar, as duas contam uma coisa só. A quebra natural vem antes das duas, nunca entre elas.

❌ Ruim: a dependência direta foi partida ao meio
public static string BuildShippingLabel(Order order)
{
    var fullName = $"{order.Customer.FirstName} {order.Customer.LastName}";
    var addressLine = $"{order.Address.Street}, {order.Address.Number}";

    var cityLine = $"{order.Address.City} - {order.Address.State}, {order.Address.ZipCode}";

    var label = $"{fullName}\n{addressLine}\n{cityLine}\nOrder #{order.Id}";

    return label;
}

cityLine é consumido pela linha seguinte. Separar as duas com uma linha em branco esconde justamente a relação que importa.

✅ Bom: quem depende fica junto
public static string BuildShippingLabel(Order order)
{
    var fullName = $"{order.Customer.FirstName} {order.Customer.LastName}";
    var addressLine = $"{order.Address.Street}, {order.Address.Number}";

    var cityLine = $"{order.Address.City} - {order.Address.State}, {order.Address.ZipCode}";
    var label = $"{fullName}\n{addressLine}\n{cityLine}\nOrder #{order.Id}";
    return label;
}

Dois pares grudados: cityLine com label, porque um alimenta o outro, e label com return label, porque um nomeia o outro. O leitor lê "nome e endereço / cidade, etiqueta, devolve."

Prepare as partes, depois monte o resultado

Quando duas ou mais linhas preparam fragmentos e uma linha final costura vários deles de uma vez, essa montagem é uma fase à parte e ganha respiro antes. É o clássico "preparar as partes, montar o resultado", e ele se distingue do caso anterior por um detalhe só: lá a última linha depende apenas da penúltima, aqui ela puxa material de várias.

A pergunta que decide:

  • A última linha usa o valor declarado logo acima? Então as duas ficam juntas.
  • A última linha costura fragmentos vindos de linhas diferentes? Então ela é montagem, e leva uma linha em branco antes.
❌ Ruim: preparação e montagem juntas, como se fossem o mesmo passo
function buildDeliveryMessage(user, order) {
  const fullName = `${user.firstName} ${user.lastName}`;
  const address = `${order.address.street}, ${order.address.city} - ${order.address.state}`;
  const deliveryMessage = `Olá ${fullName}, seu pedido #${order.id} foi confirmado e será entregue em ${address} em até ${order.deliveryDays} dias úteis.`;
  return deliveryMessage;
}

deliveryMessage consome fullName, address, order.id e order.deliveryDays. Ele monta o resultado a partir de vários fragmentos, então forma uma fase própria. Coladas como um trio, as duas fases somem.

✅ Bom: fragmentos de um lado, montagem do outro
function buildDeliveryMessage(user, order) {
  const fullName = `${user.firstName} ${user.lastName}`;
  const address = `${order.address.street}, ${order.address.city} - ${order.address.state}`;

  const deliveryMessage = `Olá ${fullName}, seu pedido #${order.id} foi confirmado e será entregue em ${address} em até ${order.deliveryDays} dias úteis.`;
  return deliveryMessage;
}

Duas fases visíveis: preparar os fragmentos, montar e entregar.

A variável e o if que a valida ficam juntos

Uma variável seguida do if que a valida formam um grupo só, desde que o guarda caiba em uma linha: if (...) return;, if (...) throw ...;. O respiro vem depois do par.

Quando o guarda é escrito com chaves, a conta muda. O bloco { } tem peso visual próprio, mesmo carregando uma única instrução dentro, e por isso vira fase separada: linha em branco antes dele. O critério aqui é visual: o que decide é quanto espaço o bloco ocupa na tela.

✅ Bom: guarda de uma linha, junto da declaração
const product = await fetchProduct(id);
if (!product) throw new NotFoundError();

const result = process(product);
✅ Bom: guarda com chaves, fase própria e respiro antes
const handler = eventHandlers[eventType];

if (!handler) {
  logUnhandledEventType(eventType);
  return;
}

const eventPayload = event.data;
✅ Bom: com chaves pede respiro antes mesmo carregando uma instrução só
const response = await requestFn();

if (response.status !== 429) {
  return response;
}

const delayMs = Math.pow(2, attempt) * 1000;

O bloco ocupa três linhas na tela: peso próprio. Escrito em uma linha, ficaria junto da declaração; com chaves, ganha o respiro antes.

Deixe cada fase do método visível

Um método que busca, transforma, persiste e responde tem quatro fases, e o leitor deveria enxergar as quatro sem ler o corpo. Cada fase ocupa até duas linhas antes do respiro, ou três quando são declarações simples do mesmo tipo.

Depois de um bloco de várias linhas, deixe um respiro

Um objeto literal, um array ou um comando quebrado em várias linhas já ocupa espaço próprio na tela. Cole uma linha em branco depois dele. Sem isso, o leitor não vê onde o bloco terminou e o próximo passo começou; os dois viram uma massa só.

❌ Ruim: objeto grande junto do comando seguinte
async function createSession(user) {
  const claims = {
    sub: user.id,
    email: user.email,
    roles: user.roles,
    issuedAt: Date.now(),
  };
  const token = await signJwt(claims);
  return token;
}
✅ Bom: o respiro depois do objeto fecha o bloco
async function createSession(user) {
  const claims = {
    sub: user.id,
    email: user.email,
    roles: user.roles,
    issuedAt: Date.now(),
  };

  const token = await signJwt(claims);
  return token;
}

Dois if seguidos com chaves pedem uma linha entre eles

Dois blocos { ... } juntos formam uma parede: o olho não acha onde um termina e o outro começa. Sempre separe os dois.

A exceção são as guardas de uma linha. Retornos antecipados curtos e seguidos formam um grupo homogêneo e ficam juntos, pela mesma regra que mantém três declarações unidas.

❌ Ruim: dois blocos juntos, sem nada entre eles
function processOrder(order) {
  if (order.status === "pending") {
    notifyCustomer(order);
    scheduleReview(order);
  }
  if (order.total > 1_000) {
    flagForAudit(order);
    notifyManager(order);
  }
}
✅ Bom: uma linha em branco entre os blocos
function processOrder(order) {
  if (order.status === "pending") {
    notifyCustomer(order);
    scheduleReview(order);
  }

  if (order.total > 1_000) {
    flagForAudit(order);
    notifyManager(order);
  }
}
✅ Bom: guardas de uma linha ficam juntas
function validateInput(input) {
  if (!input) throw new ValidationError("Input required");
  if (!input.email) throw new ValidationError("Email required");
  if (!input.password) throw new ValidationError("Password required");

  return input;
}

Não alinhe o código em colunas

Não use espaços extras para alinhar =, : ou valores na vertical. Um espaço único, sempre. O alinhamento artificial quebra a cada renomeação e enche o diff de linhas que ninguém mudou de fato.

❌ Ruim: espaços extras para alinhar as colunas
const userName     = "alice";
const userEmail    = "alice@example.com";
const userRole     = "admin";
const lastLoginAt  = new Date();
✅ Bom: um espaço só
const userName = "alice";
const userEmail = "alice@example.com";
const userRole = "admin";
const lastLoginAt = new Date();

Textos longos montados em uma linha

Uma string longa escrita direto no return esconde as partes que a compõem. Extraia os fragmentos em variáveis nomeadas e monte o resultado depois: o texto final fica legível e cada pedaço ganha um nome.


Por linguagem

Os mesmos princípios com exemplos idiomáticos de cada stack:

DoDocs v3.7.0 · Desenvolvido por @thiagocajadev · Baseado no trabalho de pmndrs/docs · Poimandres.