Clase 3: Desacoplamiento II En la clase anterior, hablamos de la importancia de las dependencias en el diseño de un programa. Un buen lenguaje de programación le permitirá expresar las dependencias entre las partes y controlarlas, evitando que surjan dependencias no deseadas. En esta clase, veremos cómo se pueden utilizar los elementos de Java para expresar y manejar dependencias. Estudiaremos también una variedad de soluciones para un problema simple de codificación, haciendo especial hincapié en el papel de las interfaces. 3.1 Repaso: Diagramas de dependencia de módulos Comencemos dando un breve repaso a los diagramas de dependencia de módulos (MDD) que vimos en la última clase. Un diagrama de dependencia de módulos (MDD) muestra dos tipos de partes en un programa: partes de la implementación (clases de Java), que aparecen como recuadros con una única raya adicional en la parte superior, y partes de la especificación, que se presentan como recuadros con una raya tanto en la parte superior como en la inferior. Las organizaciones de partes en grupos (como los paquetes de Java) pueden mostrarse como contornos que contienen partes de un programa, siguiendo el estilo de un diagrama de Venn. Una flecha simple con la punta abierta conecta la parte de la implementación A con la parte de la especificación S, e indica que el significado de A depende del significado de S. Dado que la especificación S no puede tener por sí misma un significado que dependa de otras partes, se asegura que el significado de una parte se puede determinar desde esa misma parte y desde las especificaciones de las que ésta depende, sin tener que recurrir a ningún otro dato. Una flecha de puntos que vaya desde A hasta S es una dependencia débil; indica que A depende únicamente de la existencia de una parte que satisfaga la especificación S, pero que en 1 realidad no tiene dependencia de ninguno de los detalles de S. Una flecha de punta cerrada que vaya desde una parte de la implementación A a una parte de la especificación S indica que A satisface a S: su significado se ajusta al de S. Dado que las especificaciones son tan imprescindibles, debemos asumir en todo momento que están presentes. La mayoría de las veces, no dibujaremos partes de la especificación de forma explícita y, de este modo, una flecha de dependencia entre dos partes de implementación A y B deberá interpretarse como abreviatura de una dependencia de A para la especificación de B, y como una flecha de conformidad de B para su especificación. Mostraremos las interfaces de Java explícitamente como partes de la especificación. 3.2 Java Namespace (sistema de denominación) Al igual que cualquier trabajo escrito de gran extensión, un programa también se beneficia del hecho de estar organizado conforme a una estructura jerárquica. Cuando se intenta comprender una gran estructura, suele resultar útil visualizarla de arriba abajo, comenzando por los niveles más generales de la estructura y prosiguiendo hasta llegar a los detalles más concretos. El namespace (sistema de denominación) de Java soporta esta estructura jerárquica, lo que supone otra ventaja importante: diferentes componentes pueden utilizar los mismos nombres para sus subcomponentes, con significados locales distintos. En el contexto del sistema como un todo, los subcomponentes llevarán nombres que estén condicionados por los componentes a los que pertenecen, de modo que no habrá confusión. Esto es fundamental porque permite que los programadores trabajen de forma autónoma, sin preocuparse por conflictos de denominación. El sistema de denominación de Java funciona del modo que a continuación exponemos. Los componentes considerados clave son clases e interfaces, y poseen denominaciones de métodos y campos nombrados. Las variables locales (dentro de los métodos) y los argumentos de un método también poseen su nombre. Cada nombre en un programa de Java tiene un 2 alcance: una parte del texto del programa para la que el nombre es válido y se halla asociado al componente. Los argumentos de un método, por ejemplo, poseen el mismo alcance del método; los campos tienen el alcance de la clase y, en algunas ocasiones, un alcance aún mayor. Se puede usar el mismo nombre para referirse a cosas distintas cuando no existe ambigüedad. Por ejemplo, es posible utilizar el mismo nombre para un campo, un método y una clase; consúltense las especificaciones del lenguaje Java para ver los ejemplos. Un programa de Java se organiza por paquetes. Cada clase o interfaz posee su propio fichero (haciendo caso omiso de las clases internas, que no trataremos). Los paquetes se hallan reflejados en la estructura del directorio. Al igual que los directorios, los paquetes pueden estar anidados en estructuras de profundidad arbitraria. Para organizar un código en paquetes, deben hacerse dos cosas: indicar al comienzo de cada fichero a qué paquete pertenece la clase o interfaz, y organizar los archivos físicamente dentro de la estructura de un directorio para que se ajusten a la estructura del paquete. Por ejemplo, la clase djn.browser.Protocol estaría en un fichero llamado Protocol.java en el directorio djn/browser. Podemos mostrar esta estructura en nuestro diagrama de dependencia. Las clases e interfaces constituyen las partes entre las que se muestran las dependencias. Los paquetes se presentan como contornos que engloban estas clases e interfaces. A veces resulta conveniente ocultar las dependencias exactas entre las partes de diferentes paquetes mostrando simplemente un arco de dependencia al nivel del paquete. Una dependencia desde un paquete significa que alguna clase o interfaz (o quizás varias) de ese paquete tiene una dependencia; una dependencia de un paquete significa una dependencia de alguna clase o interfaz (o quizás varias) de ese paquete. 3 3.3 Control de acceso Los mecanismos de Java para el control de acceso permiten dirigir las dependencias. En el texto de una clase, se puede indicar qué otras clases pueden tener dependencias de ella, e incluso controlar, hasta cierto punto, la naturaleza de las dependencias. Una clase declarada pública puede ser referida por cualquier otra clase; si no, puede ser referida sólo por clases del mismo paquete. Por tanto, al lanzar este modificador, podemos evitar dependencias de clase de cualquier clase que no pertenezca al paquete. Los miembros de una clase –es decir, sus campos y métodos—se pueden marcar como públicos, privados o protegidos. Un miembro público puede accederse desde cualquier parte. Un miembro privado puede accederse únicamente desde dentro de la clase en la que el campo o el método se ha declarado. Un miembro protegido puede accederse bien desde dentro del paquete o bien desde fuera por una subclase de la clase en la que el miembro es declarado, creando así un resultado muy peculiar, que consiste en que al marcar un miembro como protegido, éste no se hace menos accesible, sino más. 4 No hay que olvidar que una dependencia de A sobre B indica en realidad una dependencia de A sobre la especificación de B. Los modificadores de los miembros de B nos permiten controlar la naturaleza de la dependencia al cambiar los miembros que pertenecen a la especificación de B. Controlar el acceso a los campos de B ayuda a dar independencia de representación, pero no siempre la garantiza (como veremos próximamente en este curso). 3.4 Lenguajes seguros Una de las propiedades claves de un programa es que una parte únicamente debería depender de otra si ésta la nombra. Esto puede parecer obvio, pero es de hecho una propiedad que sólo se da en los programas escritos mediante los llamados “lenguajes seguros”. En un lenguaje inseguro, el texto de una parte puede afectar al comportamiento de otra, sin que haya ningún nombre compartido. Esto nos lleva a errores insidiosos que resultan muy difíciles de localizar, y que pueden tener resultados desastrosos e imprevisibles. Veamos cómo se producen estos errores. Piense en un programa escrito en C, en el que un módulo (en C, sólo un fichero) actualiza un array. Un intento de fijar el valor de un elemento del array más allá de los límites de éste no dará resultado en algunas ocasiones, ya que provocará un fallo de memoria, yendo más allá del área de memoria asignada al proceso. Sin embargo, y por desgracia, la mayoría de las veces el intento sí dará resultado, y éste consistirá en que se sobrescribirá una parte arbitraria de memoria; arbitraria porque el programador no sabe cómo el compilador dispuso de la memoria del programa, y no puede predecir qué otra estructura de datos se ha visto perjudicada. A consecuencia de ello, una actualización del array puede afectar al valor de una estructura de datos con el nombre d que se ha declarado en un módulo diferente y no posee ni siquiera un tipo en común con a. Los lenguajes seguros evitan estos efectos mediante la combinación de distintas técnicas. La comprobación dinámica de los límites del array impide que se produzca este tipo de actualización que acabamos de mencionar; en Java, se lanzaría una excepción. La 5 administración automática de la memoria asegura que ésta no pueda ser reciclada y posteriormente reutilizada de modo erróneo. Ambas técnicas parten de la idea básica de paradigma de tipos fuertes, que asegura que un acceso que sea declarado a un valor de tipo t en el texto del programa sea siempre un acceso a un valor de tipo t en tiempo de ejecución. No existe riesgo de que el código diseñado para un array pueda ser aplicado por error a una cadena o número entero. Los lenguajes seguros han estado circulando desde 1960. Entre los más famosos se incluyen Algol-60, Pascal, Modula, LISP, CLU, Ada, ML y ahora Java. Resulta interesante saber que durante muchos años, la industria afirmaba que los costes de seguridad eran demasiado altos, y que no era viable cambiar de lenguajes no seguros (como C++) a lenguajes seguros (como Java). Java se vio pronto favorecido por numerosos despliegues de tipo publicitario relativos a los applets, y ahora que está siendo utilizado en todo el mundo, muchas compañías han dado el paso decisivo y están reconociendo las ventajas que supone un lenguaje de programación seguro. Algunos lenguajes seguros garantizan la corrección del tipo en tiempo de compilación por medio de “tipos estáticos”. Otros, como Scheme y LISP, llevan a cabo la comprobación de su tipo en tiempo de ejecución, y sus sistemas de tipos sólo reconocen tipos primitivos. En breve veremos cómo un sistema de tipos más expresivo puede servir también para controlar las dependencias. Si lo que se desea potenciar es la fiabilidad, utilizar un lenguaje seguro es la opción más adecuada. Sirva como ejemplo de ello la historia que conté en clase sobre el uso de elementos de un lenguaje inseguro en un acelerador médico. 6 3.5 Interfaces En lenguajes de tipos estáticos se pueden controlar las dependencias mediante la elección de tipos. En líneas generales, una clase que hace referencia sólo a objetos de tipo T no puede tener una dependencia de una clase que proporciona objetos de un tipo T’ distinto. Dicho de otro modo, se puede deducir, a partir de los tipos referidos en una clase, de qué otras clases depende ésta. Sin embargo, en lenguajes con subtipos, caben posibilidades interesantes. Supongamos que la clase A hace referencia únicamente a la clase B. Esto no significa que ésta pueda solamente llamar a métodos de objetos creados por la clase B. En Java, los objetos creados por una subclase C de B se consideran también de tipo B, así que incluso aunque A no pueda crear directamente objetos de clase C, puede acceder a ellos por intermedio de otra clase. El tipo C se considera un subtipo del tipo B, ya que se puede usar un objeto C cuando se espera un objeto B. Esto se conoce con el nombre de “sustitucionabilidad”; es decir, posibilidad de sustituir. En realidad, las subclases compaginan dos conceptos distintos. Uno es el subtipado: se considera que los objetos de clase C deben tener tipos compatibles con B, por ejemplo. El otro concepto es el de herencia: el código de la clase C puede reutilizar el código de B. Más adelante, en el curso de la asignatura, trataremos algunas de las lamentables consecuencias de combinar estos dos conceptos, y veremos cómo la sustitucionabilidad no funciona siempre tan bien como cabría esperar. Por ahora, nos centraremos exclusivamente en el mecanismo de subtipado, ya que es lo más relevante con relación a lo que estamos viendo. Java proporciona una noción de interfaces que da más flexibilidad al subtipado que las subclases. Una interfaz de Java es, siguiendo nuestra terminología, una parte de especificación pura. No contiene código ejecutable y se utiliza únicamente para facilitar el desacoplamiento. 7 Analicemos su funcionamiento. En vez de tener una clase A que dependa de una clase B, introducimos una interfaz I. A ahora hace referencia a I en vez de a B, y B es necesaria para satisfacer la especificación de I. Ni que decir tiene que el compilador de Java no se ocupa de las especificaciones de comportamiento de tipos: simplemente comprueba que los tipos de los métodos de B sean compatibles con los tipos declarados en I. En tiempo de ejecución, cuando A espera un objeto de tipo I, un objeto de tipo B es aceptable. Por ejemplo, en la librería de Java hay una clase denominada java.util.LinkedList que implementa listas enlazadas. Si estamos escribiendo código que únicamente necesite que un objeto sea una lista, y que no tenga que ser necesariamente una lista enlazada, deberíamos utilizar el tipo java.util.List en nuestro código, que es una interfaz implementada mediante java.util.LinkedList. Existen otras clases, tales como ArrayList y Vector, que implementan esta interfaz. En el momento en que nuestro código se refiera sólo a la interfaz, funcionará con cualquiera de estas clases de implementación. Varias clases pueden implementar la misma interfaz, y una clase puede implementar varias interfaces. Por el contrario, puede que una clase sólo tenga a lo sumo como subclase a otra clase. Debido a esto, mucha gente usa el término “herencia de especificación múltiple” para describir el elemento de la interfaz de Java, en comparación con la verdadera herencia múltiple en la cual se puede reutilizar el código de múltiples superclases. Las interfaces presentan ante todo dos ventajas. En primer lugar, permiten al usuario expresar partes de la especificación pura en código, con lo que éste puede asegurar que el uso de una clase B por una clase A implica simplemente una dependencia de A con respecto a la especificación S, y no con relación a otras características de B. En segundo lugar, las interfaces pueden proporcionar varias partes de la implementación que satisfagan una única 8 especificación, con una selección realizada en tiempo de compilación o en tiempo de ejecución. 3.6 Ejemplo: Cómo instrumentar un programa en Java En lo que queda de clase, estudiaremos algunos mecanismos de desacoplamiento en el contexto de un ejemplo breve, pero que es representativo de una clase importante de problemas. Imaginemos que queremos dar parte de los pasos graduales de un programa cuando se ejecuta, visualizando el progreso línea por línea. Por ejemplo, en un compilador con varias fases, podríamos estar interesados en mostrar un mensaje al comienzo y al final de cada fase. En un cliente de correo electrónico, podríamos visualizar cada uno de los pasos que se producen durante la descarga de un mensaje de correo desde un servidor. Esta clase de servicios de informe resulta útil cuando los pasos por separado podrían llevarnos mucho tiempo o cuando tienen tendencia a fallar (de esta forma el usuario puede optar por cancelar el comando que los provocó). Las barras de progreso se usan normalmente en este contexto, pero presentan más complicaciones (al señalar el comienzo y el fin de una actividad y al calcular el progreso proporcional) que no tendremos en cuenta. Como ejemplo específico, pensemos en un cliente de correo electrónico que tiene un paquete central que contiene una clase Session, la cual presenta un código para establecer una sesión de comunicación con un servidor y descargar mensajes, una clase Folder para los objetos que modelan carpetas y sus contenidos, y una clase Compactor que contiene el código para comprimir la representación de carpetas en el disco. Supongamos que hay llamadas desde Session a Folder y desde Folder a Compactor, pero que las actividades intensivas de recursos que queremos instrumentar tienen lugar únicamente en Session y en Compactor, pero no en Folder. 9 El diagrama de dependencia de módulo muestra que Session depende de Folder, la cual tiene una dependencia mutua de Compactor. Examinaremos una serie de métodos para implementar nuestro servicio de instrumentación, y estudiaremos las ventajas y desventajas de cada uno de ellos. Comenzando por el diseño más simple posible, podríamos entremezclar resultados tales como System.out.println (“Comenzando descarga”); por todo el programa. 3.6.1 Abstracción por parametrización El problema de este plan es obvio. Cuando ejecutamos el programa en modo batch, podríamos redirigir la salida estándar a un fichero. Entonces nos damos cuenta de que sería útil grabar el tiempo de todos los mensajes, de modo que podamos ver más tarde, cuando leamos los ficheros, cuánto tiempo llevaron los distintos pasos. Deseamos que nuestro enunciado sea el siguiente: System.out.println (“Comenzando descarga a:” + nueva Fecha() ); Esto debería ser fácil, pero no lo es. Tenemos que encontrar todos estos enunciados en nuestro código (y diferenciarlos de otras llamadas a System.out.println que tienen objetivos distintos), y modificar cada uno por separado. 10 Por supuesto, lo que deberíamos haber hecho es definir un procedimiento para encapsular esta funcionalidad. En Java, esto sería un método estático: public class StandardOutReporter { public static void report (String msg) { System.out.println (msg); } } Ahora el cambio puede realizarse en un único punto del código. Nos limitamos a modificar el procedimiento: public class StandardOutReporter { public static void report (String msg) { System.out.println (msg + “a” + nueva Fecha()); } } Matthias Felleisen llama a esto el principio del “punto de control individual”. En este caso, el mecanismo nos resulta familiar: es lo que en el curso 6001 se denominó abstracción por parametrización, porque cada llamada al procedimiento: StandardOutReporter.report (“Comenzando descarga”); es una instanciación de la descripción genérica, con el parámetro msg ligado a un valor especial. Podemos ilustrar el único punto de control en un diagrama de dependencia de módulos. Hemos introducido una clase única, de la cual dependen, las clases que usan la función de instrumentación: StandardOutReporter. Hay que tener en cuenta que no existe 11 dependencia de Folder con respecto a StandardOutReporter, ya que el código de Folder no hace ninguna llamada a éste. 3.6.2 Desacoplamiento con interfaces Este planteamiento está lejos de ser perfecto. Aunque reunir la funcionalidad en una única clase es una buena idea, el código mantiene una dependencia de la noción de escribir a una salida estándar (standard out). Si quisiéramos crear una nueva versión de nuestro sistema con una interfaz gráfica de usuario, tendríamos que sustituir esta clase por una que contuviese el código GUI adecuado; lo que supondría cambiar todas las referencias del paquete central para que se refirieran a una clase distinta, o cambiar el código de la propia clase, y tener entonces que controlar dos versiones incompatibles de la clase con el mismo nombre. Ninguna de las dos opciones es válida. De hecho, el problema es aún más grave. En un programa que usa una GUI, se escribe a ésta invocando a un método de un objeto que represente parte de la GUI: un panel de texto o un campo del mensaje. En Swing, el kit de herramientas de la interfaz de usuario de Java, las subclases de JTextComponent poseen un método setText. Dado algún componente nombrado, por ejemplo, por la variable outputArea, el enunciado de muestra podría ser: OutputArea.setText (msg) ¿Cómo vamos a pasar la referencia al componente bajando al sitio de llamada? ¿Y cómo vamos a hacerlo sin introducir código específico de Swing en la clase reporter? Las interfaces de Java tienen la solución. Creamos una interfaz con un sólo método report (informe) que será invocado para mostrar resultados. public interface Reporter { void report (String msg); } 12 Ahora añadimos a cada método de nuestro sistema un argumento de este tipo. La clase Session, por ejemplo, puede tener un método download: Void download (Reporter r, ...) { r.report (“Comenzando descarga”); ... } Ahora definimos una clase que en realidad implementará el comportamiento del método Report (informe). Utilicemos StandardOut (salida estándar) como ejemplo porque es más sencillo: public class StandardOutReporter implements Reporter { public void report (String msg) { System.out.println (msg + “a” + nueva Fecha () ); } } Esta clase no es igual a la anterior que también tenía este nombre. El método ya no es estático, así que podemos crear un objeto de la clase y llamar al método a partir de ella. Asimismo, hemos indicado que esta clase es una implementación de la interfaz Reporter. Por supuesto, para la salida estándar esta solución presenta numerosas lagunas, y la creación del objeto parece ser gratuita. No obstante, en el caso de la GUI, haremos algo más elaborado y crearemos un objeto que esté unido a un mecanismo especial: public class JtextComponentReporter implements Reporter { JTextComponent comp.; 13 public JtextComponentReporter (JTextComponent c) {comp. = c;} public void report (String msg) { comp.setText (msg + “a” + nueva Fecha () ); } } Al comienzo del programa, crearemos un objeto y lo pasaremos a: Ahora hemos llegado a una solución interesante. La llamada a report (informe) ejecuta ahora, en tiempo de ejecución, el código que implica a System.out (salida estándar). Sin embargo, métodos como download sólo dependen de la interfaz Reporter, que no hace alusión alguna a ningún mecanismo de salida concreto. Hemos desacoplado con éxito el mecanismo de salida del programa, rompiendo la dependencia que el núcleo del programa tiene con respecto a su I/O. 14 Observemos el diagrama de dependencia de módulos, teniendo en cuenta que una flecha con la punta cerrada desde A hasta B se lee como “A satisface a B”. B podría ser una clase o una interfaz; la relación en Java puede ser de implementación o de extensión. Aquí, la clase StandardOutReporter satisface la interfaz Reporter. La característica clave de este planteamiento es que no existe ya dependencia de ningún tipo del paquete core (núcleo) con respecto a una clase del paquete gui. Todas las dependencias apuntan (¡al menos lógicamente!) desde gui a core. Para cambiar la salida de datos desde la salida estándar al mecanismo GUI, simplemente sustituiríamos la clase StandardOutReporter por la clase JtextComponentReporter, y modificaríamos el código de la clase principal del paquete gui para llamar a su constructor a las clases que realmente contienen código específico I/O. Este idioma constituye quizás el uso más generalizado de las interfaces, y merece la pena llegar a dominarlo. Recordemos que las flechas de puntos indican dependencias débiles. Una dependencia débil desde A hasta B significa que A hace referencia al nombre de B, pero no al nombre de ninguno de sus miembros. Dicho de otro modo, A sabe que la clase de la interfaz B existe, y hace referencia a las variables de ese tipo, pero no invoca a métodos de B, y no accede a campos de B. La dependencia débil de Main en relación a Reporter indica simplemente que la clase Main puede incluir código que controle a un informador genérico; lo que no supone ningún problema. Sin embargo, la dependencia débil de Folder en relación a Reporter sí que lo es. Se encuentra allí porque el objeto Reporter tiene que ser pasado a través de métodos de Folder a métodos de Compactor. Cada método en la cadena de llamada que alcanza a un método que está instrumentado debe tomar un Reporter como argumento. Se trata de una incomodidad que hace que la optimización de este planteamiento resulte demasiado laboriosa. 15 3.6.3 Interfaces y clases abstractas Cabe preguntarnos si podríamos haber usado una clase en vez de una interfaz. Una clase abstracta es aquella que no está totalmente implementada; no puede ser instanciada, pero debe ser extendida por una subclase que la complete. Las clases abstractas resultan útiles cuando se quiere reunir algún código común de varias clases. Imaginemos que quisiésemos mostrar un mensaje que nos indicase el tiempo empleado en cada paso. Podríamos implementar una clase Reporter cuyos objetos mantuviesen en su estado el tiempo de la última llamada a report (informe), y luego considerar la diferencia entre éste y el tiempo actual de la salida. Al hacer que esta clase sea una clase abstracta, podríamos reutilizar el código de cada una de las subclases específicas StandardOutReporter, JtextComponentReporter, etc. ¿Por qué no hacer que el argumento de download tenga como tipo a esta clase abstracta en vez de a una interfaz? Por dos razones. La primera es que queremos que la dependencia del código de reporter (informe) sea lo más débil posible. La interfaz no tiene ningún tipo de código; expresa la mínima especificación requerida. La segunda es que no existe la herencia múltiple en Java: una clase sólo puede incluir como máximo a otra clase. Así que cuando estemos diseñando el núcleo del programa, no nos interesa hacer uso de las subclases antes de tiempo. Una clase puede implementar cualquier número de interfaces, así que al elegir una interfaz, deja a la vista del diseñador de las clases reporter el modo en que se implementarán éstas. 3.6.4 Campos estáticos El mayor inconveniente del planteamiento que acabamos de exponer es que el objeto reporter (informe) tiene que ser enhebrado a través de todo el núcleo del programa. Si toda la salida de datos se muestra en un único componente de texto, resulta engorroso tener que pasarle una referencia por alrededor. En términos de dependencia, cada método posee al menos una dependencia débil con respecto a la interfaz Reporter. 16 Las variables globales, campos estáticos en Java, facilitan la solución a este problema. Para eliminar muchas de estas dependencias, podemos mantener al objeto reporter (informe) como campo estático de una clase: public class StaticReporter { static Reporter r; static void setReporter (Reporter r) { this.r = r; { static void report (String msg) { r.report (msg); } } Lo que tenemos que hacer ahora es colocar el static reporter (informador estático) al comienzo: StaticReporter.setReporter (new StandardOutReporter ()); y podemos enviar llamadas a éste sin tener que hacer referencia a un objeto: void download (...) { StaticReporter.report (“Comenzando descarga”); ... } En el diagrama de dependencia de módulos, el resultado de este cambio consiste en que ahora sólo las clases que realmente usan un reporter (informe) tienen dependencia de éste: 17 Obsérvese cómo la dependencia débil de Folder ha desaparecido. Por supuesto, hemos visto este concepto global antes, en nuestro segundo planteamiento, en el que el método del StandardOutputReporter era estático. Este planteamiento combina el aspecto estático con el desacoplamiento proporcionado por las interfaces. Las referencias globales resultan prácticas porque permiten cambiar el comportamiento de los métodos que se hallan en los niveles inferiores de la jerarquía de llamadas sin necesidad de introducir cambios en sus invocadores. Sin embargo, las variables globales conllevan riesgos. Pueden hacer que el código resulte terriblemente difícil de comprender. Por ejemplo, para definir el resultado de una llamada a StaticReporter.report, es necesario saber cómo está definido el campo estático r. Podría haber una llamada al método setReporter en algún lugar del código, y para ver el resultado que tiene, habría que localizar ejecuciones para tratar de ver cuándo se ha ejecutado cerca del código que nos interesa. Otro problema de las variables globales es que sólo funcionan bien cuando hay un objeto que realmente tenga una importancia constante. La salida de datos estándar es uno de estos casos. No lo son, en cambio, los componentes del texto en una GUI. Podríamos estar interesados en que las distintas partes del programa informaran de su progreso a distintos paneles de nuestro GUI. En el planteamiento en el que los objetos reporter son pasados alrededor, podemos crear 18 diferentes objetos y pasarlos a las distintas partes del código. En la versión estática, tendremos que crear varios métodos, lo que amenazaría con degradar el funcionamiento del código. La concurrencia también hace dudar acerca de la idea de usar un único objeto. Supongamos que mejoramos a nuestro cliente de correo electrónico para descargar mensajes desde varios servidores al mismo tiempo. No querríamos que el progreso de los mensajes desde todas las sesiones de descarga se viera intercalado en una única salida de datos. Una práctica que conviene seguir es no fiarse de las variables globales. Es necesario preguntarse si realmente es posible utilizar un único objeto. Normalmente hallaremos suficientes razones para tener más de un objeto alrededor. Este planteamiento recibe en la literatura de los patrones de diseño el nombre de Singleton, porque la clase contiene un único objeto. 19