As abstrações precisam reduzir a legibilidade do código?

19

Um bom desenvolvedor com quem trabalho me falou recentemente sobre algumas dificuldades que ele teve em implementar um recurso em algum código que herdamos; ele disse que o problema era que o código era difícil de seguir. A partir disso, examinei mais profundamente o produto e percebi o quão difícil era ver o caminho do código.

Usava tantas interfaces e camadas abstratas que era difícil tentar entender onde as coisas começavam e terminavam. Isso me fez pensar sobre os tempos em que eu olhei para projetos anteriores (antes de estar tão ciente dos princípios de código limpo) e achei extremamente difícil dar a volta no projeto, principalmente porque minhas ferramentas de navegação de código sempre me levavam a uma interface. Seria necessário muito esforço para encontrar a implementação concreta ou onde algo estava conectado em alguma arquitetura do tipo plug-in.

Sei que alguns desenvolvedores recusam rigorosamente os contêineres de injeção de dependência por esse mesmo motivo. Confunde tanto o caminho do software que a dificuldade de navegação de código aumenta exponencialmente.

Minha pergunta é: quando uma estrutura ou padrão introduz tanta sobrecarga como essa, vale a pena? É um sintoma de um padrão mal implementado?

Eu acho que um desenvolvedor deve olhar para o quadro geral do que essas abstrações trazem para o projeto para ajudá-lo a superar a frustração. Geralmente, porém, é difícil fazê-los ver esse quadro geral. Sei que não consegui vender as necessidades de COI e DI com TDD. Para esses desenvolvedores, o uso dessas ferramentas reduz muito a legibilidade do código.

Martin Blore
fonte

Respostas:

17

Este é realmente um longo comentário sobre a resposta de @kevin cline.

Mesmo que os próprios idiomas não causem ou impeçam necessariamente isso, acho que há algo em sua noção de que isso esteja relacionado aos idiomas (ou pelo menos às comunidades de idiomas) até certo ponto. Em particular, mesmo que você possa encontrar o mesmo problema em idiomas diferentes, geralmente ele assume formas bastante diferentes em idiomas diferentes.

Apenas por exemplo, quando você se depara com isso em C ++, é provável que isso seja menos resultado de muita abstração e mais resultado de muita esperteza. Apenas por exemplo, o programador ocultou a transformação crucial que está acontecendo (que você não consegue encontrar) em um iterador especial; portanto, o que parece ser apenas copiar dados de um lugar para outro realmente tem vários efeitos colaterais que não têm nada a ver fazer com essa cópia dos dados. Apenas para manter as coisas interessantes, isso é intercalado com a saída criada como efeito colateral da criação de um objeto temporário no processo de conversão de um tipo de objeto para outro.

Por outro lado, quando você o encontra em Java, é muito mais provável que você veja alguma variante do bem conhecido "mundo da empresa", onde, em vez de uma única classe trivial que faz algo simples, você obtém uma classe base abstrata e uma classe derivada concreta que implementa a interface X e é criada por uma classe de fábrica em uma estrutura DI, etc. As 10 linhas de código que realizam o trabalho real estão enterradas em 5000 linhas de infraestrutura.

Parte disso depende tanto do ambiente quanto da linguagem - trabalhar diretamente com ambientes de janelas como X11 e MS Windows é notório por transformar um programa trivial do tipo "olá mundo" em mais de 300 linhas de lixo quase indecifrável. Com o tempo, desenvolvemos vários kits de ferramentas para nos isolar disso - mas 1) esses kits de ferramentas não são triviais e 2) o resultado final ainda não é apenas maior e mais complexo, mas também geralmente menos flexível do que um equivalente no modo de texto (por exemplo, mesmo que esteja imprimindo algum texto, o redirecionamento para um arquivo raramente é possível / suportado).

Para responder (pelo menos em parte) à pergunta original: pelo menos quando a vi, era menos uma má implementação de um padrão do que simplesmente aplicar um padrão inadequado à tarefa em questão - a maioria muitas vezes tentando aplicar algum padrão que pode ser útil em um programa inevitavelmente grande e complexo, mas quando aplicado a um problema menor acaba tornando-o imenso e complexo, mesmo que, nesse caso, o tamanho e a complexidade sejam realmente evitáveis .

Jerry Coffin
fonte
7

Acho que isso geralmente é causado por não adotar uma abordagem YAGNI . Tudo passando por interfaces, mesmo que exista apenas uma implementação concreta e nenhum plano atual para apresentar outras, é um excelente exemplo de como adicionar complexidade que você não precisará. Provavelmente é heresia, mas sinto o mesmo sobre o uso da injeção de dependência.

Carson63000
fonte
+1 por mencionar YAGNI e abstrações com pontos de referência únicos. O papel principal de fazer uma abstração é fatorar o ponto comum de várias coisas. Se uma abstração é referenciada apenas a partir de um ponto, não podemos falar em fatorar coisas comuns, uma abstração como essa apenas contribui para o problema ioiô. Gostaria de estender isto porque isto é verdade para todos os tipos de abstrações: funções, genéricos, macros, o que quer ...
Calmarius
3

Bem, não há abstração suficiente e seu código é difícil de entender porque você não pode isolar quais partes fazem o que.

Abstração em excesso e você vê a abstração, mas não o código em si, e torna difícil seguir o segmento de execução real.

Para obter uma boa abstração, deve-se o KISS: veja minha resposta a essas perguntas para saber o que seguir para evitar esse tipo de problema .

Penso que evitar hierarquia profunda e nomeação é o ponto mais importante a ser observado no caso que você descreve. Se as abstrações fossem bem nomeadas, você não precisaria ir muito fundo, apenas para o nível de abstração em que precisa entender o que acontece. A nomeação permite identificar onde está esse nível de abstração.

O problema surge no código de baixo nível, quando você realmente precisa que todo o processo seja entendido. Então, o encapsulamento através de módulos claramente isolados é a única ajuda.

Klaim
fonte
3
Bem, não há abstração suficiente e seu código é difícil de entender porque você não pode isolar quais partes fazem o que. Isso é encapsulamento, não abstração. Você pode isolar peças em classes concretas sem muita abstração.
Declaração
Classes não são as únicas abstrações que estamos usando: funções, módulos / bibliotecas, serviços, etc. Em suas aulas, você geralmente abstrai cada funcionalidade por trás de uma função / método, que pode chamar outro método que abstraia cada outra funcionalidade.
22411 Klaim
1
@ Declaração: Encapsular dados é obviamente uma abstração.
Ed S.
Hierarquias de namespace são realmente legais, no entanto.
JAB
2

Para mim, é um problema de acoplamento e está relacionado à granularidade do design. Até a forma mais vaga de acoplamento introduz dependências de uma coisa para outra. Se isso for feito para centenas a milhares de objetos, mesmo que sejam todos relativamente simples, adira ao SRP e mesmo que todas as dependências fluam para abstrações estáveis, isso gera uma base de código que é muito difícil de raciocinar como um todo inter-relacionado.

Existem coisas práticas que ajudam a avaliar a complexidade de uma base de código, que não é discutida com frequência no SE teórico, como a profundidade da pilha de chamadas que você pode obter antes de chegar ao fim e a profundidade que precisa percorrer antes que possa, com muita confiança, entenda todos os possíveis efeitos colaterais que podem ocorrer nesse nível da pilha de chamadas, inclusive no caso de uma exceção.

E descobri, apenas na minha experiência, que sistemas mais planos com pilhas de chamadas mais rasas tendem a ser muito mais fáceis de raciocinar. Um exemplo extremo seria um sistema de componente de entidade em que os componentes são apenas dados brutos. Somente os sistemas têm funcionalidade e, no processo de implementação e uso de um ECS, achei o sistema mais fácil de todos os tempos pensar sobre quando bases de código complexas que abrangem centenas de milhares de linhas de código se resumem basicamente a algumas dezenas de sistemas que contém toda a funcionalidade.

Coisas demais fornecem funcionalidade

A alternativa anterior, quando trabalhei em bases de código anteriores, era um sistema com centenas a milhares de objetos, na maioria pequenos, em oposição a algumas dúzias de sistemas volumosos, com alguns objetos usados ​​apenas para passar mensagens de um objeto para outro ( Messageobjeto, por exemplo, que tinha seu própria interface pública). Isso é basicamente o que você obtém analogicamente quando reverte o ECS para um ponto em que os componentes têm funcionalidade e cada combinação única de componentes em uma entidade produz seu próprio tipo de objeto. E isso tenderá a gerar funções menores e mais simples herdadas e fornecidas por infinitas combinações de objetos que modelam pequenas idéias ( Particleobjeto vs.Physics System, por exemplo). No entanto, ele também tende a produzir um gráfico complexo de interdependências, o que dificulta o raciocínio sobre o que acontece no nível amplo, simplesmente porque existem muitas coisas na base de código que podem realmente fazer algo e, portanto, podem fazer algo errado - - tipos que não são "dados", mas tipos "objetos" com funcionalidade associada. Tipos que servem como dados puros sem funcionalidade associada não podem dar errado, pois não podem fazer nada por conta própria.

As interfaces puras não ajudam muito esse problema de compreensibilidade porque, mesmo que isso torne as "dependências em tempo de compilação" menos complicadas e ofereça mais espaço para mudanças e expansão, não torna as "dependências de tempo de execução" e as interações menos complicadas. O objeto cliente ainda acaba invocando funções em um objeto de conta concreto, mesmo que estejam sendo chamados IAccount. O polimorfismo e as interfaces abstratas têm seus usos, mas eles não separam as coisas da maneira que realmente ajuda você a raciocinar sobre todos os efeitos colaterais que poderiam ocorrer a qualquer momento. Para atingir esse tipo de dissociação efetiva, você precisa de uma base de código que tenha muito menos itens que contenham funcionalidade.

Mais dados, menos funcionalidade

Portanto, achei a abordagem do ECS, mesmo que você não a aplique completamente, para ser extremamente útil, pois transforma o que seriam centenas de objetos em apenas dados brutos com sistemas volumosos, mais grosseiramente projetados, que fornecem todo o funcionalidade. Maximiza o número de tipos de "dados" e minimiza o número de tipos de "objetos" e, portanto, minimiza absolutamente o número de lugares no sistema que podem realmente dar errado. O resultado final é um sistema muito "plano", sem um gráfico complexo de dependências, apenas sistemas para componentes, nunca vice-versa e nunca componentes para outros componentes. São basicamente muito mais dados brutos e muito menos abstrações que têm o efeito de centralizar e nivelar a funcionalidade da base de código em áreas-chave, abstrações-chave.

30 coisas mais simples não são necessariamente mais simples de raciocinar sobre mais de uma coisa mais complexa, se essas 30 coisas mais simples estiverem inter-relacionadas enquanto a coisa complexa é independente. Portanto, minha sugestão é transferir a complexidade das interações entre objetos e mais para objetos mais volumosos que não precisam interagir com mais nada para obter dissociação em massa, para "sistemas" inteiros (não monólitos e objetos divinos, veja bem, e não classes com 200 métodos, mas algo consideravelmente mais alto que um Messageou a Particleapesar de ter uma interface minimalista). E favorecer tipos de dados antigos mais simples. Quanto mais você depender desses, menos acoplamento terá. Mesmo que isso contradiga algumas idéias de SE, eu descobri que isso realmente ajuda muito.


fonte
0

Minha pergunta é: quando uma estrutura ou padrão introduz tanta sobrecarga como essa, vale a pena? É um sintoma de um padrão mal implementado?

Talvez seja um sintoma de escolher a linguagem de programação errada.

Kevin Cline
fonte
1
Não vejo como isso tem algo a ver com o idioma de escolha. Abstrações são um conceito independente de idioma de alto nível.
Ed S.
@ Ed: algumas abstrações são mais simplesmente realizáveis ​​em alguns idiomas do que em outros.
kevin Cline
Sim, mas isso não significa que você não pode escrever uma abstração perfeitamente sustentável e facilmente entendida nessas línguas. Meu argumento foi que sua resposta não responde à pergunta nem ajuda o OP de forma alguma.
Ed S.
0

A má compreensão dos padrões de design tende a ser uma das principais causas desse problema. Um dos piores que eu já vi nesse ioiô e pulando de interface para interface sem muitos dados concretos no meio foi uma extensão do Grid Control da Oracle.
Honestamente, parecia que alguém tinha um método abstrato de fábrica e um orgasmo de padrão decorador em todo o meu código Java. E isso me fez sentir tão vazio e sozinho.

Jeff Langemeier
fonte
-1

Eu também recomendaria não usar os recursos do IDE que facilitam a abstração de coisas.

Christopher Mahan
fonte