Clase 3: Desacoplamiento II

Anuncio
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
Descargar