7 pontos por GN⁺ 5 시간 전 | 5 comentários | Compartilhar no WhatsApp
  • A duplicação de código é muito mais barata do que uma abstração errada, e generalizar cedo demais aumenta o custo de manutenção no longo prazo
  • Mesmo uma extração que parecia razoável no início pode, conforme os requisitos mudam aos poucos, acumular parâmetros e condicionais e obscurecer a intenção original
  • Quando uma abstração compartilhada começa a carregar várias ideias diferentes, o código se transforma em um procedimento centrado em condições, e quanto mais recursos novos se adicionam, mais frágil ele fica
  • É preciso tomar cuidado com a falácia do custo afundado, que tenta preservar o esforço já investido no código; se necessário, deve-se fazer inline da abstração de volta nos pontos de chamada e deixar apenas o código realmente necessário
  • Se uma abstração errada ficou evidente, reintroduzir a duplicação para observar novamente o que os requisitos atuais têm em comum e só então extrair de novo costuma ser mais rápido

Como surge uma abstração errada

  • A frase “duplication is far cheaper than the wrong abstraction” fazia parte de uma palestra da RailsConf 2014, mas continuou sendo citada desde então
  • Um caminho de falha comum é o seguinte
    • O desenvolvedor A encontra uma duplicação
    • Extrai a duplicação para um método ou classe, dá um nome e cria uma nova abstração
    • Substitui o código repetido nos pontos de chamada por chamadas à nova abstração
    • Com o tempo, surge um novo requisito que é parecido, mas não exatamente igual
    • O desenvolvedor B tenta preservar a abstração existente, adiciona parâmetros e insere condicionais que seguem caminhos diferentes conforme os valores
    • Depois disso, a cada novo requisito aumentam os parâmetros e as condicionais, e o código vai ficando cada vez mais difícil de entender
  • Um código que já foi criado tende a parecer um investimento que precisa ser preservado
    • Entra em ação a sensação de que seria um desperdício abandonar o esforço já feito
    • Quanto mais complexo e difícil de entender o código, mais ele parece importante e trabalhoso, e mais difícil fica descartá-lo
    • Isso se conecta à falácia do custo afundado

Voltar à duplicação e extrair de novo

  • Se novos requisitos continuam sendo implementados sobre uma abstração errada, o código compartilhado passa a girar em torno de condicionais e fica mais instável a cada funcionalidade adicionada
  • Nesse momento, o caminho mais rápido não é insistir mais, e sim voltar atrás
    • Fazer inline do código abstraído novamente em cada ponto de chamada, reintroduzindo a duplicação
    • Verificar, com base nos parâmetros passados por cada ponto de chamada, apenas o código que realmente é executado
    • Remover o código desnecessário para aquele ponto de chamada
  • O processo de inline elimina ao mesmo tempo a abstração e as condicionais, reduzindo cada ponto de chamada a apenas o código de que ele precisa
  • Mesmo códigos que pareciam chamar a mesma abstração talvez estivessem, na prática, executando caminhos de código bastante distintos em cada ponto de chamada
  • Só depois de remover completamente a abstração anterior é que se pode observar novamente a duplicação e extrair uma nova abstração adequada aos requisitos atuais
  • Se parâmetros e caminhos condicionais continuam sendo adicionados ao código compartilhado, há uma grande chance de que aquela abstração não seja mais adequada
    • Ela pode até ter sido a abstração correta no início
    • Mas as mudanças nos requisitos podem ter tornado inviável mantê-la no mesmo formato
  • Numa abstração errada, reintroduzir a duplicação não é retroceder, e sim avançar melhor

5 comentários

 
dieafterwork 1 시간 전

Não sei se esse é um tema que precise de uma interpretação tão binária.

 
hanje3765 2 시간 전

Ah, me identifico muito com isso.
O que não está organizado dá para organizar, mas
parece que o custo para desfazer o que já está organizado é bem maior.

 
jimmy2056 3 시간 전

O ponytail postou, então logo apareceu um texto desses, haha

 
shakespeares 4 시간 전

É sempre um antagonismo.

 
GN⁺ 5 시간 전
Comentários do Hacker News
  • Acho que o princípio de fonte única da verdade (single source of truth) deve ser sempre mantido
    Se for código duplicado que vira bug quando diverge, ele deve ser refatorado. Caso contrário, surge um acoplamento de longa distância que futuros desenvolvedores dificilmente perceberão até o bug explodir
    Ainda assim, se isso não viola esse princípio, abstração é só uma conveniência; se começou a ficar incômoda, então não está cumprindo seu papel e não há motivo para usá-la. Se uma função precisa de vários flags para ter comportamentos customizados, há uma boa chance de ser uma abstração ruim ou uma violação do princípio da responsabilidade única
    Se realmente for preciso muita customização, muitas vezes é melhor receber uma função/functor como argumento. Por exemplo, em vez de solve(f:double -> double, max_iters = 99, x_abs_tol = 1e-15, x_rel_tol = 1e-15, ...), pode ser algo como solve(f:double -> double, stopping_criteria: StoppingCriteriaClass)

    • O ponto central do texto é tratar dos casos em que ainda não está claro quantas fontes da verdade existem
      Não está claro se dois pontos do código usam o mesmo algoritmo, ou uma versão ligeiramente diferente, e mais importante ainda, se vão mudar pelo mesmo motivo
      O aforismo do título diz que forçar coisas diferentes a parecerem iguais dói mais do que duplicar coisas iguais e depois separá-las. Também acho isso correto. No segundo caso, basta fazer a mesma mudança duas vezes ou refatorar para introduzir uma abstração; no primeiro, você fica empilhando remendos na abstração ou precisa desfazê-la
      Isso é especialmente ruim porque quebra a localidade (locality), e na hora de alterar algo essa é a propriedade que realmente importa. Quero apenas fazer essa mudança sem me preocupar se vai causar efeitos colaterais em partes não relacionadas do sistema
    • Se, por pressão extrema, o software acabou sendo empurrado para duas fontes da verdade, uma abordagem bastante útil é adicionar um teste de CI que impeça merge na main quando as duas fontes não estiverem sincronizadas
      Um exemplo típico é quando sincronizar pyproject.toml / requirements.txt realmente acaba sendo a melhor opção, e isso talvez se aplique de forma mais ampla. A premissa é que a situação já saiu dos trilhos a ponto de uma fonte única da verdade ser inviável; isso é mais redução de danos do que cura
    • O critério “se divergirem, é bug” é uma ótima regra prática
      Já passei muitas vezes por situações em que dois trechos de código pareciam parecidos num certo momento, eu abstraí demais, e depois eles acabaram se separando
    • Em teoria isso está certo, mas na prática há muita gente que tenta evitar qualquer duplicação a todo custo
      Especialmente desenvolvedores juniores às vezes tratam duplicação como se fosse a raiz de todo mal
  • Penso nisso de vez em quando. Recentemente esbarrei nesse problema num projeto pessoal lidando com sprites 2D de unidades de RTS, em que os sprites das unidades estavam organizados de maneira consistente numa spritesheet: 5 sprites em 8 direções, com 3 dessas direções espelhadas, e a ordem era stand, move, attack, die
    Então fiz um loader que recebia action + direction e devolvia o array de sprites a ser reproduzido
    Só que aí apareceram sprites de explosão sem direção, sprites de cadáver com 4 direções e apenas 2 espelhamentos e, além disso, orcs e humanos compartilhavam a maior parte, exceto os quatro primeiros
    Pensei por um instante em qual seria a abstração comum para tudo isso, mas no fim apenas separei parte do código de carregamento, criei UnitLoader, CorpseLoader e EffectLoader, e segui em frente. Pode até existir uma abstração melhor, já que os três loaders lidam com coisas um pouco parecidas, mas posso descobrir isso depois. É mais fácil remover a duplicação mais tarde, na hora certa, do que tentar criar agora um EverythingLoader complexo para cobrir todos os casos

    • Gosto da frase “as coisas devem ser tão simples quanto possível, mas não mais simples do que isso”
      Em programação, existe o instinto de simplificar código por meio de generalização, mas a realidade é bagunçada, então muitas vezes simplificamos demais. Como no texto, com o tempo e com novos requisitos, fica claro que foi uma simplificação precoce demais
      Dá até para transformar isso no aforismo “abstração prematura é a raiz de muita porcaria”
    • É bem provável que a abstração comum já esteja separada. Seria o código que carrega e exibe os pixels de um sprite individual
      No nível acima disso, a interpretação do layout da spritesheet e o tratamento dos modos de reprodução têm várias variações, e talvez não exista uma abstração comum que sirva para todos os casos
      Em vez de forçar uma abstração invisível ou tentar encaixar tudo numa abstração incompleta, prefiro fazer como você fez. Esperar até que a abstração fique totalmente clara e a necessidade dela seja óbvia é uma boa coisa
      Do lado oposto do DRY existe o antídoto WET. A ideia é escrever tudo duas ou três vezes. Mais importante ainda, abstrações deveriam surgir apenas para casos de uso realmente comprovados, normalmente aqueles que primeiro apareceram como duplicação. Código escrito para casos de uso futuros que ainda não existem frequentemente atrapalha a abstração do que você realmente tem, e sempre acho engraçado quando isso acontece
    • Esse jeito está certo. Fazer jogos deveria ser divertido
      As partes difíceis e chatas podem ficar para quando você chegar aos 10% finais do projeto
      Além disso, às vezes um “bug” criado por duplicação vira um recurso engraçado de que os jogadores gostam
  • Na época em que eu usava OOP, sofria por causa de abstrações, mas depois que migrei para uma abordagem quase puramente funcional, duplicação de código ficou rara
    Basta criar uma função e chamá-la em dois lugares. A principal questão de abstração está nas estruturas de dados, mas interfaces em TypeScript são essencialmente duck typing, então aqui também isso não costuma dar muito problema
    Por isso, duplicação de código causada por problemas de abstração é rara. Duplicação de código causada por desenvolvedores em silos é muito mais comum

    • Uso linguagens funcionais por hobby, e acho que o principal a lembrar é a técnica
      A maioria das linguagens modernas consegue incorporar bem teoria de programação funcional, e não é obrigatório conhecer Haskell. Cada pessoa pensa de um jeito, mas a ideia de que peças pequenas, simples e às vezes flexíveis formam o todo funciona muito bem para mim
      É o oposto de uma máquina de transformação de formas grande, complexa e que faz tudo
    • Não é necessário que os desenvolvedores estejam realmente em silos para surgir duplicação de código
      Quando a equipe passa de certo tamanho e já não dá para cada pessoa saber o que todas as outras estão fazendo, a duplicação de código se torna bastante inevitável. Isso continua sendo verdade mesmo que todo mundo escreva em estilo funcional
      Na prática, isso aconteceu comigo na empresa no mês passado. Escrevi uma nova função helper pura e a coloquei no topo do arquivo, e uma semana depois um colega me avisou que já existia no fim do mesmo arquivo uma helper parecida, com assinatura diferente, mas funcionalmente equivalente
    • Fiquei curioso sobre o que exatamente significa “chamar a função em duas partes”
  • No mesmo contexto do texto principal, quem já passou pelos dois casos provavelmente vai concordar. Uma base de código subprojetada é muito mais fácil de lidar do que uma base de código superprojetada

  • O pior código que já tive de manter era código que tentava seguir DRY. Só que não tentava entender a intenção original desse princípio.
    A única forma de sair daquela bagunça foi reintroduzir duplicação de código em larga escala

    • Tudo bem, não se preocupe, é só adicionar mais alguns parâmetros booleanos ambíguos à função reutilizável para dar suporte ao novo caso de uso e fazer o deploy
    • O importante é que “tentamos”. Faz-se isso por um tempo até chegar ao ponto em que não dá mais para seguir fielmente, porque a abstração estava errada
  • Isso me faz lembrar de duas apresentações: Data-Oriented Design and C++ [1], do Mike Acton, e The Complexity of Simplicity [2], do Brian Cantrill
    A apresentação do Mike diz que uma solução de código não precisa modelar o mundo real, que dados diferentes criam problemas diferentes e, portanto, exigem soluções diferentes. É difícil transmitir bem o suficiente a palestra, mas ela me influenciou muito
    A apresentação do Brian trata da abstração em geral e de como é difícil encontrar a abstração “correta”

    1. https://www.youtube.com/watch?v=rX0ItVEVjHc
    2. https://www.youtube.com/watch?v=Cum5uN2634o
    • Sempre achei estranho que até engenheiros bem inteligentes às vezes priorizem metáforas do mundo real acima das necessidades reais da base de código
      Anos atrás, pouco tempo depois de sair da faculdade, eu estava implementando um pool de conexões em Rust, e a implementação mais sensata era fazer o objeto de conexão manter uma referência fraca ao pool para que ele fosse devolvido automaticamente quando desse drop
      Meu gerente, que era muito experiente, não gostou da ideia porque “a biblioteca segura o livro; o livro não segura a biblioteca”
      Não achei que isso fosse um motivo convincente o bastante para mudar o design, mas ele não queria tratar o problema sem passar pela lente dessa metáfora
      No fim, o impasse só se resolveu quando outro gerente sugeriu: “o livro da biblioteca não contém a biblioteca, mas atrás tem um carimbo com o nome da biblioteca que indica onde devolvê-lo”. Esse gerente aparentemente achou essa extensão da analogia razoável
      Se eu fosse mais experiente na época, talvez tivesse encontrado uma forma de conversar dentro dessa metáfora sem ceder no ponto principal, mas até hoje me pareceu totalmente bizarro insistir nessa metáfora como estrutura padrão, em vez de considerar as abstrações do código e os efeitos na experiência de uso da biblioteca
  • Ninguém quer ouvir. Ninguém mesmo. Em 90% das empresas há o chamado desenvolvedor sênior que fica em êxtase ao criar uma nova abstração
    Superprojeto, abstração e otimização prematura são os três grandes desastres da engenharia
    Ao mesmo tempo, fico feliz que eles existam, porque assim sempre haverá trabalho

    • Kubernetes, mais microsserviços do que engenheiros, protocolos complexos para economizar alguns bytes de overhead, tudo na nuvem e inúmeras classes que poderiam muito bem ser apenas funções simples são exemplos perfeitos disso
  • De forma parecida, alguns desenvolvedores parecem achar que toda string inline ou constante numérica é um mal absoluto. Vi isto em um PR
    HTTPS_SCHEME = 'https'
    DOMAIN = 'www.example.com'
    url = HTTPS_SCHEME + '://' + DOMAIN
    Não vejo o que isso ganha além de seguir “não coloque constantes hardcoded” como um cargo cult. Além disso, as definições das constantes estavam no topo do arquivo, e o código que montava a URL estava a centenas de linhas de distância

    • Eu valorizo muito proximidade no código. Prefiro definir as coisas o mais perto possível de onde são usadas. Isso é um hábito que realmente me irrita
      Regex também não precisa ficar no topo do arquivo; pode ficar onde é usada. A linguagem é inteligente e provavelmente vai perceber que é uma constante
      Se for uma função muito pequena, é só usar uma lambda. Preferiria que não criassem uma função de uma linha, usada uma ou duas vezes, em algum lugar muito distante
    • Colocar constantes no topo facilita a customização. Isso vale ainda mais se esse arquivo for copiado
      Se em testes ou staging for preciso trocar https por http, separar o esquema e o domínio e manter as constantes em cima ou em outro arquivo faz sentido. Também importa se url é montada em vários lugares ou só em um
      Colocar constantes nomeadas no topo do arquivo é um estilo extremamente comum e, às vezes, faz parte do padrão de código da equipe
      Pode haver outros motivos, então vale lembrar de Chesterton’s Fence. De todo modo, não é uma boa ideia concluir de cara que é cargo cult. Alguém também poderia dizer que usar literais inline é cargo cult do mesmo jeito. Se parecer estranho, pergunte: pode haver um bom motivo, ou talvez ninguém tenha pensado muito nisso e até fiquem felizes se você refatorar e inline as constantes
    • Eu também já passei por isso. Se um Event tem nome, dá para fazer grep imediatamente em todo um monólito gigante ou em um conjunto de repositórios de microsserviços e encontrar todos os arquivos relacionados àquele evento
      Se você extrai isso para uma constante, aí precisa abrir os projetos um por um e usar Find Usages de novo
  • Com microsserviços, dá para ter as duas coisas

    • Sei que é brincadeira, mas em um mundo ideal de microsserviços não existe o conceito de duplicação de código entre serviços
      Se você mantém um serviço, não tem motivo para se importar com o código que está em outro. É código de outro time; por que se importar? Você nem precisa saber que aquele time existe. Em sistemas grandes, às vezes nem é realisticamente possível conhecer a existência de todas as aplicações
    • Mas espere! Tem mais!
      Por apenas $19.95, transformamos um ponto único de falha em vários pontos únicos de falha!
    • Em 9 de cada 10 casos, os microsserviços acabam ficando fortemente acoplados e viram um monólito distribuído
      É melhor usar arquitetura orientada a serviços, mas simplesmente fazer o deploy do monólito. Fica mais fácil de testar e você ainda evita a camada extra de serialização/desserialização
  • A maioria dos profissionais sêniores sabe que não se deve seguir o DRY cegamente. Mesmo assim, muitos de nós se sentem desconfortáveis com a ideia de manter várias fontes de código duplicadas
    Para lidar com isso, é preciso examinar de perto o modelo simples em que dois chamadores dependem de código em comum. Se o código compartilhado precisa mudar por causa da necessidade de apenas um dos chamadores, então esse código não pertence ao que é comum
    O objetivo equivocado do DRY é tentar resolver isso com encapsulamento. O encapsulamento transfere o trabalho de refatoração dos chamadores para o código comum. Mas isso não é a direção desejada, porque o impacto de atualizar o código comum é muito maior do que o de atualizar os chamadores
    É possível seguir o DRY sem recorrer ao encapsulamento. É melhor ter várias abstrações finas das quais os chamadores precisam estar cientes. Em OOP, isso envolve aprender SRP e IoC; na programação procedural, isso aparece naturalmente na forma de chamar uma série de funções auxiliares