Criar uma aplicação na qual a execução de rotinas de manutenção é uma tarefa simples e objetiva não é uma atividade das mais fáceis e o problema torna-se ainda maior quando essa aplicação precisa de uma arquitetura escalável onde diversas mudanças no código irão ocorrer quase que de forma constante de acordo com as demandas de mercado. Quando não possuímos experiência no desenvolvimento de aplicações em um paradigma específico, tal como funcional ou orientado a objetos, é normal que nossa aplicação fique com um código-fonte muito limpo e simples durante os estágios iniciais de desenvolvimento, mas conforme novas funcionalidades são adicionadas o código começa a se tornar cada vez mais difícil de entender e consequentemente de se manter. O código-fonte de uma aplicação nasce como uma cidade e cresce da mesma forma, com pessoas diferentes juntas trabalhando para construir coisas ao mesmo tempo, que estarão de uma forma ou outra entre si conectadas. Em uma cidade onde falta um plano de desenvolvimento sustentável é normal que após um tempo a locomoção torne-se inviável ou ao menos difícil, junto também com a modificação das estruturas existentes e adição de novas estruturas, enquanto em uma cidade com um bom planejamento o crescimento não é um inimigo da qualidade de vida.

Definindo o que é um código complexo

Complexidade é tudo aquilo que torna um software difícil de se manter ou adicionar novas funcionalidades. Um software criado para detectar mísseis balísticos que se aproximam do espaço aéreo de um país são complexos devidos à natureza do problema, portanto essa não é a sua inimiga. Quando tratamos de complexidade de software é importante saber diferenciar aquilo que é a complexidade natural do problema e aquilo que é a complexidade adicionada e também a complexidade consequente. A complexidade adicionada ocorre quando são criadas mais estruturas na aplicação para resolver o problema do que aquilo que é necessário, esse tipo de complexidade é fruto do overengineering. A complexidade consequente surge pelo motivo oposto, isto é, quando você precisa fazer determinadas modificações na aplicação que não são suportadas pela sua estrutura, que fora planejada com demasiada simplicidade enquanto uma maior robustez era necessária. Quando uma estrutura é criada de forma demasiada simples é normal que as mudanças subsequentes tornem-se mais complexas do que deveriam a cada novo evento de mudança que ocorre, até chegar ao ponto de que não é mais possível de se manter a aplicação de forma sustentável. Quando o nível de complexidade da sua aplicação está desbalanceado em relação à complexidade natural, seja para mais ou para menos, os seguintes sintomas passam a se tornar recorrente durante suas atividades:

  • Change amplification: quando você precisa fazer alterações em diversos pontos da aplicação para introduzir uma mudança simples. É muito importante que seu código seja coeso, e que os pontos que relacionados uns aos outros estejam próximos e sejam fáceis de ser encontrados por quem quer que seja que esteja realizando a manutenção.
  • Cognitive load: quanto mais conhecimento específico sobre o sistema é necessário para fazer uma mudança mais complexa a aplicação se torna de se manter, por isso é muito importante que os design patterns sejam aplicados sempre que possível, pois esta é uma linguagem universal utilizada por engenheiros de todo o mundo para resolver problemas padrão. Aplique design patterns em tudo onde for plausível e limite-se à receitas secretas de família somente onde for necessário.
  • Unknown unknowns: geralmente você precisa saber de algumas coisas para realizar a manutenção de uma aplicação, e este fenômeno ocorre quando você sequer sabe que coisas são essas que você precisa saber. Um software precisa ser dedutível, capaz de facilmente ser analisado através da engenharia reversa. Quando mais fácil for realizar engenharia reversa do seu código, melhor, e isso inclui material de apoio e documentação, entretanto esforce-se para que seu código seja tão fácil de ser compreendido que materiais de apoio se tornarão desnecessários. Um bom material de apoio para desenvolver-se nesse sentido é o livro Clean Code.

E por fim, a complexidade de um código-fonte não é determinada pelo autor do código, mas sim pelo leitor, portanto a opinião de quem escreve um código não é muito relevante, pois seja la qual foi o erro cometido pelo autor ele ao menos tem a informação necessária para navegar em seus próprios erros, enquanto um leitor descontextualizado precisa aprender tudo que o autor já sabe de antemão.

É muito difícil acertar a arquitetura de primeira

O primeiro passo para construção de uma aplicação com uma boa arquitetura é entender que se você está lidando com um problema pela primeira vez há uma grande chance de você não acertar de primeira. Se você está desenvolvendo uma aplicação para cobrança de dívidas por exemplo e essa é sua primeira vez com uma aplicação do gênero você irá obter determinadas informações por parte do seu cliente ou chefe com as especificações do problema, mas assim que a aplicação entrar em produção e os usuários começarem a testar as funcionalidades você irá descobrir coisas novas junto do seu product owner que ambos não esperavam. É uma tendência natural que um sistema de informação irá replicar as estruturas de relacionamento humanas existentes entre seus usuários e essas estruturas humanas são complexas de se presumir pois elas surgem das mais variadas formas e pelos mais variados motivos. Imagine que você irá emitir cobranças e existe uma taxa de juros que é praticada para determinados seguimentos, mas para uma loja específica existe um contrato diferente, pois o dono da empresa que está a contratar seu serviço também é sócio da outra empresa a ser cobrada. Essa é uma estrutura de relacionamento hipotética, mas elas podem vir a existir pelos mais variados motivos e você precisará de um determinado tempo de profissão para aprender quais são as possíveis formas de se fazer negócio na área que você está atuando, portanto, esteja preparado para jogar código-fonte fora e trabalhar duro na reescrita de módulos a camadas da sua aplicação sempre que necessário para evitar que as coisas cresçam de forma descontrolada.

Agilidade é mais importante que velocidade

Nem tudo que é veloz é necessariamente ágil, um exemplo grotesco é o foguete que leva os astronautas à estação espacial internacional, ele é extremamente veloz, mas não é mais ágil do que uma bicicleta. Sua velocidade é determinada pelo tempo que você leva para chegar de um ponto ao outro, enquanto sua agilidade é determinada pelo tempo que você leva para se ajustar à uma mudança no seu trajeto de forma que poderá desenvolver sua velocidade máxima no menor espaço de tempo possível novamente. Sempre que os objetivos dos seus usuários mudam seu software também necessitará mudança e quanto menos ágil você e seu time forem maiores serão as chances de que vocês irão precisar implementar uma solução inadequada. Uma equipe ágil busca sempre se antecipar aos problemas mantendo-se atenta ao que está acontecendo no mercado no qual ela está inserida, no seu contexto corporativo, na demografia dos seus usuários e assim por diante. Quanto mais ágil sua equipe se tornar vocês poderão responder com maior precisão e força às mudanças necessárias. Lembre-se que a agilidade não se refere só ao caso de ser ágil em reescrever o código, mas também se refere à agilidade com a qual sua equipe é capaz de assimilar o problema, planejar a alteração necessária e realizar a execução. Normalmente equipes que não desenvolvem agilidade de assimilação e planejamento costumam pular essas etapas, o que contribui para a evolução da complexidade do código fonte.

Testes unitários são mais do que necessários

Além de garantir a que as coisas estão funcionando, quando bem escritos os testes unitários irão também atuar como uma documentação das funcionalidades do seu sistema, explicando como as coisas deveriam e não funcionar. Embora eles não sejam muito eficientes para explicar o motivo de as coisas serem daquela forma, uma tarefa para a engenharia de requisitos, eles são ótimos para descrever o estado atual das coisas. Os testes unitários irão ajudar você a descobrir informações necessárias sobre o funcionamento do sistema, evitando os unknown unknowns, e também irá te ajudar a ter uma noção científica a respeito do nível de change amplification do seu software. Além disso os testes unitários evitam a complexidade consequente, fruto do processo da criação de estruturas simplistas demais, uma vez que para escrever software possível de ser testado é necessário que determinados padrões de engenharia sejam seguidos.

Modularize seu sistema

Do ponto de vista do leitor, você acha que é mais fácil adquirir o conhecimento que você procura em uma livraria onde tudo é catalogado e devidamente categorizado ou em uma onde os livros estão aleatoriamente dispostos nas prateleiras? Com certeza é mais fácil para quem está colocando os livros no lugar não se importar em colocar no lugar certo, mas para quem precisa se servir do acervo encontrar a informação que deseja se torna um processo doloroso. Quando o assunto é software as coisas não são diferentes, por isso inclusive utilizamos o termo library para nos referir à uma coleção de códigos.

Além de deixar o seu código mais organizado em categorias e subcategorias a modularização irá lhe proporcionar um efeito chamado information hiding, que é quando classes e módulos tratam de problemas específicos de forma centralizada não fazendo com que outras partes do seu código precisem ter essa informação, para não confundir as coisas é importante ter em mente que a obscuridade surge quando você não consegue encontrar uma informação que busca, enquanto information hiding tem como objetivo esconder as informações desnecessárias de estarem presentes em um determinado trecho de código.

Imagine que você está indo para o módulo de cadastro de usuário de uma aplicação, naquele módulo você está procurando detalhes referentes somente ao cadastro dos usuários, não quer saber de outros detalhes tais como taxas de comissão, regras de autenticação e assim por diante. Esses outros devem estar centralizados em seus módulos específicos. A modularização torna-se muito eficiente nesse sentido quando você está desenvolvendo um sistema que cuida de diversas áreas de processos dentro uma mesma empresa e você divide seus módulos baseando-se em nessas áreas, assim sua regra de negócio ficará organizada na aplicação de forma coerente. Além disso, David Parnas têm um artigo muito bom onde ele busca definir os critérios para modularização de uma aplicação com o objetivo de aumento de produtividade. Apesar de o artigo ter sido publicado na década de 70 este continua ainda muito atual.

Evite classes muito pequenas ou extensas

Quanto maior for uma classes mais informações ela irá esconder que outras classes não precisam saber, possuir classes com diversos métodos não é algo necessariamente ruim, entretanto como uma regra geral é importante que suas classes não possuam mais do que duas mil linhas ao mesma tempo que não devem possuir menos de duzentas linhas. Quando uma classe é demasiadamente grande é muito difícil gerenciar a sua complexidade e quando ela é muito pequena seu código está provavelmente distribuído de uma forma demasiadamente granular, o que por sua vez irá aumentar a obscuridade da sua aplicação. Encontrar um bom balanceamento do que deve estar escondido em suas classes e módulos é uma tarefa que exige um bom julgamento do desenvolvedor e difícil de por em um padrão, mas uma vez que você tem em mente qual objetivo busca-se alcançar com esse princípio então você estará pronto para fazer o julgamento.

Leave a comment

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: