Nada de mutante acaba manipulando o estado?
Sim, mas se estiver atrás de uma função de membro de uma classe pequena que é a única entidade em todo o sistema que pode manipular seu estado privado, esse estado tem um escopo muito restrito.
O que você deve ter para lidar com o mínimo de estado possível?
Do ponto de vista da variável: o menor número possível de linhas de código pode acessá-la. Limite o escopo da variável ao mínimo.
Do ponto de vista da linha de código: o menor número possível de variáveis deve ser acessível a partir dessa linha de código. Limite o número de variáveis que a linha de código pode possivelmente acesso (não importa mesmo que muito se faz acessá-lo, tudo o que importa é se ele puder ).
Variáveis globais são muito ruins porque têm escopo máximo. Mesmo que eles sejam acessados a partir de 2 linhas de código em uma base de código, a partir do ponto de vista do código, uma variável global estará sempre acessível. No POV da variável, uma variável global com ligação externa é acessível a todas as linhas de código em toda a base de código (ou a qualquer linha de código que inclua o cabeçalho de qualquer maneira). Apesar de apenas ser acessado por 2 linhas de código, se a variável global estiver visível para 400.000 linhas de código, sua lista imediata de suspeitos quando você achar que ela foi definida como um estado inválido terá 400.000 entradas (talvez seja rapidamente reduzida para 2 entradas com ferramentas, mas, no entanto, a lista imediata terá 400.000 suspeitos e esse não é um ponto de partida encorajador).
As chances são, da mesma forma, que mesmo que uma variável global comece a ser modificada apenas por 2 linhas de código em toda a base de código, a infeliz tendência de as bases de código evoluírem para trás tenderá a aumentar drasticamente esse número, simplesmente porque pode aumentar o número de linhas. desenvolvedores, frenéticos para cumprir prazos, veem essa variável global e percebem que podem usar atalhos por ela.
Em uma linguagem impura como C ++, o gerenciamento de estado não é realmente o que você está fazendo?
Em grande parte, sim, a menos que você esteja usando C ++ de uma maneira muito exótica, o que significa lidar com estruturas de dados imutáveis personalizadas e pura programação funcional por toda parte - também costuma ser a fonte da maioria dos erros quando o gerenciamento de estado se torna complexo e a complexidade é grande. frequentemente em função da visibilidade / exposição desse estado.
E quais são outras maneiras de lidar com o menor estado possível, além de limitar a vida útil variável?
Tudo isso está no campo de limitar o escopo de uma variável, mas há muitas maneiras de fazer isso:
- Evite variáveis globais brutas como a praga. Até mesmo algumas funções estúpidas de setter / getter global diminuem drasticamente a visibilidade dessa variável e, pelo menos, permitem uma maneira de manter invariantes (por exemplo: se a variável global nunca deve ter um valor negativo, o setter pode manter essa invariante). Obviamente, mesmo um design de setter / getter sobre o que de outra forma seria uma variável global é um design bastante ruim, o que quero dizer é que ainda é muito melhor.
- Reduza suas aulas quando possível. Uma classe com centenas de funções-membro, 20 variáveis-membro e 30.000 linhas de código implementando-a teria variáveis privadas "globais", pois todas essas variáveis estariam acessíveis às suas funções-membro, que consistem em 30k linhas de código. Você pode dizer que a "complexidade do estado" nesse caso, ao descontar variáveis locais em cada função de membro, é
30,000*20=600,000
. Se houvesse 10 variáveis globais acessíveis além disso, a complexidade do estado seria semelhante 30,000*(20+10)=900,000
. Uma "complexidade de estado" saudável (meu tipo pessoal de métrica inventada) deve estar na casa dos milhares ou abaixo das classes, não dezenas de milhares e, definitivamente, não centenas de milhares. Para funções gratuitas, digamos centenas ou menos antes de começarmos a ter graves dores de cabeça na manutenção.
- Na mesma linha que acima, não implemente algo como uma função membro ou função amigo que, de outra forma, poderia ser não-membro, não-amigo usando apenas a interface pública da classe. Tais funções não podem acessar as variáveis privadas da classe e, assim, reduzem o potencial de erro, reduzindo o escopo dessas variáveis privadas.
- Evite declarar variáveis muito antes de serem realmente necessárias em uma função (por exemplo, evite o estilo C herdado, que declara todas as variáveis na parte superior de uma função, mesmo que sejam necessárias muitas linhas abaixo). Se você usar esse estilo de qualquer maneira, pelo menos procure funções mais curtas.
Além das variáveis: efeitos colaterais
Muitas dessas diretrizes listadas acima estão abordando o acesso direto ao estado bruto e mutável (variáveis). No entanto, em uma base de código suficientemente complexa, apenas restringir o escopo das variáveis brutas não será suficiente para raciocinar facilmente sobre a correção.
Você poderia ter, digamos, uma estrutura central de dados, por trás de uma interface abstrata totalmente SÓLIDA, totalmente capaz de manter perfeitamente invariantes e ainda acabar sofrendo muito sofrimento devido à ampla exposição desse estado central. Um exemplo de estado central que não é necessariamente globalmente acessível, mas apenas amplamente acessível é o gráfico de cena central de um mecanismo de jogo ou a estrutura de dados da camada central do Photoshop.
Nesses casos, a idéia de "estado" vai além das variáveis brutas, e apenas das estruturas de dados e coisas desse tipo. Da mesma forma, ajuda a reduzir seu escopo (reduz o número de linhas que podem chamar funções que as modificam indiretamente).
Observe como marquei deliberadamente até a interface como vermelha aqui, já que, a partir do nível arquitetônico amplo e reduzido, o acesso a essa interface ainda está mudando de estado, embora indiretamente. A classe pode manter invariantes como resultado da interface, mas isso só vai tão longe em termos de nossa capacidade de raciocinar sobre correção.
Nesse caso, a estrutura central de dados está por trás de uma interface abstrata que pode nem ser acessível globalmente. Ele pode ser apenas injetado e, em seguida, indiretamente alterado (por meio de funções-membro) de um monte de funções em sua complexa base de código.
Nesse caso, mesmo que a estrutura de dados mantenha perfeitamente seus próprios invariantes, coisas estranhas podem acontecer em um nível mais amplo (por exemplo: um reprodutor de áudio pode manter todos os tipos de invariantes como se o nível de volume nunca estivesse fora da faixa de 0% a 100%, mas isso não o protege do usuário que pressiona o botão de reprodução e de ter um clipe de áudio aleatório diferente daquele que ele carregou mais recentemente como um evento é acionado, o que faz com que a lista de reprodução seja reorganizada de uma maneira válida, mas comportamento ainda indesejável e defeituoso da perspectiva ampla do usuário).
A maneira de se proteger nesses cenários complexos é "afunilar" os lugares na base de código que podem chamar funções que acabam causando efeitos colaterais externos, mesmo com esse tipo de visão mais ampla do sistema que vai além do estado bruto e além das interfaces.
Por mais estranho que pareça, você pode ver que nenhum "estado" (mostrado em vermelho, e isso não significa "variável bruta", significa apenas um "objeto" e possivelmente até mesmo por trás de uma interface abstrata) está sendo acessado por vários lugares . Cada uma das funções tem acesso a um estado local que também é acessível por um atualizador central, e o estado central é acessível apenas ao atualizador central (tornando-o não mais central, mas de natureza local).
Isso é apenas para bases de código realmente complexas, como um jogo que abrange 10 milhões de linhas de código, mas pode ajudar tremendamente no raciocínio sobre a correção do seu software e na descoberta de que suas alterações produzem resultados previsíveis quando você limita / afunila significativamente o número de lugares que podem modificar estados críticos em que toda a arquitetura gira para funcionar corretamente.
Além das variáveis brutas, há efeitos colaterais externos, e efeitos colaterais externos são uma fonte de erro, mesmo que estejam confinados a algumas funções-membro. Se uma carga de funções pode chamar diretamente algumas funções-membro, há uma carga de funções no sistema que pode causar indiretamente efeitos colaterais externos e aumentar a complexidade. Se houver apenas um lugar na base de código que tenha acesso a essas funções-membro, e esse caminho de execução não for acionado por eventos esporádicos em todo o lugar, mas for executado de maneira muito previsível e controlada, reduz a complexidade.
Complexidade do Estado
Até a complexidade do estado é um fator bastante importante a ser levado em consideração. Uma estrutura simples, amplamente acessível por trás de uma interface abstrata, não é tão difícil de bagunçar.
Uma estrutura complexa de dados de gráfico que representa a representação lógica central de uma arquitetura complexa é bastante fácil de bagunçar, e de uma maneira que nem viola os invariantes do gráfico. Um gráfico é muitas vezes mais complexo que uma estrutura simples e, portanto, torna-se ainda mais crucial reduzir a complexidade percebida da base de código para reduzir o número de locais que têm acesso a essa estrutura gráfica ao mínimo absoluto, e onde esse tipo de estratégia de "atualizador central" que se inverte em um paradigma de atração para evitar esporadicamente, impulsos diretos para a estrutura de dados gráficos de todo o lugar podem realmente valer a pena.