
Quando se fala em "código incorreto", as pessoas quase certamente querem dizer "código complexo", entre outros problemas populares. A questão da complexidade é que ela surge do nada. Um dia você inicia seu projeto bastante simples, outro dia você o encontra em ruínas. E ninguém sabe como e quando isso aconteceu.
Mas, isso finalmente acontece por uma razão! A complexidade do código entra na sua base de código de duas maneiras possíveis: com grandes partes e adições incrementais. E as pessoas são ruins em revisar e encontrar os dois.
Quando um grande pedaço de código entra, o revisor será desafiado a encontrar o local exato em que o código é complexo e o que fazer a respeito. Então, a revisão terá que provar o ponto: por que esse código é complexo em primeiro lugar. E outros desenvolvedores podem discordar. Todos nós conhecemos esse tipo de revisão de código!

A segunda maneira de complexidade que entra no seu código é a adição incremental: quando você envia uma ou duas linhas à função existente. E é extremamente difícil perceber que sua função estava correta há um commit atrás, mas agora é muito complexa. É preciso uma boa parte da concentração, revisão de habilidades e boas práticas de navegação de código para realmente identificá-lo. A maioria das pessoas (como eu!) Não tem essas habilidades e permite que a complexidade entre regularmente na base de código.
Então, o que pode ser feito para impedir que seu código fique complexo? Precisamos usar a automação! Vamos aprofundar a complexidade do código e as maneiras de encontrá-lo e finalmente resolvê-lo.
Neste artigo, vou guiá-lo por lugares onde a complexidade vive e como combatê-la lá. Em seguida, discutiremos o quão bem o código e a automação simples escritos possibilitam uma oportunidade dos estilos de desenvolvimento "Refatoração contínua" e "Arquitetura sob demanda".
Complexidade explicada
Alguém pode perguntar: o que exatamente é "complexidade de código"? E embora pareça familiar, existem obstáculos ocultos na compreensão da localização exata da complexidade. Vamos começar com as partes mais primitivas e depois passar para entidades de nível superior.
Lembre-se, que este artigo se chama "Cachoeira da Complexidade"? Mostrarei como a complexidade das primitivas mais simples transborda para as abstrações mais altas.
wemake-python-styleguide
python
como a linguagem principal dos meus exemplos e wemake-python-styleguide
como a principal ferramenta de aprendizado para encontrar as violações no meu código e ilustrar meu argumento.
Expressões
Todo o seu código consiste em expressões simples como a + 1
e print(x)
. Embora as expressões sejam simples, elas podem transbordar seu código com complexidade em algum momento. Exemplo: imagine que você tenha um dicionário que represente algum modelo de User
e use-o assim:
def format_username(user) -> str: if not user['username']: return user['email'] elif len(user['username']) > 12: return user['username'][:12] + '...' return '@' + user['username']
Parece bem simples, não é? De fato, ele contém dois problemas de complexidade baseados em expressões. Ele usa a string overuses 'username'
e usa o número mágico 12
(por que usamos esse número em primeiro lugar, por que não 13
ou 10
?). É difícil encontrar esse tipo de coisa sozinho. Veja como seria a versão melhor:
Existem diferentes problemas com a expressão também. Também podemos ter expressões some_object.some_attr
: quando você usa o atributo some_object.some_attr
qualquer lugar, em vez de criar uma nova variável local. Também podemos ter condições lógicas muito complexas ou acesso a pontos muito profundo .
Solução : crie novas variáveis, argumentos ou constantes. Crie e use novas funções ou métodos utilitários, se necessário.
Linhas
Expressões formam linhas de código (por favor, não confunda linhas com instruções: uma única instrução pode ter várias linhas e várias instruções podem estar localizadas em uma única linha).
A primeira e a métrica de complexidade mais óbvia para uma linha é o seu comprimento. Sim, você ouviu corretamente. É por isso que nós (programadores) preferimos manter a regra de 80
caracteres por linha e não porque ela era usada anteriormente nas máquinas de escrever teletipagem. Ultimamente, existem muitos rumores sobre isso, dizendo que não faz sentido usar 80
caracteres para o seu código em 2k19. Mas, obviamente, isso não é verdade.
A ideia é simples. Você pode ter o dobro de lógica em uma linha com 160
caracteres do que em apenas 80
caracteres. É por isso que esse limite deve ser definido e imposto. Lembre-se, essa não é uma escolha estilística . É uma métrica de complexidade!
A segunda métrica de complexidade da linha principal é menos conhecida e menos utilizada. Chama-se Jones Complexity . A idéia por trás disso é simples: contamos nós de código (ou ast
) em uma única linha para obter sua complexidade. Vamos dar uma olhada no exemplo. Essas duas linhas são fundamentalmente diferentes em termos de complexidade, mas têm exatamente a mesma largura em caracteres:
print(first_long_name_with_meaning, second_very_long_name_with_meaning, third) print(first * 5 + math.pi * 2, matrix.trans(*matrix), display.show(matrix, 2))
Vamos contar os nós no primeiro: uma chamada, três nomes. Quatro nós totalmente. O segundo tem vinte e um nós ast
. Bem, a diferença é clara. É por isso que usamos a métrica Jones Complexity para permitir a primeira linha longa e proibir a segunda com base em uma complexidade interna, não apenas no comprimento bruto.
O que fazer com linhas com uma pontuação alta na Jones Complexity?
Solução : divida-as em várias linhas ou crie novas variáveis intermediárias, funções utilitárias, novas classes etc.
print( first * 5 + math.pi * 2, matrix.trans(*matrix), display.show(matrix, 2), )
Agora é muito mais legível!
Estruturas
O próximo passo é analisar as estruturas de linguagem, como if
, for
, with
, etc, formadas a partir de linhas e expressões. Eu tenho que dizer que este ponto é muito específico da linguagem. Vou mostrar várias regras dessa categoria usando python
também.
Vamos começar com if
. O que pode ser mais fácil do que um bom if
? Na verdade, if
começar a ficar complicado muito rápido. Aqui está um exemplo de como se pode reimplement switch
if
:
if isinstance(some, int): ... elif isinstance(some, float): ... elif isinstance(some, complex): ... elif isinstance(some, str): ... elif isinstance(some, bytes): ... elif isinstance(some, list): ...
Qual é o problema com este código? Bem, imagine que temos dezenas de tipos de dados que devem ser cobertos, incluindo os costumes dos quais ainda não temos conhecimento. Então esse código complexo é um indicador de que estamos escolhendo um padrão errado aqui. Precisamos refatorar nosso código para corrigir esse problema. Por exemplo, pode-se usar typeclass
es ou singledispatch
. Eles fazem o mesmo trabalho, mas melhor.
python
nunca para para nos divertir. Por exemplo, você pode escrever with
um número arbitrário de casos , o que é muito complexo mentalmente e confuso:
with first(), second(), third(), fourth(): ...
Você também pode escrever compreensões com qualquer número de expressões if
e for
, o que pode levar a códigos complexos e ilegíveis:
[ (x, y, z) for x in x_coords for y in y_coords for z in z_coords if x > 0 if y > 0 if z > 0 if x + y <= z if x + z <= y if y + z <= x ]
Compare-o com a versão simples e legível:
[ (x, y, z) for x, y, x in itertools.product(x_coords, y_coords, z_coords) if valid_coordinates(x, y, z) ]
Você também pode incluir acidentalmente multiple statements inside a try
caso de multiple statements inside a try
, o que não é seguro, pois pode gerar e manipular uma exceção em um local esperado:
try: user = fetch_user()
E isso não é nem 10% dos casos que podem e vão dar errado com seu código python
. Existem muitos, muitos outros casos extremos que devem ser rastreados e analisados.
Solução : A única solução possível é usar um bom ponteiro para o idioma de sua escolha. E refatorar lugares complexos que esse linter destaca. Caso contrário, você precisará reinventar a roda e definir políticas personalizadas para exatamente os mesmos problemas.
Funções
Expressões, instruções e estruturas formam funções. A complexidade dessas entidades flui para funções. E é aí que as coisas começam a ficar intrigantes. Porque as funções têm literalmente dezenas de métricas de complexidade: boas e ruins.
Começaremos com os mais conhecidos: complexidade ciclomática e comprimento da função medidos em linhas de código. A complexidade ciclomática indica quantas voltas seu fluxo de execução pode levar: é quase igual ao número de testes de unidade necessários para cobrir completamente o código-fonte. É uma boa métrica, porque respeita a semântica e ajuda o desenvolvedor a refatorar. Por outro lado, o comprimento de uma função é uma métrica ruim. Não combina com a métrica Jones Complexity explicada anteriormente, já que já sabemos: várias linhas são mais fáceis de ler do que uma grande linha com tudo o que está dentro. Vamos nos concentrar apenas nas boas métricas e ignorar as ruins.
Com base na minha experiência, várias métricas úteis de complexidade devem ser contadas em vez do tamanho da função regular:
- Número de decoradores de funções; menor é melhor
- Número de argumentos; menor é melhor
- Número de anotações; maior é melhor
- Número de variáveis locais; menor é melhor
- Número de retornos, rendimentos, aguarda; menor é melhor
- Número de declarações e expressões; menor é melhor
A combinação de todas essas verificações permite realmente escrever funções simples (todas as regras também são aplicadas aos métodos).
Quando você tentar fazer algumas coisas desagradáveis com sua função, certamente quebrará pelo menos uma métrica. E isso irá decepcionar o nosso linter e explodir sua construção. Como resultado, sua função será salva.
Solução : quando uma função é muito complexa, a única solução que você tem é dividir essa função em várias.
Aulas
O próximo nível de abstração após as funções são classes. E como você já adivinhou, eles são ainda mais complexos e fluidos do que funções. Como as classes podem conter várias funções internas (chamadas método) e ter outros recursos exclusivos, como herança e mixins, atributos no nível da classe e decoradores no nível da classe. Portanto, temos que verificar todos os métodos como funções e o próprio corpo da classe.
Para as aulas, precisamos medir as seguintes métricas:
- Número de decoradores em nível de classe; menor é melhor
- Número de classes base; menor é melhor
- Número de atributos públicos em nível de classe; menor é melhor
- Número de atributos públicos no nível da instância; menor é melhor
- Número de métodos; menor é melhor
Quando qualquer uma dessas situações é excessivamente complicada - precisamos tocar o alarme e falhar na construção!
Solução : refatorar sua classe com falha! Divida uma classe complexa existente em várias simples ou crie novas funções utilitárias e use a composição.
Menção notável: também é possível rastrear métricas de coesão e acoplamento para validar a complexidade do seu projeto de POO.
Módulos
Os módulos contêm várias instruções, funções e classes. E como você já mencionou, geralmente aconselhamos dividir funções e classes em novas. É por isso que temos que ficar de olho na complexidade do módulo: ele literalmente flui para módulos de classes e funções.
Para analisar a complexidade do módulo, precisamos verificar:
- O número de importações e nomes importados; menor é melhor
- O número de classes e funções; menor é melhor
- A complexidade média de funções e classes dentro; menor é melhor
O que fazemos no caso de um módulo complexo?
Solução : sim, você acertou. Dividimos um módulo em vários.
Pacotes
Pacotes contêm vários módulos. Felizmente, isso é tudo o que eles fazem.
Portanto, o número de módulos em um pacote pode em breve começar a ser muito grande, então você acabará com muitos deles. E é a única complexidade que pode ser encontrada com os pacotes.
Solução : você precisa dividir pacotes em subpacotes e pacotes de diferentes níveis.
Efeito cascata complexidade
Agora, cobrimos quase todos os tipos possíveis de abstração em sua base de código. O que aprendemos com isso? O principal argumento, por enquanto, é que a maioria dos problemas pode ser resolvida com a ejeção da complexidade para o mesmo nível ou para o nível de abstração superior.

Isso nos leva à idéia mais importante deste artigo: não deixe seu código exceder a complexidade. Vou dar vários exemplos de como isso geralmente acontece.
Imagine que você está implementando um novo recurso. E essa é a única alteração que você faz:
Parece ok, eu passaria esse código na revisão. E nada de ruim aconteceria. Mas o ponto que estou perdendo é que a complexidade ultrapassou essa linha! É isso que o wemake-python-styleguide
relatará:

Ok, agora temos que resolver essa complexidade. Vamos fazer uma nova variável:
class Product(object): ... def can_be_purchased(self, user_id) -> bool: ... is_sub_paid = sub.is_due(tz.now() + delta) if user.is_active and user.has_sub() and is_sub_paid: ... ... ...
Agora, a complexidade da linha está resolvida. Mas espere um pouco. E se a nossa função tiver muitas variáveis agora? Porque criamos uma nova variável sem verificar seu número dentro da função primeiro. Nesse caso, teremos que dividir esse método em vários como este:
class Product(object): ... def can_be_purchased(self, user_id) -> bool: ... if self._has_paid_sub(user, sub, delta): ... ... def _has_paid_sub(self, user, sub, delta) -> bool: is_sub_paid = sub.is_due(tz.now() + delta) return user.is_active and user.has_sub() and is_sub_paid ...
Agora terminamos! Certo? Não, porque agora precisamos verificar a complexidade da classe Product
. Imagine que agora ele possui muitos métodos, pois criamos um novo _has_paid_sub
.
Ok, executamos nosso linter para verificar a complexidade novamente. Acontece que nossa classe de Product
é realmente muito complexa no momento. Nossas ações? Dividimos em várias classes!
class Policy(object): ... class SubcsriptionPolicy(Policy): ... def can_be_purchased(self, user_id) -> bool: ... if self._has_paid_sub(user, sub, delta): ... ... def _has_paid_sub(self, user, sub, delta) -> bool: is_sub_paid = sub.is_due(tz.now() + delta) return user.is_active and user.has_sub() and is_sub_paid class Product(object): _purchasing_policy: Policy ... ...
Por favor, diga-me que é a última iteração! Sinto muito, mas agora temos que verificar a complexidade do módulo. E adivinhe? Agora temos muitos membros do módulo. Então, temos que dividir os módulos em outros! Em seguida, verificamos a complexidade do pacote. E também possivelmente dividi-lo em vários subpacotes.
Você viu isso? Devido às regras de complexidade bem definidas, nossa modificação de linha única acabou sendo uma enorme sessão de refatoração com vários novos módulos e classes. E não tomamos uma única decisão: todos os nossos objetivos de refatoração foram conduzidos pela complexidade interna e pelo prazo que a revela.
É o que chamo de processo de "refatoração contínua". Você é forçado a fazer a refatoração. Sempre.
Esse processo também tem uma consequência interessante. Ele permite que você tenha "Architecture on Demand". Deixe-me explicar. Com a filosofia "Architecture on Demand", você sempre começa pequeno. Por exemplo, com um único arquivo logic/domains/user.py
E você começa a colocar tudo relacionado ao User
lá. Porque neste momento você provavelmente não sabe como será sua arquitetura. E você não se importa. Você tem apenas três funções.
Algumas pessoas caem na armadilha da arquitetura versus a complexidade do código. Eles podem complicar demais sua arquitetura desde o início com as camadas completas de repositório / serviço / domínio. Ou eles podem complicar demais o código-fonte sem uma separação clara. Lute e viva assim por anos (se eles conseguirem viver por anos com o código assim!).
O conceito "Arquitetura sob demanda" resolve esses problemas. Você começa pequeno, quando chega a hora - você divide e refatora as coisas:
- Você começa com
logic/domains/user.py
e coloca tudo lá - Mais tarde, você cria
logic/domains/user/repository.py
quando tiver coisas relacionadas ao banco de dados suficientes - Em seguida, divida-o em
logic/domains/user/repository/queries.py
e logic/domains/user/repository/commands.py
quando a complexidade solicitar que você faça isso - Então você cria
logic/domains/user/services.py
com coisas relacionadas ao http
- Em seguida, você cria um novo módulo chamado
logic/domains/order.py
- E assim por diante
É isso. É uma ferramenta perfeita para equilibrar sua arquitetura e complexidade de código. E obtenha a arquitetura que você realmente precisa no momento.
Conclusão
O linter bom faz muito mais do que encontrar vírgulas ausentes e aspas ruins. Um bom linter permite confiar nele nas decisões de arquitetura e ajudá-lo no processo de refatoração.
Por exemplo, wemake-python-styleguide
pode ajudá-lo com a complexidade do código-fonte python
, permitindo:
- Lute com sucesso contra a complexidade em todos os níveis
- Aplique a enorme quantidade de padrões de nomes, práticas recomendadas e verificações de consistência
- Integre-o facilmente a uma base de código herdada com a ajuda da opção
diff
ou da ferramenta flakehell
, para que as antigas violações sejam perdoadas, mas novas não serão permitidas - Ative-o no seu [CI] (), mesmo como uma ação do Github
Não deixe a complexidade estourar seu código, use um bom linter !