Skip to content

AlexKHecht/ComplexidadeOusterhout

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 

Repository files navigation

A Natureza da Complexidade

(Este texto é uma tradução do capítulo 2 do livro "A Philosophy of Software Design" do John Ousterhout. O livro ainda não tem tradução oficial. Esta tradução foi feita com o único objetivo de permitir que pessoas que eu conheço e que têm interesse em ler minha cópia do livro, mas não sabem inglês, tenham acesso ao conteúdo. Não há qualquer intenção de ganho monetário ou não-monetário com esta publicação) link do livro)

Este livro trata de como projetar sistemas de software para minimizar sua complexidade. O primeiro passo é entender o inimigo. Exatamente o que é "complexidade"? Como você pode dizer se um sistema é desnecessariamente complexo? O que faz os sistemas se tornarem complexos? Este capítulo abordará essas questões em um nível alto; os capítulos subsequentes mostrarão como reconhecer a complexidade em um nível mais baixo, em termos de características estruturais específicas.

A habilidade de reconhecer a complexidade é uma competência crucial de design. Ela permite que você identifique problemas antes de investir muito esforço neles, e permite que você faça boas escolhas entre alternativas. É mais fácil dizer se um design é simples do que criar um design simples, mas uma vez que você consegue reconhecer que um sistema é muito complicado, você pode usar essa habilidade para guiar sua filosofia de design em direção à simplicidade. Se um design parece complicado, tente uma abordagem diferente e veja se é mais simples. Com o tempo, você notará que certas técnicas tendem a resultar em designs mais simples, enquanto outras se correlacionam com a complexidade. Isso permitirá que você produza designs mais simples mais rapidamente.

Este capítulo também estabelece algumas suposições básicas que fornecem uma base para o resto do livro. Os capítulos posteriores tomam o material deste capítulo como dado e o usam para justificar uma variedade de refinamentos e conclusões.

Definindo Complexidade

Para os propósitos deste livro, eu defino "complexidade" de uma maneira prática. Complexidade é qualquer coisa relacionada à estrutura de um sistema de software que torna difícil entender e modificar o sistema. A complexidade pode assumir muitas formas. Por exemplo, pode ser difícil entender como um trecho de código funciona; pode exigir muito esforço para implementar uma pequena melhoria, ou pode não estar claro quais partes do sistema devem ser modificadas para fazer a melhoria; pode ser difícil corrigir um bug sem introduzir outro. Se um sistema de software é difícil de entender e modificar, então ele é complicado; se é fácil de entender e modificar, então é simples.

Você também pode pensar na complexidade em termos de custo e benefício. Em um sistema complexo, é preciso muito trabalho para implementar até mesmo pequenas melhorias. Em um sistema simples, melhorias maiores podem ser implementadas com menos esforço.

Complexidade é o que um desenvolvedor experimenta em um determinado momento ao tentar alcançar um objetivo específico. Não se relaciona necessariamente com o tamanho geral ou funcionalidade do sistema. As pessoas frequentemente usam a palavra "complexo" para descrever sistemas grandes com recursos sofisticados, mas se tal sistema é fácil de trabalhar, então, para os propósitos deste livro, ele não é complexo. Claro, quase todos os sistemas de software grandes e sofisticados são, de fato, difíceis de trabalhar, então eles também atendem à minha definição de complexidade, mas isso não precisa necessariamente ser o caso. Também é possível que um sistema pequeno e não sofisticado seja bastante complexo.

A complexidade é determinada pelas atividades que são mais comuns. Se um sistema tem algumas partes que são muito complicadas, mas essas partes quase nunca precisam ser tocadas, então elas não têm muito impacto na complexidade geral do sistema. Para caracterizar isso de uma maneira matemática rudimentar: image

A complexidade geral de um sistema (C) é determinada pela complexidade de cada parte p (cp) ponderada pela fração de tempo que os desenvolvedores gastam trabalhando nessa parte (tp). Isolar a complexidade em um lugar onde ela nunca será vista é quase tão bom quanto eliminar a complexidade inteiramente.

A complexidade é mais aparente para os leitores do que para os escritores. Se você escrever um trecho de código e ele parecer simples para você, mas outras pessoas acharem que é complexo, então ele é complexo. Quando você se encontrar em situações como essa, vale a pena sondar os outros desenvolvedores para descobrir por que o código parece complexo para eles; provavelmente há algumas lições interessantes a serem aprendidas com a desconexão entre sua opinião e a deles. Seu trabalho como desenvolvedor não é apenas criar código com o qual você possa trabalhar facilmente, mas criar código com o qual outros também possam trabalhar facilmente.

Sintomas da complexidade de software

Amplificação de mudanças: O primeiro sintoma de complexidade é que uma mudança aparentemente simples requer modificações de código em muitos lugares diferentes. Por exemplo, considere um site contendo várias páginas, cada uma exibindo um banner com uma cor de fundo. Em muitos sites antigos, a cor era especificada explicitamente em cada página, como mostrado na Figura 2.1(a). Para mudar o fundo de tal site, um desenvolvedor poderia ter que modificar manualmente cada página existente; isso seria quase impossível para um site grande com milhares de páginas. Felizmente, os sites modernos usam uma abordagem como a da Figura 2.1(b), onde a cor do banner é especificada uma vez em um lugar central, e todas as páginas individuais fazem referência a esse valor compartilhado. Com essa abordagem, a cor do banner de todo o site pode ser alterada com uma única modificação. Um dos objetivos de um bom design é reduzir a quantidade de código que é afetada por cada decisão de design, para que as mudanças de design não exijam muitas modificações de código. image Figura 2.1: Cada página em um site exibe um banner colorido. Em (a), a cor de fundo do banner é especificada explicitamente em cada página. Em (b), uma variável compartilhada contém a cor de fundo e cada página faz referência a essa variável. Em (c), algumas páginas exibem uma cor adicional para ênfase, que é um tom mais escuro da cor de fundo do banner; se a cor de fundo mudar, a cor de ênfase também deve mudar.

Carga cognitiva: O segundo sintoma de complexidade é a carga cognitiva, que se refere a quanto um desenvolvedor precisa saber para completar uma tarefa. Uma carga cognitiva maior significa que os desenvolvedores têm que gastar mais tempo aprendendo as informações necessárias, e há um risco maior de bugs porque eles podem ter perdido algo importante. Por exemplo, suponha que uma função em C aloque memória, retorne um ponteiro para essa memória e assuma que o chamador liberará a memória. Isso adiciona à carga cognitiva dos desenvolvedores que usam a função; se um desenvolvedor não liberar a memória, haverá um vazamento de memória. Se o sistema puder ser reestruturado de modo que o chamador não precise se preocupar em liberar a memória (o mesmo módulo que aloca a memória também assume a responsabilidade de liberá-la), isso reduzirá a carga cognitiva. A carga cognitiva surge de muitas maneiras, como APIs com muitos métodos, variáveis globais, inconsistências e dependências entre módulos.

Os projetistas de sistemas às vezes assumem que a complexidade pode ser medida por linhas de código. Eles assumem que se uma implementação é mais curta que outra, então deve ser mais simples; se são necessárias apenas algumas linhas de código para fazer uma mudança, então a mudança deve ser fácil. No entanto, essa visão ignora os custos associados à carga cognitiva. Já vi frameworks que permitiam que aplicativos fossem escritos com apenas algumas linhas de código, mas era extremamente difícil descobrir quais eram essas linhas. Às vezes, uma abordagem que requer mais linhas de código é na verdade mais simples, porque reduz a carga cognitiva.

Incógnitas desconhecidas: O terceiro sintoma da complexidade é que não é óbvio quais partes do código devem ser modificadas para completar uma tarefa, ou que informações um desenvolvedor deve ter para realizar a tarefa com sucesso. A Figura 2.1(c) ilustra esse problema. O site usa uma variável central para determinar a cor de fundo do banner, então parece fácil de mudar. No entanto, algumas páginas da web usam um tom mais escuro da cor de fundo para ênfase, e essa cor mais escura é especificada explicitamente nas páginas individuais. Se a cor de fundo mudar, então a cor de ênfase deve mudar para combinar. Infelizmente, é improvável que os desenvolvedores percebam isso, então podem mudar a variável central bannerBg sem atualizar a cor de ênfase. Mesmo que um desenvolvedor esteja ciente do problema, não será óbvio quais páginas usam a cor de ênfase, então o desenvolvedor pode ter que pesquisar cada página do site.

Das três manifestações da complexidade, as incógnitas desconhecidas são os piores. Uma incógnita desconhecida significa que há algo que você precisa saber, mas não há maneira de descobrir o que é, ou mesmo se há um problema. Você só descobrirá quando bugs aparecerem depois de fazer uma mudança. A amplificação de mudanças é irritante, mas desde que esteja claro qual código precisa ser modificado, o sistema funcionará uma vez que a mudança tenha sido concluída. Da mesma forma, uma alta carga cognitiva aumentará o custo de uma mudança, mas se estiver claro quais informações ler, é provável que a mudança ainda esteja correta. Com incógnitas desconhecidas, não está claro o que fazer ou se uma solução proposta sequer funcionará. A única maneira de ter certeza é ler cada linha de código no sistema, o que é impossível para sistemas de qualquer tamanho. Mesmo isso pode não ser suficiente, porque uma mudança pode depender de uma decisão de design sutil que nunca foi documentada.

Um dos objetivos mais importantes de um bom design é que um sistema seja óbvio. Isso é o oposto de alta carga cognitiva e incógnitas desconhecidas. Em um sistema óbvio, um desenvolvedor pode entender rapidamente como o código existente funciona e o que é necessário para fazer uma mudança. Um sistema óbvio é aquele onde um desenvolvedor pode fazer um palpite rápido sobre o que fazer, sem pensar muito, e ainda assim estar confiante de que o palpite está correto. O Capítulo 18 discute técnicas para tornar o código mais óbvio.

Causas da Complexidade

Agora que você conhece os sintomas de alto nível da complexidade e por que a complexidade torna o desenvolvimento de software difícil, o próximo passo é entender o que causa a complexidade, para que possamos projetar sistemas para evitar os problemas. A complexidade é causada por duas coisas: dependências e obscuridade. Esta seção discute esses fatores em alto nível; os capítulos subsequentes discutirão como eles se relacionam com decisões de design de nível mais baixo.

Para os propósitos deste livro, uma dependência existe quando um determinado trecho de código não pode ser entendido e modificado isoladamente; o código se relaciona de alguma forma com outro código, e o outro código deve ser considerado e/ou modificado se o código dado for alterado. No exemplo do site da Figura 2.1(a), a cor de fundo cria dependências entre todas as páginas. Todas as páginas precisam ter o mesmo fundo, então se o fundo for alterado para uma página, ele deve ser alterado para todas elas. Outro exemplo de dependências ocorre em protocolos de rede. Tipicamente, há código separado para o remetente e o receptor do protocolo, mas ambos devem estar em conformidade com o protocolo; mudar o código do remetente quase sempre requer mudanças correspondentes no receptor, e vice-versa. A assinatura de um método cria uma dependência entre a implementação desse método e o código que o invoca: se um novo parâmetro for adicionado a um método, todas as invocações desse método devem ser modificadas para especificar esse parâmetro.

Dependências são uma parte fundamental do software e não podem ser completamente eliminadas. Na verdade, introduzimos intencionalmente dependências como parte do processo de design de software. Cada vez que você escreve uma nova classe, cria dependências em torno da API dessa classe. No entanto, um dos objetivos do design de software é reduzir o número de dependências e tornar as dependências que permanecem o mais simples e óbvias possível.

Considere o exemplo do site. No antigo site com o fundo especificado separadamente em cada página, todas as páginas da web eram dependentes umas das outras. O novo site resolveu esse problema especificando a cor de fundo em um lugar central e fornecendo uma API que as páginas individuais usam para recuperar essa cor quando são renderizadas. O novo site eliminou a dependência entre as páginas, mas criou uma nova dependência em torno da API para recuperar a cor de fundo. Felizmente, a nova dependência é mais óbvia: é claro que cada página individual da web depende da cor bannerBg, e um desenvolvedor pode facilmente encontrar todos os lugares onde a variável é usada pesquisando seu nome. Além disso, os compiladores ajudam a gerenciar dependências de API: se o nome da variável compartilhada mudar, ocorrerão erros de compilação em qualquer código que ainda use o nome antigo. O novo site substituiu uma dependência não óbvia e difícil de gerenciar por uma mais simples e mais óbvia.

A segunda causa de complexidade é a obscuridade. A obscuridade ocorre quando informações importantes não são óbvias. Um exemplo simples é um nome de variável tão genérico que não carrega muita informação útil (por exemplo, tempo). Ou, a documentação de uma variável pode não especificar suas unidades, então a única maneira de descobrir é examinar o código em busca de lugares onde a variável é usada. A obscuridade é frequentemente associada a dependências, onde não é óbvio que uma dependência existe. Por exemplo, se um novo status de erro for adicionado a um sistema, pode ser necessário adicionar uma entrada a uma tabela que contém mensagens de string para cada status, mas a existência da tabela de mensagens pode não ser óbvia para um programador olhando para a declaração do status. A inconsistência também é um grande contribuinte para a obscuridade: se o mesmo nome de variável for usado para dois propósitos diferentes, não será óbvio para os desenvolvedores qual desses propósitos uma determinada variável serve.

Em muitos casos, a obscuridade surge devido à documentação inadequada; o Capítulo 13 trata desse tópico. No entanto, a obscuridade também é uma questão de design. Se um sistema tem um design limpo e óbvio, então precisará de menos documentação. A necessidade de documentação extensa é frequentemente um sinal de alerta de que o design não está totalmente correto. A melhor maneira de reduzir a obscuridade é simplificando o design do sistema.

Juntas, as dependências e a obscuridade respondem pelas três manifestações de complexidade descritas na Seção 2.2. As dependências levam à amplificação de mudanças e a uma alta carga cognitiva. A obscuridade cria incógnitas desconhecidas e também contribui para a carga cognitiva. Se pudermos encontrar técnicas de design que minimizem as dependências e a obscuridade, então podemos reduzir a complexidade do software.

Complexidade é Incremental

A complexidade não é causada por um único erro catastrófico; ela se acumula em muitos pequenos pedaços. Uma única dependência ou obscuridade, por si só, é improvável que afete significativamente a manutenibilidade de um sistema de software. A complexidade surge porque centenas ou milhares de pequenas dependências e obscuridades se acumulam ao longo do tempo. Eventualmente, há tantas dessas pequenas questões que toda possível mudança no sistema é afetada por várias delas.

A natureza incremental da complexidade torna difícil controlá-la. É fácil se convencer de que um pouco de complexidade introduzida por sua mudança atual não é grande coisa. No entanto, se cada desenvolvedor adotar essa abordagem para cada mudança, a complexidade se acumula rapidamente. Uma vez acumulada a complexidade, é difícil eliminá-la, já que corrigir uma única dependência ou obscuridade não fará, por si só, uma grande diferença. Para desacelerar o crescimento da complexidade, você deve adotar uma filosofia de "tolerância zero", como discutido no Capítulo 3.

Conclusão

A complexidade vem de um acúmulo de dependências e obscuridades. À medida que a complexidade aumenta, ela leva à amplificação de mudanças, a uma alta carga cognitiva e a incógnitas desconhecidas. Como resultado, são necessárias mais modificações de código para implementar cada nova funcionalidade. Além disso, os desenvolvedores gastam mais tempo adquirindo informações suficientes para fazer a mudança com segurança e, no pior caso, eles nem conseguem encontrar todas as informações de que precisam. O resultado final é que a complexidade torna difícil e arriscado modificar uma base de código existente.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published