Antes de comenzar, permítanme decir que conozco bien los conceptos de abstracción e inyección de dependencia. No necesito mis ojos abiertos aquí.
Bueno, la mayoría de nosotros dice, (también) muchas veces sin comprender realmente, "No use variables globales" o "Los Singletons son malos porque son globales". Pero, ¿qué tiene de malo el ominoso estado global?
Digamos que necesito una configuración global para mi aplicación, por ejemplo, rutas de carpetas del sistema o credenciales de base de datos para toda la aplicación.
En ese caso, no veo ninguna buena solución que no sea proporcionar esta configuración en algún tipo de espacio global, que comúnmente estará disponible para toda la aplicación.
Sé que es malo abusar de él, pero ¿el espacio global es realmente TAN malo? Y si es así, ¿qué buenas alternativas hay?
fuente
Respuestas:
Muy brevemente, hace que el estado del programa sea impredecible.
Para elaborar, imagine que tiene un par de objetos que usan la misma variable global. Suponiendo que no esté utilizando una fuente de aleatoriedad en ningún lugar dentro de ninguno de los módulos, la salida de un método en particular se puede predecir (y, por lo tanto, probar) si se conoce el estado del sistema antes de ejecutar el método.
Sin embargo, si un método en uno de los objetos desencadena un efecto secundario que cambia el valor del estado global compartido, entonces ya no sabe cuál es el estado inicial cuando ejecuta un método en el otro objeto. Ahora ya no puede predecir qué salida obtendrá cuando ejecute el método y, por lo tanto, no puede probarlo.
En un nivel académico, esto puede no sonar tan serio, pero ser capaz de unificar el código de prueba es un paso importante en el proceso de probar su corrección (o al menos la idoneidad para el propósito).
En el mundo real, esto puede tener algunas consecuencias muy graves. Suponga que tiene una clase que llena una estructura de datos global y una clase diferente que consume los datos en esa estructura de datos, cambiando su estado o destruyéndolos en el proceso. Si la clase de procesador ejecuta un método antes de que se complete la clase de población, el resultado es que la clase de procesador probablemente tendrá datos incompletos para procesar, y la estructura de datos en la que estaba trabajando la clase de población podría corromperse o destruirse. El comportamiento del programa en estas circunstancias se vuelve completamente impredecible y probablemente conducirá a una pérdida épica.
Además, el estado global perjudica la legibilidad de su código. Si su código tiene una dependencia externa que no se introduce explícitamente en el código, entonces quienquiera que tenga el trabajo de mantener su código tendrá que buscarlo para averiguar de dónde proviene.
En cuanto a qué alternativas existen, es imposible no tener un estado global en absoluto, pero en la práctica generalmente es posible restringir el estado global a un solo objeto que envuelva a todos los demás, y al que nunca se debe hacer referencia confiando en las reglas de alcance del lenguaje que estás usando. Si un objeto particular necesita un estado particular, entonces debe solicitarlo explícitamente al pasarlo como argumento a su constructor o mediante un método de establecimiento. Esto se conoce como inyección de dependencia.
Puede parecer una tontería pasar en un estado al que ya puede acceder debido a las reglas de alcance de cualquier idioma que esté utilizando, pero las ventajas son enormes. Ahora, si alguien mira el código de forma aislada, está claro qué estado necesita y de dónde viene. También tiene enormes beneficios con respecto a la flexibilidad de su módulo de código y, por lo tanto, las oportunidades para reutilizarlo en diferentes contextos. Si se pasa el estado y los cambios en el estado son locales para el bloque de código, puede pasar en cualquier estado que desee (si es el tipo de datos correcto) y hacer que su código lo procese. El código escrito en este estilo tiende a tener la apariencia de una colección de componentes poco asociados que pueden intercambiarse fácilmente. El código de un módulo no debería importar de dónde viene el estado, sino cómo procesarlo.
Hay muchas otras razones por las cuales pasar el estado es muy superior a depender del estado global. Esta respuesta no es de ninguna manera integral. Probablemente podrías escribir un libro completo sobre por qué el estado global es malo.
fuente
being able to unit test code is a major step in the process of proving its correctness (or at least fitness for purpose)
. No, no lo es. "Han pasado dos décadas desde que se señaló que las pruebas de programas pueden demostrar de manera convincente la presencia de errores, pero nunca pueden demostrar su ausencia. Después de citar este comentario tan publicitado devotamente, el ingeniero de software vuelve al orden del día y continúa para refinar sus estrategias de prueba, al igual que el alquimista de antaño, que continuó refinando sus purificaciones crisocósmicas ". - Djikstra, 1988. (Eso hace 4.5 décadas ahora ...)El estado global mutable es malo por muchas razones:
Alternativas al estado global mutable:
fuente
grep
para el nombre del tipo averiguar qué funciones lo usan.Simplemente pase una referencia a las funciones que lo necesitan. No es tan dificil.
fuente
Si dice "estado", eso generalmente se toma como "estado mutable". Y el estado global mutable es totalmente malo, porque significa que cualquier parte del programa puede influir en cualquier otra parte (al cambiar el estado global).
Imagínese depurar un programa desconocido: encuentra que la función A se comporta de cierta manera para ciertos parámetros de entrada, pero a veces funciona de manera diferente para los mismos parámetros. Usted encuentra que usa la variable global x .
Busca lugares que modifiquen x , y encuentra que hay cinco lugares que lo modifican. Ahora buena suerte descubriendo en qué casos la función A hace qué ...
fuente
Como que respondiste tu propia pregunta. Son difíciles de manejar cuando se 'abusan', pero pueden ser útiles y [algo] predecibles cuando se usan adecuadamente, por alguien que sabe cómo contenerlos. El mantenimiento y los cambios en / sobre las glóbulos generalmente es una pesadilla, que empeora a medida que aumenta el tamaño de la aplicación.
Los programadores experimentados que pueden notar la diferencia entre que los globales son la única opción, y que son la solución fácil, pueden tener problemas mínimos para usarlos. Pero los posibles problemas interminables que pueden surgir con su uso requieren el asesoramiento contra su uso.
editar: Para aclarar lo que quiero decir, los globales son impredecibles por naturaleza. Al igual que con cualquier cosa impredecible, puede tomar medidas para contener la imprevisibilidad, pero siempre hay límites para lo que se puede hacer. Además de la molestia de que los nuevos desarrolladores que se unan al proyecto tengan que lidiar con variables relativamente desconocidas, las recomendaciones contra el uso de globales deberían ser comprensibles.
fuente
Hay muchos problemas con Singletons: estos son los dos problemas más grandes en mi mente.
Hace que las pruebas unitarias sean problemáticas. El estado global puede contaminarse de una prueba a la siguiente.
Aplica la estricta regla de "Uno y solo uno", que, aunque no podría cambiar, de repente lo hace. Todo un montón de código de utilidad que usaba el objeto accesible globalmente, entonces necesita ser alterado.
Dicho esto, la mayoría de los sistemas tienen cierta necesidad de Big Global Objects. Estos son elementos que son grandes y caros (por ejemplo, administradores de conexión de base de datos), o que contienen información generalizada del estado (por ejemplo, información de bloqueo).
La alternativa a un Singleton es tener estos Big Global Objects creados en el inicio y pasarlos como parámetros a todas las clases o métodos que necesitan acceso a este objeto.
El problema aquí es que terminas con un gran juego de "pasar el paquete". Tiene un gráfico de componentes y sus dependencias, y algunas clases crean otras clases, y cada una tiene que contener un montón de componentes de dependencia solo porque sus componentes generados (o los componentes de los componentes generados) los necesitan.
Te encuentras con nuevos problemas de mantenimiento. Un ejemplo: De repente, su componente "WidgetFactory", en lo profundo del gráfico, necesita un objeto de temporizador del que desee burlarse. Sin embargo, "WidgetFactory" es creado por "WidgetBuilder", que forma parte de "WidgetCreationManager", y debe tener tres clases que conozcan este objeto del temporizador aunque solo uno lo use realmente. Deseas darte por vencido y volver a Singletons, y simplemente hacer que este objeto de temporizador sea accesible globalmente.
Afortunadamente, este es exactamente el problema que se resuelve con un marco de inyección de dependencia. Simplemente puede decirle al marco qué clases necesita crear, y utiliza la reflexión para descubrir el gráfico de dependencia por usted, y construye automáticamente cada objeto cuando se necesitan.
Entonces, en resumen, los Singletons son malos, y la alternativa es usar un marco de Inyección de dependencias.
Por casualidad uso Castle Windsor, pero tienes muchas opciones. Consulte esta página de 2008 para obtener una lista de los marcos disponibles.
fuente
En primer lugar, para que la inyección de dependencia sea "con estado", necesitaría usar singletons, por lo que las personas que dicen que de alguna manera es una alternativa están equivocadas. Las personas usan objetos de contexto global todo el tiempo ... Incluso el estado de sesión, por ejemplo, es esencialmente una variable global. Pasar todo por la inyección de dependencia o no no siempre es la mejor solución. Actualmente trabajo en una aplicación muy grande que usa muchos objetos de contexto global (singletons inyectados a través de un contenedor IoC) y nunca ha sido un problema depurar. Especialmente con una arquitectura dirigida por eventos, se puede preferir usar objetos de contexto global en lugar de transmitir lo que haya cambiado. Depende de a quién le preguntes.
Se puede abusar de cualquier cosa y también depende del tipo de aplicación. El uso de variables estáticas, por ejemplo, en una aplicación web es completamente diferente a una aplicación de escritorio. Si puede evitar las variables globales, hágalo, pero a veces tienen sus usos. Como mínimo, asegúrese de que sus datos globales estén en un objeto contextual claro. En cuanto a la depuración, nada que una pila de llamadas y algunos puntos de interrupción no puedan resolver.
Quiero enfatizar que usar ciegamente variables globales es una mala idea. Las funciones deberían ser reutilizables y no debería importarles de dónde provienen los datos; en referencia a las variables globales, la función se combina con una entrada de datos específica. Es por eso que debe pasarse y por qué la inyección de dependencia puede ser útil, aunque todavía se trata de un almacén de contexto centralizado (a través de singletons).
Por cierto ... Algunas personas piensan que la inyección de dependencia es mala, incluido el creador de Linq, pero eso no impedirá que la gente lo use, incluido yo mismo. En definitiva, la experiencia será tu mejor maestro. Hay momentos para seguir reglas y tiempos para romperlos.
fuente
Dado que algunas otras respuestas aquí hacen la distinción entre el estado global mutable e inmutable , me gustaría opinar que incluso las variables / configuraciones globales inmutables son a menudo una molestia .
Considere las preguntas:
Correcto, para un programa pequeño, esto probablemente no sea un problema, pero tan pronto como lo haga con componentes en un sistema un poco más grande, escribir pruebas automatizadas de repente se vuelve más difícil porque todas las pruebas (que se ejecutan dentro del mismo proceso) deben funcionar con El mismo valor de configuración global.
Si todos los datos de configuración se pasan explícitamente, los componentes se vuelven mucho más fáciles de probar y nunca tendrá que preocuparse de cómo iniciar un valor de configuración global para múltiples pruebas, tal vez incluso en paralelo.
fuente
Bueno, por un lado, puede encontrarse exactamente con el mismo problema que con singletons. Lo que hoy parece una "cosa global de la que solo necesito uno" de repente se convertirá en algo que necesita más en el futuro.
Por ejemplo, hoy crea este sistema de configuración global porque desea una configuración global para todo el sistema. Unos años más tarde, se transfiere a otro sistema y alguien dice "oye, ya sabes, esto podría funcionar mejor si hubiera una configuración global general y una configuración específica de la plataforma". De repente, tiene todo este trabajo para hacer que sus estructuras globales no sean globales, por lo que puede tener varias.
(Este no es un ejemplo aleatorio ... esto sucedió con nuestro sistema de configuración en el proyecto en el que estoy actualmente).
Teniendo en cuenta que el costo de hacer algo no global suele ser trivial, es una tontería hacerlo. Solo estás creando problemas futuros.
fuente
El otro problema curiosamente es que hacen que una aplicación sea difícil de escalar porque no son lo suficientemente "globales". El alcance de una variable global es el proceso.
Si desea escalar su aplicación utilizando múltiples procesos o ejecutándose en múltiples servidores, no puede hacerlo. Al menos no hasta que elimine todos los globales y los reemplace con algún otro mecanismo.
fuente
El estado global mutable es malo porque es muy difícil para nuestro cerebro tener en cuenta más de unos pocos parámetros a la vez y descubrir cómo se combinan desde una perspectiva de tiempo y una perspectiva de valor para afectar algo.
Por lo tanto, somos muy malos depurando o probando un objeto cuyo comportamiento tiene más de unas pocas razones externas para ser alterado durante la ejecución de un programa. Y mucho menos cuando tenemos que razonar sobre docenas de estos objetos tomados juntos.
fuente
Los lenguajes diseñados para un diseño de sistemas seguro y robusto a menudo eliminan por completo el estado mutable global . (Podría decirse que esto significa que no hay globales, ya que los objetos inmutables en cierto sentido no tienen estado ya que nunca tienen una transición de estado).
Joe-E es un ejemplo, y David Wagner explica la decisión así:
Entonces, una forma de pensarlo es
Por lo tanto, el estado globalmente mutable hace que sea más difícil
El estado mutable global es similar al infierno de DLL . Con el tiempo, las diferentes partes de un sistema grande requerirán un comportamiento sutilmente diferente de las partes compartidas de estado mutable. Resolver el infierno de DLL y las inconsistencias de estado mutable compartidas requiere una coordinación a gran escala entre equipos dispares. Estos problemas no se producirían si el estado global hubiera tenido el alcance adecuado para comenzar.
fuente
Los globales no son tan malos. Como se indicó en varias otras respuestas, el verdadero problema con ellas es que, hoy, su ruta de carpeta global puede ser, mañana, una de varias, o incluso cientos. Si está escribiendo un programa rápido y único, use globales si es más fácil. En general, sin embargo, permitir múltiples, incluso cuando solo cree que necesita uno, es el camino a seguir. No es agradable tener que reestructurar un gran programa complejo que de repente necesita hablar con dos bases de datos.
Pero no perjudican la fiabilidad. Cualquier dato al que se haga referencia desde muchos lugares en su programa puede causar problemas si cambia inesperadamente. Los enumeradores se ahogan cuando la colección que están enumerando cambia a mitad de la enumeración. Los eventos de la cola de eventos pueden jugar trucos entre ellos. Los hilos siempre pueden causar havok. Cualquier cosa que no sea una variable local o un campo inalterable es un problema. Los problemas globales son este tipo de problema, pero no vas a solucionarlo haciéndolos no globales.
Si está a punto de escribir en un archivo y la ruta de la carpeta cambia, el cambio y la escritura deben sincronizarse. (Como una de las mil cosas que podrían salir mal, digamos que toma la ruta, luego ese directorio se elimina, luego la ruta de la carpeta se cambia a un buen directorio, luego intenta escribir en el directorio eliminado). El problema existe si la ruta de la carpeta es global o es una de las mil que el programa está usando actualmente.
Existe un problema real con los campos a los que pueden acceder diferentes eventos en una cola, diferentes niveles de recursión o diferentes subprocesos. Para hacerlo simple (y simplista): las variables locales son buenas y los campos son malos. Pero los antiguos niveles globales seguirán siendo campos, por lo que este problema (aunque de importancia crítica) no se aplica al estado Bueno o Malo de los campos globales.
Adición: Problemas de subprocesos múltiples:
(Tenga en cuenta que puede tener problemas similares con una cola de eventos o llamadas recursivas, pero el subproceso múltiple es, con mucho, el peor). Considere el siguiente código:
Si
filePath
es una variable local o algún tipo de constante, su programa no fallará cuando se ejecute porquefilePath
es nulo. El cheque siempre funciona. Ningún otro hilo puede cambiar su valor. De lo contrario , no hay garantías. Cuando comencé a escribir programas multiproceso en Java, obtuve NullPointerExceptions en líneas como esta todo el tiempo. Algunaotro hilo puede cambiar el valor en cualquier momento, y a menudo lo hacen. Como señalan otras respuestas, esto crea serios problemas para las pruebas. La declaración anterior puede funcionar mil millones de veces, realizar pruebas exhaustivas y exhaustivas, y luego explotar una vez en producción. Los usuarios no podrán reproducir el problema, y no volverá a suceder hasta que se hayan convencido de que estaban viendo cosas y lo hayan olvidado.Los globales definitivamente tienen este problema, y si puede eliminarlos por completo o reemplazarlos con constantes o variables locales, eso es algo muy bueno. Si tiene código sin estado ejecutándose en un servidor web, probablemente pueda. Por lo general, todos los problemas de subprocesos múltiples pueden ser asumidos por la base de datos.
Pero si su programa tiene que recordar cosas de una acción del usuario a la siguiente, tendrá campos accesibles por cualquier subproceso en ejecución. Cambiar un campo global a uno no global no ayudará a la confiabilidad.
fuente
El estado es inevitable en cualquier aplicación real. Puede concluirlo de la manera que desee, pero una hoja de cálculo debe contener datos en las celdas. Puede crear objetos de celda con solo funciones como una interfaz, pero eso no restringe cuántos lugares pueden llamar a un método en la celda y cambiar los datos. Construye jerarquías de objetos completos para tratar de ocultar interfaces para que otras partes del código no puedancambia los datos por defecto. Eso no impide que una referencia al objeto que contiene se pase arbitrariamente. Tampoco nada de eso elimina los problemas de concurrencia por sí mismo. Sí hace que sea más difícil proliferar el acceso a los datos, pero en realidad no elimina los problemas percibidos con los globales. Si alguien quiere modificar una parte del estado, lo hará a la intemperie si es global o mediante una API compleja (la última solo desalentará, no evitará).
La verdadera razón para no usar el almacenamiento global es evitar las colisiones de nombres. Si carga varios módulos que declaran el mismo nombre global, o tiene un comportamiento indefinido (muy difícil de depurar porque las pruebas unitarias pasarán) o un error de enlazador (estoy pensando C - ¿Su enlazador advierte o falla en esto?).
Si desea reutilizar el código, debe poder tomar un módulo de otro lugar y no dejar que accidentalmente pise su global porque usaron uno con el mismo nombre. O si tiene suerte y obtiene un error, no desea tener que cambiar todas las referencias en una sección de código para evitar la colisión.
fuente
Cuando es fácil ver y acceder a todo el estado global, los programadores terminan invariablemente haciéndolo. Lo que obtienes es tácito y las dependencias difíciles de rastrear (int blahblah significa que array foo es válido en lo que sea). Esencialmente, hace que sea casi imposible mantener invariantes del programa, ya que todo se puede girar de forma independiente. someInt tiene una relación entre otherInt, que es difícil de administrar y más difícil de probar si puede cambiar directamente en cualquier momento.
Dicho esto, se puede hacer (cuando era la única forma en algunos sistemas), pero esas habilidades se pierden. Giran principalmente en torno a las convenciones de codificación y nomenclatura: el campo ha avanzado por una buena razón. Su compilador y enlazador hacen un mejor trabajo al verificar invariantes en datos protegidos / privados de clases / módulos que confiar en que los humanos sigan un plan maestro y lean la fuente.
fuente
No voy a decir si las variables globales son buenas o malas, pero lo que voy a agregar a la discusión es decir que si no está utilizando el estado global, probablemente esté desperdiciando mucha memoria, especialmente cuando usa classess para almacenar sus dependencias en campos.
Para el estado global, no existe tal problema, todo es global.
Por ejemplo: imagine un escenario siguiente: tiene una cuadrícula de 10x10 que está hecha de clases "Tablero" y "Azulejo".
Si desea hacerlo de la manera OOP, probablemente pasará el objeto "Tablero" a cada "Mosaico". Digamos ahora que "Tile" tiene 2 campos de tipo "byte" que almacenan su coordenada. La memoria total que necesitaría en una máquina de 32 bits para un mosaico sería (1 + 1 + 4 = 6) bytes: 1 para x coord, 1 para y coord y 4 para un puntero a la placa. Esto da un total de 600 bytes para la configuración de 10x10 mosaicos
Ahora, para el caso en que la placa está en el ámbito global, un solo objeto accesible desde cada mosaico solo tendría que obtener 2 bytes de memoria por cada mosaico, esos son los bytes de coordenadas x e y. Esto daría solo 200 bytes.
Entonces, en este caso, obtienes 1/3 del uso de la memoria si solo usas el estado global.
Esto, además de otras cosas, supongo que es una razón por la cual el alcance global aún permanece en lenguajes (relativamente) de bajo nivel como C ++
fuente
Hay varios factores a considerar con el estado global:
Cuantos más globales tenga, mayor será la posibilidad de introducir duplicados y, por lo tanto, romper las cosas cuando los duplicados no estén sincronizados. Mantener todos los globales en su falsa memoria humana se vuelve necesario y doloroso.
Inmutables / escritura una vez generalmente están bien, pero tenga cuidado con los errores de secuencia de inicialización.
Globales mutables a menudo se confunden con globales inmutables ...
Una función que utiliza globals efectivamente tiene parámetros "ocultos" adicionales, lo que dificulta la refactorización.
El estado global no es malo, pero tiene un costo definido: úselo cuando el beneficio sea mayor que el costo.
fuente