Antes de começarmos, deixe-me dizer que estou bem ciente dos conceitos de Abstração e Injeção de Dependência. Não preciso abrir meus olhos aqui.
Bem, a maioria de nós diz (muitas vezes) sem entender realmente: "Não use variáveis globais" ou "Singletons são maus porque são globais". Mas o que é realmente tão ruim no estado global ameaçador?
Digamos que eu precise de uma configuração global para meu aplicativo, por exemplo, caminhos de pasta do sistema ou credenciais de banco de dados em todo o aplicativo.
Nesse caso, não vejo outra solução boa além de fornecer essas configurações em algum tipo de espaço global, que estará normalmente disponível para todo o aplicativo.
Eu sei que é ruim para abusar dela, mas é o espaço global realmente QUE mal? E se for, que boas alternativas existem?
fonte
Respostas:
Muito brevemente, torna o estado do programa imprevisível.
Para elaborar, imagine que você tenha alguns objetos que usam a mesma variável global. Supondo que você não esteja usando uma fonte de aleatoriedade em qualquer lugar de qualquer módulo, a saída de um método específico pode ser prevista (e, portanto, testada) se o estado do sistema for conhecido antes da execução do método.
No entanto, se um método em um dos objetos acionar um efeito colateral que altera o valor do estado global compartilhado, você não saberá mais qual é o estado inicial ao executar um método no outro objeto. Agora você não pode mais prever qual saída obterá quando executar o método e, portanto, não poderá testá-lo.
No nível acadêmico, isso pode não parecer muito sério, mas ser capaz de realizar o código do teste de unidade é um passo importante no processo de comprovação de sua correção (ou pelo menos adequação à finalidade).
No mundo real, isso pode ter algumas consequências muito graves. Suponha que você tenha uma classe que preenche uma estrutura de dados global e uma classe diferente que consome os dados nessa estrutura de dados, alterando seu estado ou destruindo-os no processo. Se a classe do processador executar um método antes da conclusão da classe do preenchedor, o resultado é que a classe do processador provavelmente terá dados incompletos para processar e a estrutura de dados na qual a classe do preenchedor estava trabalhando pode ser corrompida ou destruída. O comportamento do programa nessas circunstâncias se torna completamente imprevisível e provavelmente levará a uma perda épica.
Além disso, o estado global prejudica a legibilidade do seu código. Se o seu código tem uma dependência externa que não é explicitamente introduzida no código, quem conseguir manter seu código precisará procurar por ele para descobrir de onde veio.
Quanto às alternativas existentes, bem, é impossível não ter um estado global, mas, na prática, geralmente é possível restringir o estado global a um único objeto que envolve todos os outros, e que nunca deve ser referenciado com base nas regras de escopo do idioma que você está usando. Se um objeto em particular precisa de um estado específico, ele deve solicitá-lo explicitamente, passando-o como argumento para seu construtor ou por um método setter. Isso é conhecido como injeção de dependência.
Pode parecer bobagem passar em um estado que você já pode acessar devido às regras de escopo de qualquer idioma que esteja usando, mas as vantagens são enormes. Agora, se alguém olhar o código isoladamente, ficará claro de que estado ele precisa e de onde ele vem. Ele também traz enormes benefícios com relação à flexibilidade do seu módulo de código e, portanto, às oportunidades de reutilizá-lo em diferentes contextos. Se o estado for passado e as alterações forem locais para o bloco de código, você poderá passar o estado que desejar (se for o tipo de dados correto) e fazer com que seu código o processe. O código escrito nesse estilo tende a parecer uma coleção de componentes vagamente associados que podem ser facilmente trocados. O código de um módulo não deve se importar de onde vem o estado, apenas como processá-lo.
Existem muitas outras razões pelas quais a transferência de estado é muito superior à dependência do estado global. Esta resposta não é de forma alguma abrangente. Você provavelmente poderia escrever um livro inteiro sobre por que o estado global é ruim.
fonte
being able to unit test code is a major step in the process of proving its correctness (or at least fitness for purpose)
. Não, não é. "Já se passaram duas décadas desde que foi apontado que o teste do programa pode demonstrar de forma convincente a presença de bugs, mas nunca pode demonstrar sua ausência. Depois de citar essa observação bem divulgada com devoção, o engenheiro de software retorna à ordem do dia e continua refinar suas estratégias de teste, assim como o alquimista de outrora, que continuou refinando suas purificações crisocósmicas ". - Djikstra, 1988. (Isso faz 4,5 décadas agora ...)O estado global mutável é mau por vários motivos:
Alternativas ao estado global mutável:
fonte
grep
saber o nome do tipo para descobrir quais funções o utilizam.Basta passar uma referência para as funções que precisam. Não é tão difícil.
fonte
Se você diz "estado", isso geralmente significa "estado mutável". E o estado mutável global é totalmente mau, porque significa que qualquer parte do programa pode influenciar qualquer outra parte (alterando o estado global).
Imagine depurar um programa desconhecido: você acha que a função A se comporta de certa maneira para determinados parâmetros de entrada, mas às vezes funciona de maneira diferente para os mesmos parâmetros. Você descobre que ele usa a variável global x .
Você procura lugares que modificam x e descobre que há cinco lugares que o modificam. Agora boa sorte descobrir em que casos a função A faz o que ...
fonte
Você meio que respondeu sua própria pergunta. Eles são difíceis de gerenciar quando 'abusados', mas podem ser úteis e [um pouco] previsíveis quando usados adequadamente, por alguém que sabe como contê-los. A manutenção e as alterações nos globais geralmente são um pesadelo, agravadas à medida que o tamanho do aplicativo aumenta.
Programadores experientes que sabem a diferença entre os globais serem a única opção e a solução mais fácil, podem ter problemas mínimos ao usá-los. Mas os intermináveis problemas possíveis que podem surgir com o uso deles requerem conselhos contra o uso deles.
editar: Para esclarecer o que quero dizer, os globais são imprevisíveis por natureza. Como em qualquer coisa imprevisível, você pode tomar medidas para conter a imprevisibilidade, mas sempre há limites para o que pode ser feito. Adicione a isso o incômodo de novos desenvolvedores ingressarem no projeto, tendo que lidar com variáveis relativamente desconhecidas, as recomendações contra o uso de globais devem ser compreensíveis.
fonte
Existem muitos problemas com Singletons - aqui estão os dois maiores problemas em minha mente.
Isso torna o teste de unidade problemático. O estado global pode ficar contaminado de um teste para o outro
Ela impõe a regra rígida "Um-e-um-único", que, mesmo que não possa mudar, muda repentinamente. Todo um código de utilidade que usou o objeto acessível globalmente precisa ser alterado.
Dito isto, a maioria dos sistemas precisa de grandes objetos globais. Esses são itens grandes e caros (por exemplo, gerenciadores de conexão com o banco de dados), ou mantêm informações gerais do estado (por exemplo, informações de bloqueio).
A alternativa para um Singleton é ter esses grandes objetos globais criados na inicialização e passados como parâmetros para todas as classes ou métodos que precisam acessar esse objeto.
O problema aqui é que você acaba com um grande jogo de "passe-o-pacote". Você tem um gráfico de componentes e suas dependências, e algumas classes criam outras classes, e cada uma precisa conter vários componentes de dependência apenas porque seus componentes gerados (ou os componentes dos componentes gerados) precisam deles.
Você encontra novos problemas de manutenção. Um exemplo: De repente, seu componente "WidgetFactory", no fundo do gráfico, precisa de um objeto de timer que você deseja zombar. No entanto, "WidgetFactory" é criado por "WidgetBuilder", que faz parte de "WidgetCreationManager", e você precisa ter três classes que conheçam esse objeto de timer, mesmo que apenas uma realmente o use. Você deseja desistir e reverter para os Singletons e apenas tornar esse objeto de timer globalmente acessível.
Felizmente, esse é exatamente o problema resolvido por uma estrutura de injeção de dependência. Você pode simplesmente dizer à estrutura que classes ele precisa criar e usar a reflexão para descobrir o gráfico de dependência para você, e constrói automaticamente cada objeto quando necessário.
Portanto, em resumo, os singletons são ruins e a alternativa é usar uma estrutura de injeção de dependência.
Por acaso uso o Castelo Windsor, mas você é mimado pela escolha. Veja esta página em 2008 para obter uma lista das estruturas disponíveis.
fonte
Antes de tudo, para que a injeção de dependência seja "stateful", você precisará usar singletons, para que as pessoas que dizem que essa seja uma alternativa sejam enganadas. As pessoas usam objetos de contexto global o tempo todo ... Mesmo o estado da sessão, por exemplo, é essencialmente uma variável global. Passar tudo ao redor, seja por injeção de dependência ou não, nem sempre é a melhor solução. Atualmente, trabalho em um aplicativo muito grande que usa muitos objetos de contexto global (singletons injetados por meio de um contêiner de IoC) e nunca foi um problema para depurar. Especialmente com uma arquitetura orientada a eventos, pode ser preferível usar objetos de contexto global versus repassar o que mudou. Depende de quem você pergunta.
Qualquer coisa pode ser abusada e isso também depende do tipo de aplicativo. O uso de variáveis estáticas, por exemplo, em um aplicativo Web é completamente diferente de um aplicativo de desktop. Se você pode evitar variáveis globais, faça-o, mas às vezes elas têm seus usos. No mínimo, verifique se seus dados globais estão em um objeto contextual claro. Quanto à depuração, nada que uma pilha de chamadas e alguns pontos de interrupção não possam resolver.
Quero enfatizar que usar cegamente variáveis globais é uma má ideia. As funções devem ser reutilizáveis e não se importam de onde vêm os dados - a referência a variáveis globais associa a função a uma entrada de dados específica. É por isso que deve ser repassado e por que a injeção de dependência pode ser útil, embora você ainda esteja lidando com um armazenamento de contexto centralizado (via singletons).
Btw ... Algumas pessoas pensam que a injeção de dependência é ruim, incluindo o criador do Linq, mas isso não impede as pessoas de usá-lo, inclusive eu. Em última análise, a experiência será o seu melhor professor. Há momentos para seguir regras e momentos para quebrá-las.
fonte
Como algumas outras respostas aqui fazem a distinção entre estado global mutável e imutável , eu gostaria de opinar que mesmo variáveis / configurações globais imutáveis costumam ser um aborrecimento .
Considere a pergunta:
Certo, para um programa pequeno, isso provavelmente não é um problema, mas assim que você faz isso com componentes em um sistema um pouco maior, escrever testes automatizados de repente se torna mais difícil porque todos os testes (~ executados no mesmo processo) devem funcionar com o mesmo valor de configuração global.
Se todos os dados de configuração forem transmitidos explicitamente, os componentes ficarão muito mais fáceis de testar e você nunca precisará se preocupar em como inicializar um valor de configuração global para vários testes, talvez até em paralelo.
fonte
Bem, por um lado, você pode encontrar exatamente o mesmo problema que pode com os singletons. O que hoje parece uma "coisa global da qual preciso apenas de" irá repentinamente se transformar em algo de que você precisa mais no futuro.
Por exemplo, hoje você cria esse sistema de configuração global porque deseja uma configuração global para todo o sistema. Alguns anos depois, você faz a porta para outro sistema e alguém diz "ei, você sabe, isso poderia funcionar melhor se houvesse uma configuração global geral e uma configuração específica da plataforma". De repente, você tem todo esse trabalho para tornar suas estruturas globais não globais, para que você possa ter várias.
(Este não é um exemplo aleatório ... isso aconteceu com nosso sistema de configuração no projeto em que estou atualmente.)
Considerando que o custo de tornar algo não global geralmente é trivial, é bobagem fazê-lo. Você está apenas criando problemas futuros.
fonte
O outro problema, curiosamente, é que eles dificultam o dimensionamento de um aplicativo, porque não são suficientemente "globais". O escopo de uma variável global é o processo.
Se você quiser escalar seu aplicativo usando vários processos ou executando em vários servidores, não poderá. Pelo menos até você fatorar todas as globais e substituí-las por algum outro mecanismo.
fonte
O estado global mutável é mau, porque é muito difícil para o nosso cérebro levar em consideração mais do que alguns parâmetros de cada vez e descobrir como eles se combinam tanto de uma perspectiva de tempo quanto de uma perspectiva de valor para afetar alguma coisa.
Portanto, somos muito ruins em depurar ou testar um objeto cujo comportamento tem mais de algumas razões externas para serem alteradas durante a execução de um programa. Muito menos quando tivermos que raciocinar sobre dezenas desses objetos juntos.
fonte
Os idiomas projetados para o design de sistemas seguros e robustos geralmente se livram completamente do estado mutável global . (Indiscutivelmente, isso significa que não há globais, pois objetos imutáveis não são, de certo modo, com estado, pois nunca têm uma transição de estado.)
Joe-E é um exemplo, e David Wagner explica a decisão assim:
Então, uma maneira de pensar sobre isso é
Portanto, o estado globalmente mutável torna mais difícil
O estado mutável global é semelhante ao inferno da DLL . Com o tempo, diferentes partes de um sistema grande exigirão um comportamento sutilmente diferente das partes compartilhadas do estado mutável. Resolver o inferno da DLL e inconsistências compartilhadas de estado mutável requer uma coordenação em larga escala entre equipes diferentes. Esses problemas não ocorreriam se o estado global tivesse um escopo adequado para começar.
fonte
Globais não são tão ruins. Como afirmado em várias outras respostas, o verdadeiro problema com elas é que, hoje, o caminho de sua pasta global pode, amanhã, ser uma das várias, ou mesmo centenas. Se você estiver escrevendo um programa rápido e único, use globals se for mais fácil. Geralmente, porém, permitir múltiplos, mesmo quando você pensa que precisa apenas de um, é o caminho a percorrer. Não é agradável ter que reestruturar um grande programa complexo que de repente precisa conversar com dois bancos de dados.
Mas eles não prejudicam a confiabilidade. Quaisquer dados referenciados de vários locais do seu programa podem causar problemas se forem alterados inesperadamente. Os enumeradores engasgam quando a coleção que eles estão enumerando é alterada na enumeração intermediária. Eventos da fila de eventos podem fazer truques um com o outro. Threads sempre podem causar estragos. Qualquer coisa que não seja uma variável local ou um campo imutável é um problema. Globais são esse tipo de problema, mas você não vai consertar isso, tornando-os não globais.
Se você estiver prestes a gravar em um arquivo e o caminho da pasta for alterado, a alteração e a gravação precisarão ser sincronizadas. (Como uma das milhares de coisas que podem dar errado, digamos que você pegue o caminho, esse diretório será excluído, o caminho da pasta será alterado para um bom diretório e tente gravar no diretório excluído.) o caminho da pasta é global ou é um dos mil que o programa está usando no momento.
Há um problema real com campos que podem ser acessados por diferentes eventos em uma fila, diferentes níveis de recursão ou diferentes threads. Para simplificar (e simplificar): variáveis locais são boas e campos são ruins. Mas os ex-globais ainda serão campos, portanto essa questão (por mais importante que seja) não se aplica ao status de bem ou mal dos campos globais.
Adição: problemas de multithreading:
(Observe que você pode ter problemas semelhantes com uma fila de eventos ou chamadas recursivas, mas o multithreading é de longe o pior.) Considere o seguinte código:
Se
filePath
for uma variável local ou algum tipo de constante, seu programa não falhará durante a execução porquefilePath
é nulo. A verificação sempre funciona. Nenhum outro encadeamento pode alterar seu valor. Caso contrário , não há garantias. Quando comecei a escrever programas multithread em Java, recebia NullPointerExceptions em linhas como essa o tempo todo. Qualqueroutro segmento pode alterar o valor a qualquer momento, e geralmente o fazem. Como várias outras respostas apontam, isso cria sérios problemas para o teste. A declaração acima pode funcionar um bilhão de vezes, passando por testes extensivos e abrangentes e explodir uma vez na produção. Os usuários não poderão reproduzir o problema, e isso não acontecerá novamente até que eles se convencam de que estão vendo as coisas e as esquecem.Os Globals definitivamente têm esse problema, e se você pode eliminá-los completamente ou substituí-los por constantes ou variáveis locais, isso é uma coisa muito boa. Se você tem código sem estado em execução em um servidor web, provavelmente pode. Normalmente, todos os seus problemas de multithreading podem ser assumidos pelo banco de dados.
Mas se o seu programa precisar se lembrar de uma ação do usuário para a próxima, você terá campos acessíveis por qualquer thread em execução. Mudar um campo global para um campo não global não ajudará na confiabilidade.
fonte
Estado é inevitável em qualquer aplicação real. Você pode agrupá-lo da maneira que quiser, mas uma planilha deve conter dados nas células. Você pode criar objetos de célula apenas com funções como interface, mas isso não restringe quantos lugares podem chamar um método na célula e alterar os dados. Você cria hierarquias de objetos inteiros para tentar ocultar interfaces para que outras partes do código não possamaltere os dados por padrão. Isso não impede que uma referência ao objeto contido seja transmitida arbitrariamente. Isso também não elimina problemas de concorrência por si só. Isso dificulta a proliferação do acesso aos dados, mas na verdade não elimina os problemas percebidos com os globais. Se alguém quiser modificar uma parte do estado, fará com que seja global ou através de uma API complexa (a última só desencorajará, não impedirá).
O verdadeiro motivo para não usar o armazenamento global é evitar colisões de nomes. Se você carregar vários módulos que declaram o mesmo nome global, você terá um comportamento indefinido (muito difícil de depurar porque os testes de unidade serão aprovados) ou um erro de vinculador (estou pensando em C - O vinculador avisa ou falha nisso?).
Se você deseja reutilizar o código, precisa conseguir pegar um módulo de outro lugar e não fazê-lo pisar acidentalmente na sua global porque eles usaram um com o mesmo nome. Ou, se você tiver sorte e receber um erro, não precisará alterar todas as referências em uma seção do código para evitar a colisão.
fonte
Quando é fácil ver e acessar todo o estado global, os programadores invariavelmente acabam fazendo isso. O que você obtém é tácito e difícil de rastrear dependências (int blahblah significa que o array foo é válido em qualquer que seja). Essencialmente, torna quase impossível manter os invariantes do programa, pois tudo pode ser modificado independentemente. someInt tem um relacionamento entre otherInt, que é difícil de gerenciar e mais difícil de provar se você pode alterar diretamente a qualquer momento.
Dito isto, isso pode ser feito (quando era a única maneira em alguns sistemas), mas essas habilidades estão perdidas. Eles giram principalmente em torno de convenções de codificação e nomeação - o campo mudou por um bom motivo. Seu compilador e vinculador fazem um trabalho melhor ao verificar invariantes em dados protegidos / privados de classes / módulos do que depender de humanos para seguir um plano mestre e ler a fonte.
fonte
Não vou dizer se as variáveis globais são boas ou más, mas o que vou acrescentar à discussão é dizer que, se você não estiver usando o estado global, provavelmente estará gastando muita memória, especialmente quando você usa classes para armazenar suas dependências em campos.
Para o estado global, não existe esse problema, tudo é global.
Por exemplo: imagine um cenário a seguir: Você tem uma grade de 10x10 composta de classes "Quadro" e "Lado a lado".
Se você quiser fazê-lo da maneira OOP, provavelmente passará o objeto "Board" para cada "Tile". Digamos agora que "Tile" tenha 2 campos do tipo "byte" que armazenam suas coordenadas. A memória total necessária para uma máquina de 32 bits para um bloco seria (1 + 1 + 4 = 6) bytes: 1 para x coord, 1 para y coord e 4 para um ponteiro para o quadro. Isso fornece um total de 600 bytes para a configuração de 10x10 blocos
Agora, no caso em que a placa está no escopo global, um único objeto acessível a partir de cada bloco, você só precisará obter 2 bytes de memória por cada bloco, que são os bytes das coordenadas xey. Isso daria apenas 200 bytes.
Portanto, nesse caso, você recebe 1/3 do uso de memória se usar apenas o estado global.
Isso, além de outras coisas, acho que é uma razão pela qual o escopo global ainda permanece em linguagens de nível (relativamente) de baixo nível, como C ++
fonte
Existem vários fatores a serem considerados no estado global:
Quanto mais globais você tiver, maior a chance de introduzir duplicatas e, assim, quebrar as coisas quando as duplicatas ficarem fora de sincronia. Manter todos os globais em sua memória humana falível é necessário e dor.
Imutáveis / gravação uma vez geralmente são bons, mas cuidado com os erros na sequência de inicialização.
Globais mutáveis são frequentemente confundidos com globos imutáveis…
Uma função que usa globals efetivamente possui parâmetros "ocultos" extras, dificultando a refatoração.
O estado global não é mau, mas tem um custo definido - use-o quando o benefício for superior ao custo.
fonte