alexrios
open main menu
Módulos com diferentes graus de coesão interna
Part of series:design

Coesão na era das entregas mais rápidas que nunca

/ 7 min read

Uma função pode passar em todos os testes, fazer exatamente o que o ticket pediu, e ainda viver num módulo onde ela não tem nada que fazer.

Tenho visto muito disso recentemente. O código chega mais rápido do que é pensado. O modelo produz uma função que funciona em segundos, mas não tem senso de onde a função pertence, nem de o que mais deveria viver ao lado dela. Coesão é a primeira propriedade que se desgasta quando a velocidade de entrega ultrapassa o pensamento de design, porque coesão é um julgamento que não aparece na suíte de testes.

Isso era matéria de curso pra júnior no final dos anos 2000. Em algum momento depois de 2020 virou skill de programação avançada, dessas que aparecem em escada de carreira ao lado de system design. Por motivos que vou deixar pra outra pessoa explicar.

Coesão não é uma propriedade só. Ela tem formatos pra refatorar de cara, formatos pra manter, e formatos que pedem julgamento de verdade. A maior parte do código gerado por IA cai nos formatos que pedem refator de cara.

Coincidental

Considere essa função:

func setupUserAndLog(u User, a Action) error {
    if err := validateEmail(u.Email); err != nil {
        return err
    }
    logActivity(a)
    initCache()
    return nil
}

Três statements, zero relação entre eles. Compartilham um stack frame, só isso. A unidade é coincidental. Alguma coisa percebeu que essas chamadas aconteciam perto umas das outras e empacotou.

Esse tipo é quase sempre refator. Costuma ser o resíduo de DRY levado literalmente demais: extrai qualquer duplicação, sem perguntar se ela representa uma ideia. A correção não é renomear a função. É apagar a função e devolver as três chamadas pros donos de verdade.

Lógica

Ou em Zig:

const StringHelpers = struct {
    pub fn validate(s: []const u8) bool { ... }
    pub fn format(s: []const u8, out: []u8) ![]u8 { ... }
    pub fn encrypt(s: []const u8, key: [32]u8) ![]u8 { ... }
};

Tudo aqui opera em string. Nada aqui compartilha propósito. O módulo é uma categoria, não um trabalho. Validação, formatação, criptografia: três preocupações sem relação compartilhando o tipo do parâmetro.

O sinal é nome do tipo substantivo+sufixo. Helpers, Utils, Manager, Service. Se o módulo está nomeado pelo assunto e não pelo que faz, é esse tipo que você tem em mãos. Modelos amam esses nomes porque parecem organizados. Não são. A correção é reagrupar por propósito, não por tipo de dado.

Temporal

Um bloco init:

func init() {
    loadConfig()
    setupDatabase()
    registerMetrics()
    warmCache()
}

O que amarra esses caras? Todos rodam no startup. É a relação inteira. Quando, não o quê. O equivalente em Zig é idêntico em espírito:

pub fn main() !void {
    try loadConfig();
    try setupDatabase();
    try registerMetrics();
    try warmCache();
}

Coesão temporal é aceitável em lugares estreitos: o init literal, o teardown literal, um bloco defer que precisa disparar em ordem. Deixa de ser aceitável no instante em que qualquer um desses passos cresce. Quando setupDatabase precisa do config carregado antes, a relação vira ordenação, não co-ocorrência, e o módulo virou outro tipo de coesão sem você perceber.

Procedural

Passos amarrados por ordem, sem fluxo de dados entre eles:

func CloseFiscalQuarter() {
    freezeJournals()
    notifyTaxAdvisors()
    generateBoardReport()
    archivePaperTrail()
}

A ordem é real, mas é ordem de processo de negócio, não dependência de dado. Cada chamada é independente em código. A função só significa “essas quatro coisas, nessa ordem”.

Teste útil: se trocar a ordem não quebra a compilação, você está em Temporal. Se a ordem é fixa mas nenhum passo consome a saída do anterior, é Procedural. No momento em que um passo começa a produzir saída que o próximo consome, o módulo está fazendo outra coisa, e o nome devia bater.

Comunicacional

Mesmo dado, operações diferentes e independentes:

func ProduceOrderArtifacts(o Order) {
    pdf     := buildInvoicePDF(o)
    json    := marshalForWebhook(o)
    hash    := computeIntegrityHash(o)
    preview := generateThumbnail(o)
    _ = pdf; _ = json; _ = hash; _ = preview
}

As quatro leem o. Nenhuma depende das outras. Compartilham um sujeito e nada mais. Tá bom. O risco é o módulo virar saco de gatos pra “coisas que se fazem com um pedido”. Toda preocupação nova com cara de pedido cai aqui porque o parâmetro bate. É o lugar onde os modelos estacionam comportamento novo por padrão. Todo prompt do tipo “adiciona X ao Order” deposita mais um verbo nesse módulo até ele ter trinta.

O mesmo formato em Zig:

fn analyzeProduct(p: Product) void {
    const margin = calculateProfitMargin(p);
    const carbon = estimateCarbonFootprint(p);
    const shelf  = recommendShelfPosition(p);
    const legal  = checkExportCompliance(p);
    _ = margin; _ = carbon; _ = shelf; _ = legal;
}

Mesmo tipo. Mesmo risco.

Sequencial

Aqui o dado começa a fluir:

func GenerateReport(q Query) (Report, error) {
    rows, err := db.Run(q)
    if err != nil {
        return Report{}, err
    }
    summary := aggregate(rows)
    return format(summary), nil
}

A saída de um passo é a entrada do próximo. A função corresponde a uma cadeia de transformação. Você lê a assinatura e entende a jornada: Query → Report. Mantém.

Funcional

Cada parte do módulo a serviço de uma coisa:

const Crc32 = struct {
    const lookup_table: [256]u32 = initTable();

    pub fn hash(data: []const u8) u32 {
        var crc: u32 = 0xFFFFFFFF;
        for (data) |byte| {
            const idx = @as(u8, @truncate(crc)) ^ byte;
            crc = (crc >> 8) ^ lookup_table[idx];
        }
        return ~crc;
    }

    fn initTable() [256]u32 { ... }
};
// package base64url: Base64 URL-safe (RFC 4648 §5)
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"

func Encode(dst, src []byte)              { ... }
func Decode(dst, src []byte) (int, error) { ... }

func encodeQuantum(dst, src []byte)              { ... } // só existe pra Encode
func decodeQuantum(dst, src []byte) (int, error) { ... } // só existe pra Decode

Um trabalho. Todo símbolo privado existe pra servir o que é público. O módulo se recusa a fazer outra coisa útil pro mundo lá fora. Nada pode ser adicionado sem diluir o propósito.

É o tipo em que módulos viram componíveis.

O que isso significa pra review hoje

Modelos são excepcionalmente bons em produzir código que funciona. Não se importam com onde ele vive. Você entrega mais rápido do que antes, mas a conta vem em coesão, a propriedade que ninguém está checando porque os testes continuam passando.

Você não precisa decorar os nomes. Precisa de algumas perguntas, nessa ordem, toda vez que você lê um módulo:

  1. Esses statements compartilham um propósito? Se não, separa.
  2. Compartilham um sujeito? Se sim, podem ficar juntos.
  3. A saída de um alimenta o próximo? Se sim, nomeia o módulo pela transformação, não pelos passos.

O hábito de maior alavanca que adotei é mover cada módulo pro tipo que nomeia o que ele faz. Um StringHelpers se quebra em peças focadas e funcionais. Um bundle coincidental se dissolve de volta nos donos de verdade. Um init() vira uma transformação explícita e ordenada no instante em que qualquer um dos passos depende da saída de um irmão.

Cada reescrita encolhe a quantidade de contexto que o próximo leitor precisa montar. Humano ou modelo. Os dois têm que ler a coisa.

A velocidade de produção de código subiu. O senso de design, não.

Coesão continua sendo o que o revisor tem que trazer.

Veja também: O helper que comeu a base de código.