Como uma variável introduz o estado?


11

Eu estava lendo os "C ++ Coding Standards" e esta linha estava lá:

As variáveis ​​introduzem estado, e você deve lidar com o menor estado possível, com a vida útil o mais curta possível.

Nada de mutante acaba manipulando o estado? O que você deve ter para lidar com o mínimo de estado possível ?

Em uma linguagem impura como C ++, o gerenciamento de estado não é realmente o que você está fazendo? E quais são outras maneiras de lidar com o menor estado possível, além de limitar a vida útil variável?

Respostas:


16

Alguma coisa mutável realmente não manipula o estado?

Sim.

E o que significa "você deveria ter que lidar com pouco estado"?

Isso significa que menos estado é melhor que mais estado. Mais estado tende a introduzir mais complexidade.

Em uma linguagem impura como C ++, o gerenciamento de estado não é realmente o que você está fazendo?

Sim.

Quais são outras maneiras de "lidar com pouco estado" além de limitar a vida útil da variável?

Minimize o número de variáveis. Isole o código que manipula algum estado em uma unidade independente para que outras seções do código possam ignorá-lo.


9

Alguma coisa mutável realmente não manipula o estado?

Sim. Em C ++, as únicas coisas mutáveis ​​são (não const) variáveis.

E o que significa "você deveria ter que lidar com pouco estado"?

Quanto menos estado um programa tiver, mais fácil será entender o que ele faz. Portanto, você não deve introduzir um estado que não é necessário e não deve mantê-lo quando não precisar mais dele.

Em uma linguagem impura como C ++, o gerenciamento de estado não é realmente o que você está fazendo?

Em uma linguagem de múltiplos paradigmas como o C ++, geralmente há uma escolha entre uma abordagem funcional "pura" ou uma orientada a estado, ou algum tipo de híbrido. Historicamente, o suporte a idiomas para programação funcional tem sido bastante fraco em comparação com alguns idiomas, mas está melhorando.

Quais são outras maneiras de "lidar com pouco estado" além de limitar a vida útil da variável?

Restrinja o escopo, bem como a vida útil, para reduzir o acoplamento entre objetos; favorecer variáveis ​​locais em vez de globais e membros privados em vez de objetos públicos.


5

state significa que algo está sendo armazenado em algum lugar para que você possa consultá-lo mais tarde.

Criando uma variável, cria algum espaço para você armazenar alguns dados. Esses dados são o estado do seu programa.

Você o usa para fazer coisas, alterá-lo, computá-lo etc.

Isso é estado , enquanto as coisas que você faz não são estado.

Em uma linguagem funcional, você lida principalmente apenas com funções e passando funções como se fossem objetos. Embora essas funções não tenham estado, e passar a função ao redor, não apresenta estado (além de talvez dentro da própria função).

No C ++, você pode criar objetos de função , que são structou classtipos que foram operator()()sobrecarregados. Esses objetos de função podem ter estado local, embora isso não seja necessariamente compartilhado entre outros códigos no seu programa. Functors (ou seja, objetos de função) são muito fáceis de distribuir. Isso é o mais próximo que você pode imitar um paradigma funcional em C ++. (ATÉ ONDE SEI)

Ter pouco ou nenhum estado significa que você pode otimizar facilmente seu programa para execução paralela, porque não há nada que possa ser compartilhado entre encadeamentos ou CPUs; portanto, nada sobre o que se possa criar contenção e nada que você precise proteger contra corridas de dados etc.


2

Outros forneceram boas respostas para as três primeiras perguntas.

E quais são outras maneiras de "lidar com o mínimo de estado possível" além de limitar a vida útil variável?

A resposta chave para a pergunta 1 é sim, qualquer coisa que sofrer mutação acaba impactando o estado. A chave então é não mudar as coisas. Tipos imutáveis, usando um estilo de programação funcional em que o resultado de uma função é passado diretamente para a outra e não armazenado, passando mensagens ou eventos diretamente em vez de armazenar estado, calculando valores em vez de armazená-los e atualizá-los ...

Caso contrário, você ficará limitando o impacto do estado; via visibilidade ou vida útil.


1

E o que significa "você deveria ter que lidar com pouco estado"?

Isso significa que suas aulas devem ser o menor possível, representando de maneira ideal uma única abstração. Se você colocar 10 variáveis ​​em sua classe, provavelmente está fazendo algo errado e deve ver como refatorar sua classe.


1

Para entender como um programa funciona, você deve entender suas mudanças de estado. Quanto menos estado você tiver e mais local for para o código que o usa, mais fácil será.

Se você já trabalhou com um programa que tinha um grande número de variáveis ​​globais, você entenderia implicitamente.


1

Estado é simplesmente dados armazenados. Toda variável é realmente algum tipo de estado, mas geralmente usamos "state" para nos referirmos a dados persistentes entre as operações. Como um exemplo simples e sem sentido, você pode ter uma classe que armazena internamente as funções membro e intpossui increment()e possui decrement(). Aqui, o valor interno é state porque persiste por toda a vida da instância dessa classe. Em outras palavras, o valor é o estado do objeto.

Idealmente, o estado que uma classe define deve ser o menor possível, com redundância mínima. Isso ajuda sua classe a cumprir o princípio de responsabilidade única , melhora o encapsulamento e reduz a complexidade. O estado de um objeto deve ser totalmente encapsulado pela interface para esse objeto. Isso significa que o resultado de qualquer operação nesse objeto será previsível, dada a semântica do objeto. Você pode melhorar ainda mais o encapsulamento, minimizando o número de funções que têm acesso ao estado .

Essa é uma das principais razões para evitar o estado global. O estado global pode introduzir uma dependência para um objeto sem a interface que o expressa, tornando esse estado oculto para o usuário do objeto. Invocar uma operação em um objeto com uma dependência global pode ter resultados variados e imprevisíveis.


1

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).

insira a descrição da imagem aqui

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.

insira a descrição da imagem aqui

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.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.