Visual density

Visual density (densidade visual) é a quantidade de informação por bloco visual. Olhos cansam quando linhas se acumulam sem respiro; raciocínio quebra quando trechos não relacionados ficam colados. A solução é agrupar por intenção semântica e separar grupos com linha em branco. Cada grupo conta uma micro-história.

Os mesmos princípios de densidade visual com exemplos em JavaScript/Node.js.

Conceitos fundamentais

ConceitoO que é
visual density (densidade visual)Quantidade de informação por bloco visual; alvo é baixa por bloco, alta por arquivo
semantic group (grupo semântico)Conjunto pequeno de linhas que executa uma micro-tarefa coesa (ex: validar, calcular, persistir)
blank line (linha em branco)Separador entre grupos semânticos; substitui comentário de seção
tight pair (par tight)Duas linhas com relação direta (declaração + uso, const + return) sem blank entre elas; o respiro vem antes ou depois do par, não no meio
atomic trio (trio atômico)Três declarações simples consecutivas e homogêneas (const/let); mantidas juntas sem blank; preferir ao 2+1 que cria órfão
semantic pair (par semântico encadeado)Par tight em que a última linha usa diretamente o valor declarado na penúltima; nunca separar a dependência direta
single-line orphan (órfão de 1)Grupo isolado de uma única linha que parece esquecido; resolve juntando ao vizinho ou quebrando 4 em 2+2
explaining return (retorno explicativo)Caso particular de tight pair: const X = … single-line + return X sem blank entre eles
multi-line block (bloco multi-linha)Objeto literal, array literal ou statement quebrado em várias linhas; pede blank depois para isolar o bloco
fragments → assembly (fragmentos → montagem)Linha final que costura múltiplos fragmentos anteriores; fase distinta, blank antes da montagem
boundary (limite)Linha que separa camadas (handler ↔ service, service ↔ repository); merece linha em branco antes
column alignment (alinhamento de coluna)Espaços extras para alinhar = ou : verticalmente; antipadrão: frágil a rename, gera diff ruidoso

A regra central

Grupos pequenos separados por uma linha em branco. Dois é o tamanho natural; três é permitido quando a divisão criaria órfão de 1; quatro quebra em 2+2.

❌ Ruim: denso demais: todos os passos colados
async function registerUser(input) {
  const { name, email } = input;
  const exists = await db.users.findByEmail(email);
  if (exists) throw new ConflictError("Email taken");
  const hash = await hashPassword(input.password);
  const user = await db.users.create({ name, email, hash });
  const token = generateToken(user.id);
  await sendWelcomeEmail(email, token);
  return user;
}
✅ Bom: fases visíveis, no máximo 2 linhas por grupo
async function registerUser(input) {
  const { name, email } = input;
  const exists = await userRepository.findByEmail(email);
  if (exists) throw new ConflictError("Email taken");

  const hash = await hashPassword(input.password);
  const user = await userRepository.create({ name, email, hash });

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

  return user;
}

Explaining Return: par tight

Uma const nomeada acima do return explica o valor retornado. Sempre que a linha imediatamente acima for essa const (single-line) e o return retornar essa variável, os dois formam par de 2 linhas sem blank, não importa quantos passos haja acima. A linha em branco separa o par do que vem antes, não fragmenta o par.

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

  return status;
}
✅ Bom: par tight
function mapErrorToStatus(error) {
  const status = errorStatusByCode[error.code] ?? 500;
  return status;
}

Return tight vs return separado

A regra é simples: return é tight com a linha imediatamente acima somente quando essa linha é a const que nomeia o valor retornado (Explaining Return), e essa const está em uma única linha.

Em todos os outros casos, vai blank antes do return:

  • linha acima é multi-linha (objeto/statement quebrado em várias linhas);
  • linha acima é side effect (await, função sem retorno) que não nomeia o valor;
  • valor retornado foi criado vários passos antes, sem par direto.
❌ Ruim: return fragmentado quando a linha acima é single-line
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",
    timeZone: "America/Sao_Paulo",
  });
  const formattedDate = formatter.format(parsedDate);

  return formattedDate;
}

formatter multi-linha exige blank depois de si, mas o blank foi posto antes do return. formattedDate e return formattedDate formam Explaining Return tight, não devem ser separados.

✅ Bom: multi-linha isolada, Explaining Return tight
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",
    timeZone: "America/Sao_Paulo",
  });

  const formattedDate = formatter.format(parsedDate);
  return formattedDate;
}

O blank fica depois do formatter multi-linha. O par formattedDate + return formattedDate permanece tight.

✅ Bom: return com blank quando construído a partir de objeto multi-linha
function buildOrderResponse(order, requestId) {
  const data = {
    id: order.id,
    total: order.total,
    items: order.items,
  };

  return { data, requestId };
}

data é objeto multi-linha; o blank antes do return isola o bloco grande do envelope final.

Exceção: funções de uma linha ficam compactas. O return é o único conteúdo.

function findPendingOrders(userId) {
  return orderRepository.findByStatus(userId, "pending");
}

Declaração + guarda = 1 grupo

Uma variável seguida do seu if de guarda formam par semântico quando o guarda cabe em uma única linha: if (...) return;, if (...) throw ...;. Nesse caso a linha em branco vem depois do par, nunca entre eles.

Quando o guarda é escrito em bloco { } (qualquer quantidade de linhas físicas, mesmo com uma única instrução dentro), o if vira fase própria: o bloco já ocupa peso visual próprio. Aplica-se a regra de multi-linha pede respiro: linha em branco antes do bloco. O critério é visual, não semântico.

❌ Ruim: variável solta do seu guarda inline
const order = await fetchOrder(orderId);

if (!order) return;
const invoice = buildInvoice(order);
✅ Bom: guarda inline (uma linha), par tight com a declaração
const order = await fetchOrder(orderId);
if (!order) return;

const invoice = buildInvoice(order);
✅ Bom: guarda em bloco, fase própria com blank antes
const handler = eventHandlers[eventType];

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

const eventPayload = event.data;
✅ Bom: guarda em bloco mesmo com uma única instrução pede respiro antes
const response = await requestFn();

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

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

O bloco ocupa três linhas físicas, com peso visual próprio. Inline ficaria tight, mas em bloco, blank antes.

Órfão de 1 linha: pior que trio atômico

Três declarações simples consecutivas (const, let, var) formam grupo coeso. Partir em 2+1 deixa a última linha solitária entre blanks. Mantenha as três juntas. Só divida em 2+2 a partir de quatro.

❌ Ruim: órfão entre blanks
const MINIMUM_DRIVING_AGE = 18;
const ORDER_STATUS_APPROVED = 2;

const ONE_DAY_MS = 86_400_000;
✅ Bom: trio tight
const MINIMUM_DRIVING_AGE = 18;
const ORDER_STATUS_APPROVED = 2;
const ONE_DAY_MS = 86_400_000;
✅ Bom: 4 atomics 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;

Par semântico encadeado

Quando a linha final depende da penúltima (usa o valor recém declarado), as duas formam par. A quebra natural fica antes do par, não entre ele e sua dependência direta.

❌ Ruim: dependência direta partida
function buildShippingLabel(order) {
  const fullName = `${order.customer.firstName} ${order.customer.lastName}`;
  const addressLine = `${order.address.street}, ${order.address.number}`;

  const cityLine = `${order.address.city} - ${order.address.state}, ${order.address.zipCode}`;

  const label = `${fullName}\n${addressLine}\n${cityLine}\nOrder #${order.id}`;
  return label;
}
✅ Bom: par semântico tight
function buildShippingLabel(order) {
  const fullName = `${order.customer.firstName} ${order.customer.lastName}`;
  const addressLine = `${order.address.street}, ${order.address.number}`;

  const cityLine = `${order.address.city} - ${order.address.state}, ${order.address.zipCode}`;
  const label = `${fullName}\n${addressLine}\n${cityLine}\nOrder #${order.id}`;
  return label;
}

Fragmentos → montagem: blank antes do consumidor

Quando há dois ou mais fragmentos preparados e uma linha final que consome múltiplos fragmentos (não depende só do último), trate a montagem como fase distinta, com blank antes dela. É o caso clássico "preparar partes → montar resultado", diferente do par semântico encadeado (onde a última depende diretamente da penúltima e por isso fica tight).

Heurística rápida:

  • A última linha usa só o valor recém-declarado acima? → par semântico encadeado, fica tight.
  • A última linha costura múltiplos fragmentos declarados em linhas diferentes? → fragmentos → montagem, blank antes.
❌ Ruim: fragmentos e montagem coladas como se fossem trio homogêneo
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 e address e order.id e order.deliveryDays. Não é par direto com address: é a fase de montagem. Coladas como trio, as fases ficam invisíveis.

✅ Bom: fragmentos como par, montagem isolada, Explaining Return tight
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 fragmentos" (par) e "montar + entregar" (Explaining Return tight).

✅ Bom: contraste: par semântico encadeado (última depende só da penúltima)
function buildOrderSlug(order) {
  const normalizedTitle = order.title.toLowerCase().replace(/\s+/g, "-");
  const slug = `${order.id}-${normalizedTitle}`;
  return slug;
}

slug depende diretamente de normalizedTitle (penúltima). Par semântico encadeado: as duas ficam tight, e o return ainda tight com o último.

2+1 dentro de blocos curtos

Em loops e branches curtos, 2+1 ainda é a quebra natural quando as linhas não são todas atômicas homogêneas.

❌ Ruim: 3 linhas heterogêneas coladas
while (attempt < maxAttempts) {
  const connection = connectToDatabase();
  if (connection.isReady) break;
  attempt++;
}
✅ Bom: declaração + guarda em par, incremento separado
while (attempt < maxAttempts) {
  const connection = connectToDatabase();
  if (connection.isReady) break;

  attempt++;
}

Fases de um método

Métodos com múltiplos passos (buscar, transformar, persistir, responder) devem deixar cada fase visível.

❌ Ruim: todas as fases coladas, sem separação visual
async function createUserHandler(req, res) {
  const sanitized = sanitizeCreateUser(req.body);
  const input = createUserSchema.parse(sanitized);
  await createUser(input);
  const body = { id: input.id };
  res.status(201).json(body);
}
✅ Bom: fases explícitas
async function createUserHandler(request, response) {
  const sanitized = sanitizeCreateUser(request.body);
  const input = createUserSchema.parse(sanitized);

  await createUser(input);

  const body = { id: input.id };
  response.status(201).json(body);
}

Testes: expect como fase própria

O expect é fase distinta. A linha em branco antes dele separa o que está sendo verificado do como está sendo verificado.

❌ Ruim: expect colado ao setup, fases invisíveis
it("applies percentage discount to order price", () => {
  const order = { price: 100, discountPct: 10 };
  const actualOrder = applyDiscount(order);
  const expectedPrice = 90;
  expect(actualOrder.price).toBe(expectedPrice);
});
✅ Bom: expect separado, assertion como fase própria
it("applies percentage discount to order price", () => {
  const order = { price: 100, discountPct: 10 };
  const actualOrder = applyDiscount(order);
  const expectedPrice = 90;

  expect(actualOrder.price).toBe(expectedPrice);
});

Multi-linha: respiro depois do bloco

Quando um objeto literal, array literal ou statement quebra em várias linhas, o bloco já ocupa espaço visual próprio. Cole uma linha em branco depois dele para isolar o bloco grande do próximo passo. Sem respiro, o leitor não vê onde o bloco termina e o próximo começa.

❌ Ruim: objeto multi-linha colado ao próximo statement
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: blank depois do objeto isola 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;
}

Ifs consecutivos: blocos com chaves precisam de respiro

Dois if consecutivos com bloco multi-linha ({ ... }) coladas formam muralha: o olho não distingue onde um bloco termina e o outro começa. Sempre insira blank entre eles.

Exceção: guardas de uma linha (early returns curtos) formam trio homogêneo e ficam tight: a regra do trio atômico se aplica.

❌ Ruim: dois blocos {} colados
function processOrder(order) {
  if (order.status === "pending") {
    notifyCustomer(order);
    scheduleReview(order);
  }
  if (order.total > 1_000) {
    flagForAudit(order);
    notifyManager(order);
  }
}
✅ Bom: blank 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 tight (trio atômico)
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;
}

Sem column alignment

Não alinhe verticalmente =, : ou valores com múltiplos espaços. Use sempre um espaço único. Alinhamento artificial quebra com qualquer rename, gera diff ruidoso e treina o olho a procurar colunas que somem na primeira refator.

❌ Ruim: espaços extras para alinhar colunas
const userName = "alice";
const userEmail = "alice@example.com";
const userRole = "admin";
const lastLoginAt = new Date();
✅ Bom: espaço único, sem padding
const userName = "alice";
const userEmail = "alice@example.com";
const userRole = "admin";
const lastLoginAt = new Date();

Strings longas

Uma string longa colada em um return esconde as partes que a compõem. Extraia fragmentos em variáveis nomeadas antes de montar o resultado.

❌ Ruim: string imensa inline, sem semântica nas partes
function buildDeliveryMessage(user, order) {
  return `Olá ${user.firstName} ${user.lastName}, seu pedido #${order.id} foi confirmado e será entregue no endereço ${order.address.street}, ${order.address.city} - ${order.address.state} em até ${order.deliveryDays} dias úteis.`;
}
✅ Bom: fragmentos nomeados, template final limpo
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;
}

Desenvolvido por @thiagocajadev · Fork baseado no repositório pmndrs/docs · Poimandres.