我们大多数人说,(通常不知道),
“不要使用全局变量”
Martin Fowler在他的一本书《企业应用程序架构模式》中表示,
“任何全局变量都是有罪的,除非另有证明”
- 为什么使用全局变量是不好的做法?
- 它们真的有害吗?
- 还是纯粹主义者
的非理性偏见仇恨?
资料来源: mundogeek.net - 全局变量
我们大多数人说,(通常不知道),
“不要使用全局变量”
Martin Fowler在他的一本书《企业应用程序架构模式》中表示,
“任何全局变量都是有罪的,除非另有证明”
资料来源: mundogeek.net - 全局变量
变化的影响
全局变量的问题在于它们创建了隐藏的依赖关系。对于大型应用程序,您甚至不知道/记住/清楚您拥有的对象及其关系。
因此,您无法清楚地知道有多少对象正在使用您的全局变量。如果您想更改全局变量的某些内容,例如,它的每个可能值的含义或它的类型,该怎么办?此更改将影响多少个类或编译单元?如果金额很小,则可能值得进行更改。如果影响很大,可能值得寻找另一种解决方案。
但有什么影响?因为全局变量可以在代码中的任何位置使用,所以很难测量它。
此外,我们总是尽量使变量具有尽可能短的生命周期,以便使用该变量的代码量尽可能少,从而更好地了解其用途以及修改它的人。
全局变量在程序运行期间持续存在,因此,任何人都可以使用该变量来读取它,或者更糟糕的是,改变它的值,这使得在任何时候都很难知道该变量将具有什么值在程序中给定点。
销毁令
另一个问题是破坏的顺序。变量总是以其创建的相反顺序被销毁,无论它们是局部变量还是全局/静态变量(一个例外是原始类型、
int
、enum
s 等,如果它们是全局/静态的,则在程序结束之前永远不会被销毁)。问题是很难知道全局(或静态)变量的构造顺序。原则上,它是不确定的。
如果您所有的全局/静态变量都在一个编译单元中(即您只有一个
.cpp
),那么构建顺序与写入顺序相同(即首先定义的变量首先构建)。但是,如果您有多个
.cpp
具有自己的全局/静态变量,则全局构建顺序是不确定的。当然,每个编译单元(每个.cpp
)中的顺序尤其受到尊重:如果全局变量A
定义在之前B
,A
它将在之前构建B
,但其他变量可能会进入A
并被B
初始化.cpp
。例如,如果您有三个具有以下全局/静态变量的驱动器:在可执行文件中,它可以按以下顺序创建(或以任何其他顺序,只要尊重每个顺序中的相对顺序
.cpp
):为什么这很重要?因为如果不同的静态全局对象之间存在关系,例如,它们在析构函数中相互使用,也许在全局变量的析构函数中,您使用来自另一个编译单元的另一个全局对象,该对象恰好已经被销毁(由后来建造的)。
隐藏的依赖和测试用例
我试图找到我要在这个例子中使用的字体,但我找不到它(无论如何它是为了举例说明单例的使用,尽管这个例子适用于全局和静态变量)。如果对象依赖于全局变量的状态,隐藏的依赖关系也会产生与控制对象行为相关的新问题。
假设您有一个支付系统,并且您想测试它以了解它是如何工作的,因为您需要进行更改,并且代码来自其他人(或您的,但来自几年前)。你打开一个新的
main
,并调用你的提供银行卡支付服务的全局对象的相应函数,结果你输入了你的数据,他们向你收费。在一个简单的测试中,我如何使用生产版本?如何进行简单的付款测试?在询问其他同事后,事实证明您必须
bool
在开始充电过程之前“设置为 true”,这是一个指示我们是否处于测试模式的全局变量。你提供支付服务的对象依赖于另一个提供支付模式的对象,而这种依赖对程序员来说是不可见的。换句话说,全局变量(或单例)使得无法进入“测试模式”,因为全局变量不能被“测试”实例替换(除非您修改创建或定义所述实例的代码)。 ,但我们假设测试是在不修改母代码的情况下完成的)。
解决方案
这可以通过所谓的依赖注入来解决,它包括将对象在其构造函数或相应方法中所需的所有依赖项作为参数传递。通过这种方式,程序员可以看到发生在他身上的事情,因为他必须用代码编写它,从而为开发人员节省了大量时间。
如果全局对象太多,并且需要它们的函数中的参数太多,您总是可以将您的“全局对象”捆绑到一个工厂风格的类中,该类构造并返回(模拟)“全局对象”的实例“你想要,将工厂作为参数传递给需要所述全局对象作为依赖项的对象。
如果切换到测试模式,您始终可以创建一个测试工厂(返回相同对象的不同版本),并将其作为参数传递,而无需修改目标类。
但总是不好吗?
不一定,全局变量可能有很好的用途。比如常数值(PI的值)。作为一个常数值,不存在由于来自另一个模块的任何类型的修改而在程序中的给定点不知道其值的风险。此外,常数值往往是原始的,它们的定义不太可能改变。
在这种情况下,使用全局变量会更舒服,这样就不必将变量作为参数传递,从而简化函数的签名。
另一个可能是非侵入式“全局”服务,例如日志记录类(将发生的事情保存在文件中,通常在程序中是可选的和可配置的,因此不会影响应用程序的核心行为),或者
std::cout
,std::cin
或者std::cerr
,它们也是全局对象。其他任何东西,即使它的生命周期几乎与程序的生命周期一致,也总是将其作为参数传递。该变量甚至可以在模块中是全局的,仅在其中没有其他任何人可以访问,但在任何情况下,依赖项始终作为参数存在。
全局变量是一个坏主意,至少有 5 个原因:
另一个问题是跟踪您的更改非常困难。有可能在某些时候您创建了另一个具有相同名称的全局变量,并最终在没有意识到的情况下覆盖了它的值,这将产生最深奥的错误并且最难调试。
在来自软件工程社区的这个问题中,您可以获得更多信息:
https://softwareengineering.stackexchange.com/questions/148108/why-is-global-state-so-evil
全局变量是您的程序的任何部分或在与您的应用程序相同的上下文中运行的任何其他程序可以访问的内存空间,因此也可以访问所述内存空间。
由于几个原因,这被认为是一种反模式或不好的做法。这里我提一些:
你的程序不容易推理。
在对其依赖项的多次更新之间,程序的自然流程可能会丢失。
复杂性增加
随着越来越多的参与方在公共区域进行交互,您现在将更加难以预测应用程序在每个时刻的状态。这对于它正常工作非常重要。
你可能会得到不可预知的结果
从以上推导,假设您有一个带有值的变量,当您去操作它时,您意识到这不是您所期望的,并且代码的另一部分更改了该值。这可能会导致您的程序完全崩溃或执行您不希望它执行的操作。它以可预测的方式运行是拥有良好架构的要求之一。
造成不安全感
您的数据不是私人的。在某些情况下,您的程序与其他人编写的代码共享上下文。一个典型的例子是网页中包含的第三方小部件(twitter、facebook 等)。这些脚本在与您的应用程序相同的上下文中执行,因此可以访问您的所有全局变量。显然,在该环境中存储任何有价值的数据并不是一个好主意,因为它可能会被其他人获取。
其他人可以破坏您的代码
从上面推导出来,可能是另一个程序员决定使用与您相同的变量名(这实际上经常发生)。由于他们都试图使用与自己相同的内存区域,这将导致您的程序崩溃。这就是为什么,例如,网络上几乎所有的库都有一种方法
noConflict
或类似的方法,可以在它们之间进行互操作而不会产生冲突。其他争论可能是它们违反了引用透明性并且无助于线程安全,使得竞争条件很难检测到。想象一下同一个程序的两个线程试图同时将一个值写入同一个地方。
我可以想到更多,但我认为这个论点很有说服力,因为所有编程语言都有工件和技术可以轻松避免此类问题,因此除非绝对必要,否则没有必要陷入此类有问题的模式...
我可以举一个例子来帮助你更好地理解:
想象一下,您在公共访问区域有一个工具箱,您需要拧紧螺丝,打开盒子却找不到螺丝刀或螺丝刀已使用并损坏。框中的工具是您的全局变量。在这种情况下,您将永远无法完成您的工作。
使用全局变量的一些问题:
什么是全局变量?
我认为首先对全局变量是什么有一个简短的概念很重要。
在计算机编程中,全局变量是具有全局范围的变量,这意味着它在整个程序中都是可见的(因此可访问),除非它是隐藏的。所有全局变量的集合称为全局环境或全局状态。在编译语言中,全局变量通常是静态变量,其长度(生命周期)是程序的整个执行过程,虽然在解释语言(包括命令行解释器)中,全局变量通常是在声明时动态赋值的,因为它们无法提前知道。
在某些编程语言中,所有变量默认都是全局的或全局的,而在大多数现代编程语言中,变量的作用域是有限的,通常是词法作用域,尽管全局变量通常可以通过声明一个变量来获得。程序。但是,在其他语言中是没有全局变量的;这些通常是强制执行模块结构的模块化编程语言或强制执行类结构的基于类的面向对象的编程语言。
全局变量不好吗?
答案是肯定的,它们几乎总是坏的。虽然我们知道,没有 100% 满足的规则,我们将看到原因。从一开始就应该明确的是,全局变量在不必要时应该避免使用。但是让我们通过使用我们正在使用的编程语言提供的适当替代方案来避免它。
当全局变量不是绝对必要的时候,为什么要避免它们?
当我们避免使用全局变量时,代码通常更清晰且更易于维护,尽管如前所述,也有例外。相反,它的使用通常会带来非常严重的问题。
让我们看看使用全局变量时出现的一些严重问题:
非局部性- 当单个元素的范围有限时,源代码更容易理解。程序的任何部分都可以读取或更改全局变量,因此很难记住或推理每种可能的用途。
没有访问控制或约束检查:程序的任何部分都可以获取或设置全局变量,并且有关其使用的任何规则都可以很容易地被破坏或忘记。(换句话说,get/set 访问器通常比直接数据访问更可取,这对于全局数据更为重要。)通过扩展,缺乏访问控制使得在您想要的情况下实现安全性变得极其困难。运行不受信任的代码(例如使用第三方插件)。
隐式耦合:具有许多全局变量的程序通常在其中一些变量之间存在紧密耦合,以及变量和函数之间的耦合。将耦合的元素组合成有凝聚力的单元通常会导致更好的程序。
并发问题:如果全局变量可以被多个线程访问,同步是必要的(而且经常被忽略)。通过将模块动态绑定到全局变量,即使在几十个不同上下文中测试的两个独立模块是线程安全的,复合系统也可能不是线程安全的。
命名空间污染:全局名称随处可见。当您认为您正在使用本地时(由于拼写错误或忘记声明本地),您可能会在不知不觉中最终使用全局,反之亦然。此外,如果您必须链接具有相同全局变量名称的模块,如果幸运的话,您会遇到链接错误。如果你不走运,链接器只会将所有使用相同名称的对象视为同一个对象。
内存分配问题:某些环境的内存分配方案使全局分配变得困难。在“构造函数”具有除赋值以外的副作用的语言中尤其如此(因为在这种情况下,可以表达两个全局变量相互依赖的不安全情况)。此外,当动态链接模块时,可能不清楚不同的库是否有自己的全局实例或全局变量是否共享。
测试和限制:使用全局变量的源代码更难测试,因为你不能轻易地在运行之间建立一个“干净”的环境。更一般地,使用未明确提供给该源的任何类型的全局服务(例如,读取和写入文件或数据库)的源由于同样的原因而难以测试。对于通信系统,测试系统不变量的能力可能需要一个系统的多个“副本”同时运行,这受到使用共享服务(包括全局内存)的极大阻碍,这些共享服务没有提供。考试。
小例外
Hay casos en los que la conveniencia de las variables globales supera los problemas potenciales mencionados anteriormente.
Imaginemos por ejemplo un programa muy pequeño o especial, especialmente del tipo 'plugin' en el que básicamente escribes un solo objeto o guión corto para un sistema más grande, usar globales puede ser lo más simple en este caso.
Cuando las variables globales representan instalaciones que realmente están disponibles en todo el programa, su uso simplifica el código.
Algunos lenguajes de programación no proporcionan soporte ni soporte mínimo para variables no globales.
Falsas alternativas al uso de globales o usarlas "creyendo" que no lo son
Algunas personas saltan a través de aros muy complicados para evitar el uso de globales. Muchos usos del SingletonPattern son apenas globales velados delgadamente. Si algo realmente debe ser global, hazlo global. No hagas algo complicado porque tal vez lo necesites algún día. Si existe una variable global, asumiría que se utiliza. Si se utiliza, hay métodos asociados con él. Colocar esos métodos en una sola clase y uno ha creado un singleton. Realmente es mejor especificar todas las reglas para el uso de una variable global en un lugar donde se pueden revisar por coherencia. El velo puede ser delgado, pero es valioso.
Incluso en los casos anteriores, es aconsejable considerar el uso de una de las Alternativas a Variables Globales para controlar el acceso a esta facilidad. Mientras que esto ayuda a prueba de futuro el código, por ejemplo, cuando su "pequeño" programa se convierte en uno muy grande - también puede simplificar problemas más inmediatos como probar el código o hacer que funcione correctamente en un entorno concurrente.
La declaración irreflexiva de variables se puede considerar un vicio de programación, en el cual podemos caer cuando empezamos a trabajar en un problema y cedemos a la tentación: "Necesito esa lista en muchos lugares diferentes. Declaro una Variable global ... y voila! ". Luego experimentamos que el número de globales comienza a ser inmanejable, entonces decidimos poner a todas las globales en una gran lista global de globales. A veces el globo explota cuando es demasiado tarde :)
Y es que el vicio ocurre porque agregar globales es muy fácil. Es fácil adquirir el hábito de declararlas. Es mucho más rápido que pensar en un buen diseño. Pero como se suele decir, lo barato sale caro. Es cierto que en algunas circunstancias simples, realmente la cosa más simple de hacer es usar una variable global. Pero cuando se trata de un programa complejo, una vez que se tiene una variable global, es mucho más difícil deshacerse de ella mientras pasa el tiempo y el programa crece. Nuestro código termina siendo dependiente de los posibles caprichos de dicha variable y arrojarnos resultados inesperados.
Realmente malas razones para usar variables globales
-¿Qué es una "variable local"? O sea, cuando no se entiende lo que es una variable local ni como funciona.
-¿Qué es un "miembro de datos"? Lo mismo, la ignorancia...
-"Soy una mecanógrafa lenta, los globales me guardan las pulsaciones de teclas". Pues ya ves :)
-"No quiero pasar parámetros todo el tiempo." No seas vago :)
-"No estoy seguro de a qué clase pertenecen estos datos, así que lo haré global." Vea las noticias, infórmese
Alternativas a variables globales
Las alternativas a variables globales son múltiples. Aunque es bueno señalar que escoger una alternativa supone a veces un paso delicado, en el sentido de que podemos estar optando por una alternativa que a lo mejor no tiene la capacidad de resolver esa situación concreta. Es el ejemplo típico mencionado más arriba en el caso de los SingletonPattern.
Veamos algunas de estas alternativas:
Objetos de Contexto: permiten agrupar y abstraer las dependencias globales y luego moverlas en un programa, funcionando efectivamente como variables globales pero mucho más fácil de anular y manipular localmente. Desafortunadamente, la mayoría de los lenguajes no ofrecen soporte para ContextObjects, que requiere "pasarlo todo el tiempo". Los hilos (Threading) de un ContextObject son ayudados por los alcances dinámicos (DynamicScoping) y las variables especiales (SpecialVariables).
Inyección de dependencia (DependencyInjection): La capacidad de configurar gráficos de objetos complejos en un programa reduce en gran medida la necesidad de pasar "variables" alrededor de las que llevan información global / de contexto. La fuerza de este enfoque es visible en paradigmas que hacen mucho menos uso de globales, como DataflowProgramming. Algunos lenguajes (como Java) tienen estructuras maduras de DependencyInjection que a menudo funcionan de manera algo estática (por ejemplo, a partir de un archivo de configuración, o no integran objetos vivos) y eso solo es suficiente para coincidir con muchos usos comunes de globales.
El soporte de primera clase (FirstClass) para DependencyInjection y la construcción de gráficos de flujo de datos o de objetos permite además componer sistemas complejos sobre la marcha en tiempo de ejecución (permitiendo un medio de composición para objetos primitivos que faltan en los lenguajes OO tradicionales) y además permite una enorme gama de optimizaciones, eliminación de códigos muertos, evaluación parcial, etc., lo que hace que esta sea una alternativa bastante atractiva a las globales.
Globales ocultas: las globales ocultas tienen un alcance de acceso bien definido e incluyen, por ejemplo, variables privadas 'estáticas' en clases y variables 'estáticas' en archivos '.c' y variables en espacios de nombres anónimos en C ++. Esta solución enjaula y localiza globales en lugar de domesticarlos - todavía se morderá cuando se trata de concurrencia y modularización y pruebas / confinamiento, pero al menos todos estos problemas serán localizados y fáciles de reparar, y no habrá problemas de vinculación .
Procedimientos de estado: Se trata de un conjunto global de setters y getters u operaciones que actúan sobre lo que es, implícitamente, el estado subyacente. Estos sufren muchos de los problemas asociados con los globales en lo que respecta a pruebas, concurrencia y asignación / intialización. Sin embargo, ofrecen un mejor control de acceso, la oportunidad de sincronización y una considerable capacidad de abstraer la implementación (por ejemplo, se podría poner el estado global en una base de datos).
SingletonPattern: Construye un objeto globalmente, permite el acceso a él a través de un procedimiento de estado. SingletonPattern ofrece la oportunidad conveniente para la especialización de una sola vez de un global basado en argumentos y el ambiente, y así puede servir bastante bien para abstraer los recursos que son verdaderamente parte del ambiente de programación (por ejemplo, monitores, altavoces, consola, etc.). Sin embargo, SingletonPattern no ofrece nunca la flexibilidad ofrecida por DependencyInjection o ContextObject, y está a la par con los procedimientos de estado en cuanto que ayudan al programador a controlar los problemas a los que los usuarios todavía se enfrentarán.
Base de datos o TupleSpace o DataDistributionService: A veces las variables globales se utilizan para representar datos. Esto es especialmente el caso de mapas globales, hashtables globales, listas globales. En menor grado, también es el caso de las colas de tareas. Para estos casos en los que los datos son realmente "globales", especialmente si cualquier parte del programa se dedica a empujar partes de estos datos globales a usuarios externos, el uso de un sistema dedicado simplificará en gran medida el programa y probablemente lo hará más robusto al mismo tiempo.
En Resumen
Usa las globales solamente cuando las puedas tener enjauladas (en un pequeño programa) y sean realmente necesarias en ese caso, o cuando no te quede ninguna otra alternativa.
使用全局变量的“坏习惯”可以很容易地解决。它被放置在Singleton中(不好的做法?),通过同步的getter和setter访问,并且 - 圣洁的补救措施 - 不再是不好的做法。;)
尽管通常建议尽可能限制变量的范围,但在某些情况下,您只需要保存全局变量,因为您正在备份全局值。例如,数据库中的值与全局变量没有什么不同。
因此,使用全局变量应该应用与使用数据库值(事务、原子化访问、本地副本等)时相同的注意事项。
全局作用域变量只有在没有保护其内容完整性的好方法时才危险。修饰符如
volatile
,可同步的getter和setter , 类型如AtomicInteger , ... 是允许您为变量提供所需范围的工具。仅当开发人员不知道自己在做什么或为什么要这样做时,才存在不良做法。最糟糕的做法是盲目地遵循良好做法而不了解原因,或者迷失在避免可能是解决特定问题的最佳方法的所谓不良做法中。
一些反思...
全局变量往往会引入错误。
如果是全局变量,则可以修改,否则为常量。现在,如果它可以被修改,那么它可以从代码中的任何地方修改,所以你无法控制谁修改它的值。
调试可能来自任何地方的错误通常是一件令人头疼的事情。
最后,不需要全局变量。没有一个用例可以完全由全局变量覆盖,而不能由其他结构覆盖。例如,在 OOP 中
Singleton
,您有一个模式,虽然不强烈推荐,但它是一个有效的替代方案。了解全局变量的危险和祸害的一个好方法是检查一个导致问题的典型示例。我将使用 C# 作为示例,但该原则适用于任何编程语言。
让我们看下面的例子:
假设我被要求找出上面抛出异常的原因,因为它
mivariable
是null
,即使它从来不应该是。在这种情况下,因为mivariable
它不是全局的,而是具有明确定义的方法输入(参数)的变量,使用 Visual Studio 等现代工具,很容易追溯方法调用的线程以调查在哪个位置mivariable
被破坏了。关键是 的范围mivariable
是有限的,所以我不必在我的代码中调查我可能已经改变了值的无数地方mivariable
相反,如果我要调查相同的错误,但将其
mivariable
定义为全局变量,该怎么办:现在我的调查的复杂性成倍增加,因为我现在不仅必须找到代码的其他部分可能被操纵的所有地方
mivariable
,而且我敢肯定会有很多地方,而且我必须了解所有的顺序执行这些其他地方是为了知道什么时候做的值mivariable
。这是一个巨大的头痛。类似地,当涉及到全局变量时,不仅问题的调查更加复杂,而且出于同样的原因,由于误解变量的使用地点和时间而在代码中引入细微缺陷的机会也会增加。
当然,项目越复杂,你拥有的代码越多,它就越会成为一个真正的问题。这种类型的代码被称为意大利面条代码,因为它理解起来非常复杂。
最糟糕的是,没有理由使用全局变量。通常,介绍它们的人都是出于懒惰并希望节省几行代码。但这样做的长期成本非常高。
学校的一位老师告诉我们,全局变量非常容易访问,甚至可以通过另一个应用程序访问,这种情况大大增加了恶意代理丢失或窃取信息的风险。
变量必须具有最小的必要范围。我不认为对全局变量有任何不合理的仇恨。请注意,拥有一个全局范围变量应该是合理的。在适当的上下文中使用变量(或单例)并不是一种反模式。当变量的上下文被不必要地扩展时,问题就来了。这意味着代码的一部分可以访问不属于他们的责任的变量,无论是由于设计缺陷还是试图重用资源,这可能会导致使用中非常危险的歧义问题。
类似的事情发生在我们的类封装不好的情况下,因为我们可以提供过多的可见性/访问权限,甚至会影响更大的依赖关系,这将影响我们在未来的维护中。