diseño orientado a objetos y software estadístico ... coruna febrer 03.pdf · diseño orientado a...

27
Diseño orientado a objetos y software estadístico: patrones de diseño, UML y composición vs. herencia Jordi Ocaña Rebull Alexandre Sánchez Pla Departament d’Estadística Universitat de Barcelona RESUMEN En este trabajo se analizan algunos aspectos del diseño de programas estadísticos. Aunque las ideas expuestas tienen una aplicabilidad más general, los ejemplos se refieren, principalmente, a cuestiones relacionadas con la simulación en Estadística. El hilo conductor de la argumentación es la idea de que bastantes de los “patrones de diseño” comúnmente empleados en otras áreas del desarrollo informático, son especialmente aplicables en la creación de programas estadísticos, a menudo reflejan de forma bastante directa conceptos estadísticos, y podrían contribuir a mejorar características del “software” estadístico como la claridad y la facilidad de mantenimiento y de ampliación. En concreto, se describen y se discuten con cierto detalle los patrones denominados “Estrategia”, “Visitante” (junto con el concepto de multimétodo) y “Decorador”, y se apunta la posible utilidad de otros patrones. La exposición se basa en gran medida en la utilización de diagramas UML (Unified Modelling Language) como herramienta de modelado expresiva e independiente (hasta cierto punto) de lenguajes de programación concretos. 1. INTRODUCCIÓN En la actualidad, existe una estrecha vinculación de la Estadística con la Informática. Esta vinculación es evidente en el usuario de un método estadístico, que precisa realizar análisis sencillos o no tan sencillos y que para ello utiliza programas estadísticos especializados o herramientas de propósito más general, como hojas de cálculo. Pero también en el desarrollo de un nuevo método estadístico surge, casi siempre, la necesidad de implementarlo de alguna manera en forma de herramienta informática, o de realizar estudios de simulación para determinar algunas propiedades del mismo. Es decir, en mayor o menor grado, una persona que investiga en Estadística es también un desarrollador informático. Chambers(2000) analiza estas cuestiones y deduce que, en realidad, existe una gradación entre estos dos extremos de utilización de la Informática en la Estadística. Un usuario que inicialmente se limitó a realizar tareas elementales (al menos desde su punto de vista, internamente estas tareas pueden ser de gran complejidad) con un programa estadístico puede verse en la necesidad de agruparlas de alguna manera para evitar la realización de tareas repetitivas, lo cual ya es un primer paso hacia la programación. Más aún (y mucho más difícil de automatizar que lo anterior), este usuario puede verse en la necesidad de extender de alguna manera la implementación disponible de determinado método estadístico. Por ejemplo, desea aplicar determinado método de clasificación jerárquica… sobre una matriz de distancias calculada a partir de un índice que no consta entre las opciones del programa que emplea. A partir de consideraciones de este tipo, Chambers(2000) propone una lista de cinco condiciones básicas que debería cumplir toda herramienta informática empleada en Estadística: 1. Especificación fácil de tareas sencillas;

Upload: doantram

Post on 19-Sep-2018

223 views

Category:

Documents


0 download

TRANSCRIPT

Diseño orientado a objetos y software estadístico: patrones de diseño, UML y composición vs. herencia

Jordi Ocaña Rebull Alexandre Sánchez Pla

Departament d’Estadística Universitat de Barcelona

RESUMEN En este trabajo se analizan algunos aspectos del diseño de programas estadísticos. Aunque las ideas expuestas tienen una aplicabilidad más general, los ejemplos se refieren, principalmente, a cuestiones relacionadas con la simulación en Estadística. El hilo conductor de la argumentación es la idea de que bastantes de los “patrones de diseño” comúnmente empleados en otras áreas del desarrollo informático, son especialmente aplicables en la creación de programas estadísticos, a menudo reflejan de forma bastante directa conceptos estadísticos, y podrían contribuir a mejorar características del “software” estadístico como la claridad y la facilidad de mantenimiento y de ampliación. En concreto, se describen y se discuten con cierto detalle los patrones denominados “Estrategia”, “Visitante” (junto con el concepto de multimétodo) y “Decorador”, y se apunta la posible utilidad de otros patrones. La exposición se basa en gran medida en la utilización de diagramas UML (Unified Modelling Language) como herramienta de modelado expresiva e independiente (hasta cierto punto) de lenguajes de programación concretos.

1. INTRODUCCIÓN En la actualidad, existe una estrecha vinculación de la Estadística con la Informática. Esta vinculación es evidente en el usuario de un método estadístico, que precisa realizar análisis sencillos o no tan sencillos y que para ello utiliza programas estadísticos especializados o herramientas de propósito más general, como hojas de cálculo. Pero también en el desarrollo de un nuevo método estadístico surge, casi siempre, la necesidad de implementarlo de alguna manera en forma de herramienta informática, o de realizar estudios de simulación para determinar algunas propiedades del mismo. Es decir, en mayor o menor grado, una persona que investiga en Estadística es también un desarrollador informático. Chambers(2000) analiza estas cuestiones y deduce que, en realidad, existe una gradación entre estos dos extremos de utilización de la Informática en la Estadística. Un usuario que inicialmente se limitó a realizar tareas elementales (al menos desde su punto de vista, internamente estas tareas pueden ser de gran complejidad) con un programa estadístico puede verse en la necesidad de agruparlas de alguna manera para evitar la realización de tareas repetitivas, lo cual ya es un primer paso hacia la programación. Más aún (y mucho más difícil de automatizar que lo anterior), este usuario puede verse en la necesidad de extender de alguna manera la implementación disponible de determinado método estadístico. Por ejemplo, desea aplicar determinado método de clasificación jerárquica… sobre una matriz de distancias calculada a partir de un índice que no consta entre las opciones del programa que emplea.

A partir de consideraciones de este tipo, Chambers(2000) propone una lista de cinco condiciones básicas que debería cumplir toda herramienta informática empleada en Estadística:

1. Especificación fácil de tareas sencillas;

2. capacidad de refinamiento gradual de las tareas;

3. posibilidades ilimitadas de extensión mediante programación;

4. desarrollo de programas de alta calidad; y

5. posibilidad de integrar los resultados de los puntos 2 a 4 como nuevas herramientas informáticas.

Una de las consecuencias más directas de las condiciones anteriores es que el estadístico debería contar con un entorno de trabajo y de desarrollo “orientado a objetos”. Aunque la adscripción de una herramienta de este tipo a este paradigma informático parece ciertamente muy conveniente, no proporciona una garantía segura de que el material desarrollado a partir de ella vaya a ser “extensible” o de “calidad”, por citar las dos palabras clave de los puntos anteriores más directamente relacionadas con la orientación a objetos. Frecuentemente, los programas desarrollados por los estadísticos, aún utilizando lenguajes de programación orientados a objetos como S o Java, caen dentro de una de dos categorías posibles, asociadas a sendas estrategias de desarrollo. Una posibilidad es ignorar completamente el paradigma de la orientación a objetos, de manera que –en el mejor de los casos en cuanto a la extensibilidad- el resultado es una librería de funciones como la que hace unas décadas se habría desarrollado en FORTRAN, con la salvedad de que las funciones están agrupadas en unos contenedores llamados “clases”, que más bien molestan. La otra posibilidad es abusar de las características más llamativas de la orientación a objetos, en especial de la herencia: todos los conceptos estadísticos representados tienen que acabar encajando en grandes y complejas clases, derivadas por herencia de otras clases más generales, en general también muy complejas. Los autores tienen que reconocer que algunos de sus intentos previos de realización de librerías de clases y de aplicaciones, diseñadas con la intención de facilitar el empleo de, y la investigación en, simulación estadística y remuestreo, incurrieron más bien en este último defecto. Este trabajo pretende plasmar parte de lo aprendido en estos pasados errores. Pretende ser una discusión sobre aspectos generales de diseño, sin entrar en consideraciones sobre la conveniencia de uno u otro lenguaje concreto, ni en consideraciones sobre la eficiencia u otras virtudes de algoritmos concretos y de sus posibles implementaciones en determinado lenguaje. La idea básica consistirá en mostrar que muchos de los llamados “patrones de diseño” son directamente aplicables al diseño de programas estadísticos, y que conciernen a partes de los mismos que se refieren a conceptos propiamente estadísticos. Los patrones se expresarán en UML (véase más adelante) para emplear una notación general, bastante expresiva. Si en alguna ocasión hay que entrar en algo más de detalle, se ilustrarán los conceptos en Java. Un tema de investigación relacionado, no tratado en este trabajo, es la posibilidad de que existan patrones de diseño específicos de este ámbito concreto de aplicación, el desarrollo de programas estadísticos, y de identificarlos si existen.

2. ALGUNOS CONCEPTOS PREVIOS Se supone que el lector parte con un cierto conocimiento de las ideas básicas del enfoque “orientado a objetos”. En caso contrario se recomienda la lectura previa de alguna descripción general del tema, como el capítulo primero de Eckel(2002), cuya versión original en inglés se puede descargar en la dirección Internet http://www.mindview.net/Books/TIJ/. La exposición resultará más motivadora si se tiene cierta experiencia en la utilización de algún lenguaje, o herramienta similar, orientado a objetos. Aunque pretende ser general, se supone que el desarrollo de programas se va a realizar dentro de lo que sería la “ortodoxia” de la programación

orientada a objetos, de manera que se supone la existencia, en su sentido habitual, de conceptos básicos como el de clase, método o función miembro, herencia, etc –el enfoque de los lenguajes “clásicos” como C++, Java, Eiffel o Samalltalk. Es cierto que, por ejemplo, es perfectamente concebible una programación orientada a objetos sin el concepto de clase, substituido por conceptos alternativos como el de “prototipo” (Hallman, 1997), que existen lenguajes que implementan este enfoque (Self, Kevo, …) y que incluso pueden resultar más apropiados en ámbitos como Internet o el modelado de patologías clínicas. Pero se trata de enfoques todavía lejos de un nivel mínimo de estandarización. Además, el enfoque (¿platónico desde un punto de vista filosófico?) basado en clases encaja bien la manera de describir el mundo de la Estadística, basado en conceptos como el de distribución o estadístico: una distribución de Poisson “es una” (es decir, “desciende de una”) distribución discreta, una distribución de Poisson de parámetro 2 es perfectamente concebible como un objeto, una realización de, la clase “Poisson”, etc.

2.1. El concepto de patrón de diseño El término “patrón de diseño” no es extremadamente preciso pero sí muy útil. En la forma en la que se suele entender actualmente fue acuñado en Gamma y col.(1994), importante referencia que se suele designar abreviadamente como GoF1. Por la manera en que se emplea este concepto en esta referencia básica y en la mayoría de usos posteriores, se refiere a una manera especialmente inteligente y perspicaz de resolver un tipo general de problema.

A pesar de constar el término “diseño” no se suele considerar que se refiera únicamente a la fase de diseño de un programa, es una solución completa que incluye análisis, diseño e implementación. Un “patrón de diseño” no se considera bien especificado hasta que se ha podido plasmar en forma de código en algún lenguaje, pero claramente no es una convención o receta “idiomática”, ligada a un lenguaje concreto, como podría la relacionada con “cómo recorrer de forma eficiente un vector en C empleando aritmética de punteros”, por poner un ejemplo. Sin embargo, en GoF se remarca que un patrón puede serlo, tener sentido identificarlo como tal o no, dependiendo del lenguaje utilizado. En el lenguaje de implementación elegido en GoF, C++, un patrón como “herencia” no tiene sentido (ya está implícito en el propio lenguaje) mientras que sería una solución general muy adecuada a numerosos problemas si el lenguaje de implementación fuese C, por ejemplo. En un ámbito de problemas y de conceptos más cercano a la Estadística, y que trataremos con mayor detalle más adelante, un patrón de diseño muy común como “objeto función” según la terminología de Eckel(2003) o “comando” según la terminología de GoF, tiene sentido en Java o en C++, pero no en Python, donde todo, y en concreto cualquier método o función miembro, es un objeto.

Tampoco hay que confundir “patrón de diseño” con un algoritmo adecuado para resolver un tipo concreto de problema –que, lógicamente, se tendría que implementar de forma distinta según el lenguaje. Un patrón de diseño debe ser una especificación muy general, una especie de invariante que se cumple en problemas de ámbitos diversos y que define una solución general y muy estable.

1 Por gang of four: E. Gamma, R. Helm, R. Johnson y J. Vlissides, “la banda de cuatro”. GoF suele servir para referirse a los autores, y en itálica, GoF, al propio libro. Los componentes de la banda de cuatro que nos ocupa no cayeron en desgracia como los integrantes del grupo de dirigentes chinos que recibió el mismo nombre durante la Revolución Cultural.

A modo de intento de ilustración del concepto de patrón, vamos comentar uno de los patrones identificados y analizados en GoF y cuya comprensión es crucial para entender el resto de este trabajo. Posiblemente es el patrón de diseño más utilizado y entendido sin problemas por los usuarios de las herramientas de programación orientadas a objetos. Nos referimos al patrón denominado en inglés Template Method, que tal vez se podría traducir como “Método prototipo”.

Lo que en inglés se suele denominar application frameworks, es decir las librerías de clases y similares que han sido diseñadas para ser utilizadas en la elaboración de aplicaciones concretas mediante especialización posterior de sus clases, suelen estar llenas de “métodos prototipo”. Se trata de métodos que expresan secuencias de operaciones habituales en determinado ámbito. Por ejemplo, en Estadística un fragmento de tal método podría ser (empleando una sintaxis cercana a la de Java –y en particular considerando el texto a la derecha de // como un comentario hasta el final de la línea): …

Data datos; // una variable “datos” StatResult resultado;// variable “resultado de cálculo estadístico” // Data y StatResult son clases muy generales

resultado = datos.summarize( ); resultado.print( ); resultado.plot( );

Cuando de verdad se utilice este método, el objeto datos sobre el que se ejecutarán las instrucciones anteriores será de una clase descendiente de Data, mucho más especializada que ella, una clase que probablemente ni existía cuando se creó el método patrón. La clase que realmente tendrá resultado también será una especialización de StatResult; de hecho la clase de resultado probablemente la decidirá el método summarize asociado a la auténtica clase del objeto datos. Qué pasará realmente cuando se impriman y se dibujen unos resultados estadísticos contenidos en el objeto resultado dependerá de la definición concreta de los métodos print y plot para la clase de resultado. De esta manera se puede programar de una manera muy genérica y al mismo tiempo con unas posibilidades casi ilimitadas de extensión.

2.2. Nociones sobre UML Tal como especifica en la documentación oficial de UML (véase, por ejemplo OMG, 1999) que se puede consultar, actualizada, en www.rational.com, UML (Unified Modelling Language) se define como un “lenguaje consistente para especificar, visualizar, construir y documentar los artefactos de los sistemas de software, así como para el modelado de negocios”. Efectivamente, UML es una notación, definida a múltiples niveles, adecuada para especificar modelos, especialmente si éstos utilizan conceptos orientados a objetos. La parte más llamativa y más utilizada es la notación gráfica. UML nació en 1994 como una fusión de los métodos y las notaciones de G. Booch y J. Rumbaugh, a los que posteriormente se unió I. Jacobson2. Fue propuesto como un estándar en 1997, ante el OMG (Object Management Group), asociación dedicada a hacer propuestas de estándares para la industria. Al margen de este

2 De todas maneras, tiene que quedar claro que UML es únicamente una notación, no una metodología de modelado.

reconocimiento oficial, finalmente obtenido, se ha ido imponiendo como un estándar “de facto”. Prueba de ello es que se utiliza como notación en toda clase de documentos y publicaciones, y no solamente en las relacionadas con el modelado de software o el “modelado de negocios”, como indica la documentación oficial. Por ejemplo, Garrido(2001), un libro dedicado a la simulación discreta, representa los modelos a simular utilizando la notación gráfica de UML. Otra prueba del éxito de UML es la multitud de herramientas CASE disponibles para utilizar cómodamente esta notación.

La notación gráfica de UML consta de varios tipos de diagramas. El lector interesado puede consultar alguna obra introductoria, como Stevens y Pooley(2000). En este trabajo utilizaremos exclusivamente “diagramas de clases”, suficientes para expresar las relaciones entre clases implicadas en los patrones de diseño descritos. Los diagramas son bastante expresivos y se intentarán complementar con explicaciones “sobre la marcha”.

3. ¿QUÉ ALGORITMO UTILIZAMOS?

3.1. Planteamiento Es evidente que la elección del algoritmo más adecuado para resolver un problema es crucial en Estadística computacional, y en muchas otras áreas. A menudo debemos escoger entre más de un algoritmo posible de solución de un mismo problema. En ocasiones las diferencias entre los algoritmos posibles se hallan en aspectos puramente computacionales –es decir, para (intentar) obtener la misma solución desde un punto de vista matemático podemos optar entre varios algoritmos, que difieren en su velocidad, en su estabilidad numérica, etc. Este es el caso de los, a menudo numerosos, métodos de generación de variables aleatorias disponibles para una misma distribución. En otras ocasiones la solución no es ni siquiera la misma en teoría, si bien es una de las soluciones aceptables en una aplicación concreta. Por ejemplo, es frecuente poder optar entre varios posibles métodos de estimación para estimar los parámetros de un modelo no lineal, por citar una situación común.

En cualquier caso, lo más común es que no exista “el mejor” algoritmo. Dependiendo de qué se pretende hacer y de las limitaciones, posiblemente será mejor utilizar un algoritmo u otro. Por ejemplo, un algoritmo de generación aleatoria puede ser más veloz para determinado rango de los parámetros de la distribución considerada, pero comparativamente ineficiente para otros valores de los parámetros. Incluso es posible que un algoritmo basado en la técnica de rechazo sea siempre el más eficiente, pero que en ocasiones sea preferible realizar una simulación empleando un algoritmo de inversión de la función de distribución, para aprovechar las propiedades de monotonicidad de este método –volveremos a hablar de este tema en la sección final. El estadístico que se plantea escribir una librería de funciones o de clases que sea útil para los investigadores que precisen hacer simulaciones de Montecarlo, y por lo tanto generar variables aleatorias, se encuentra, inexorablemente a poco que medite cuál es el diseño más adecuado, con el dilema de qué métodos de generación programa, y en general de cómo organiza todo este material (distribuciones concretas, métodos de generación para estas distribuciones, etc).

El camino más directo para crear una tal librería es, por ejemplo, el seguido por el lenguaje S (o R) y por paquetes estadísticos como S-Plus, SPSS o Statgraphics. Se dispone de un repertorio fijo de posibles distribuciones, las que se haya considerado que son las más básicas y, de una manera u otra (llamada a una función, menú,…), se da la

opción de generar valores según la distribución elegida, sin controlar el método concreto de generación empleado –de hecho, en general sin documentar de ninguna manera el método empleado. Ciertamente esto permite cumplir el objetivo de hacer fáciles las cosas básicas, pero no permite una utilización más avanzada por parte de usuarios más avanzados. Este enfoque resuelve el dilema citado en el párrafo anterior simplemente ignorándolo.

Todavía bajo un enfoque funcional, una solución consistiría en crear una función de generación aleatoria para cada distribución, con todos los algoritmos de generación considerados codificados en la misma. El código de dicha función sería seguramente complejo y largo, aunque se podría hacer más modular e inteligible delegando determinadas tareas en otras funciones, que, por ejemplo, acabasen de implementar los detalles concretos de cada posible algoritmo alternativo –pero en todo caso sería conveniente la existencia de esta función “fachada” para simplificar la organización del código, desligando los detalles de cuáles serían los parámetros propios de cada distribución concreta, control de errores en los valores de estos parámetros, etc de los detalles referentes a cómo se realiza la generación. También sería algo difícil de utilizar, debido a los numerosos y complicados parámetros de entrada que requeriría. Esta dificultad sería, no obstante, relativamente fácil de soslayar con un buen sistema de ayuda y tal vez con una buena elección de opciones por defecto –si fuese posible en el lenguaje empleado. Una dificultad mucho más seria se presentaría cuando el algoritmo de generación adecuado en un caso concreto no estuviese inicialmente previsto en el código inicial. En este caso, el problema podría resultar sencillamente insoluble para un usuario que desease aprovechar parte de lo codificado previamente (solamente hacer algunos retoques o adiciones) y que no dispusiese del código fuente de la función. La única solución posible será codificar “desde cero”, “reinventar la rueda”. Disponiendo del código fuente, seguramente sería posible entrar en los detalles internos de la función y hacer los cambios deseados, tarea difícil incluso para el propio autor de la función que, repetimos, posiblemente sería bastante larga y complicada.

En principio, un enfoque más conveniente lo proporcionaría una librería de clases que, de alguna manera, representaría los conceptos estadísticos implicados. Parece que una buena opción (como mínimo es la más comúnmente empleada en las librerías existentes en este ámbito) es organizar las distribuciones de probabilidad en una jerarquía de clases. La organización concreta de esta jerarquía es una cuestión bastante delicada. Supongamos que todas estas clases fuesen descendientes de una clase ancestora común ProbabilityDistribution, a su vez con descendientes como UnivariateDistribution, con descendientes como UnivariateDiscrete o UnivariateAbsolutelyContinuous, de las cuales descenderían clases más concretas como Poisson o Normal. Las clases más generales serían, normalmente, abstractas. No se instanciarían nunca (es decir, nunca se crearían objetos de aquella clase) pero permitirían definir partes comunes de la interfaz, y posiblemente de la implementación, de sus clases descendientes. Aparte de otros métodos o funciones miembro dedicados a otras tareas propias de lo que se considerase la interfaz propia de estas clases (por ejemplo, cdf para calcular la función de distribución o quantile para determinar cuantiles), todas ellas contarían con una función miembro asociada llamada, por ejemplo, nextRand, encargada de la tarea de generar valores aleatorios según aquella distribución.

Ahora la organización del código sería mucho más lógica y modular. Para un objeto de (una instancia de) la clase Normal estaría claro el significado de una llamada a su método densidad, que tendría un efecto evidentemente distinto a la llamada del mismo método para un objeto de clase Binomial –lo cual ayudaría a solucionar el problema de

nombres para designar todas estas funciones en un lenguaje funcional: binomialCdf, binomialPdf, NormalCdf, NormalPdf, …

Figura 1. Elección del método de generación basada, exclusivamente, en la herencia

En un primer impulso (muy normal en quien se inicia en la programación orientada a objetos), la implementación de los posibles algoritmos de generación para una misma distribución se podría llevar a cabo especializando por herencia otras clases. Por ejemplo, para implementar el algoritmo de generación de Ahrens y Dieter(1988), aplicable a la distribución normal, una posibilidad sería crear una nueva clase, NormalAhrensDieter1988, especializando una clase general Normal, sobrescribiendo su método nextRand –pero lógicamente aprovechando (heredando) las otras partes de la misma. Normal podría implementar un método sencillo (por ejemplo, el conciso aunque no especialmente eficiente algoritmo de Box y Muller, 1958) o simplemente ser abstracta y no proporcionar ninguna implementación para nextRand.

Aunque el enfoque anterior funciona, es un claro sobre abuso de la herencia, concepto por otra parte ciertamente útil. Produciría una jerarquía de clases muy intrincada, con árboles de gran profundidad, difícil de entender y de mantener. La Figura 1 trata de reflejar este hecho, empleando la notación UML. Las clases se representan mediante rectángulos divididos en tres zonas (que no siempre se muestran), la primera de ellas indica el nombre de la clase, la central puede informar de posibles atributos (variables) contenidos en los objetos de la clase, la parte inferior se dedica a mostrar los métodos

asociados a la clase. La relación de especialización (herencia) se muestra con una flecha en la que la línea es continua y la punta está vacía. La flecha siempre señala de la clase más especializada (la descendiente) a la más general (la superclase).

Piénsese además en la todavía mayor cantidad de clases que se generarían si, además del posible algoritmo de generación, se considerasen criterios adicionales de especialización del concepto de distribución.

Un enfoque alternativo consistiría en dotar ya de entrada a cada clase con métodos distintos para los distintos algoritmos de generación. Por ejemplo, se crearía la clase Normal ya con los métodos boxMullerNextRand, ahrensDieter1988NextRand, etc. Posiblemente también existiría un método general nextRand que internamente llamaría a alguno de los otros, de acuerdo con el valor de algún parámetro de entrada que especificase el algoritmo a emplear.

Figura 2. Estructura general del patrón de diseño Estrategia

Son numerosas las objeciones que se pueden poner al enfoque anterior. Si los métodos que implementasen cada algoritmo (boxMullerNextRand, ahrensDieter1988NextRand, …) fuesen públicos, es decir, fuese posible acceder a ellos desde el exterior de la clase, produciríamos clases con unas interfaces realmente complicadas, largas y distintas de una clase (distribución) a otra. Un principio de diseño elemental es el de crear interfaces cuanto más cortas y claras mejor. Alternativamente, estos métodos podrían ser privados, no ser accesibles desde el exterior de la clase. En este caso no serían tampoco modificables, aprovechables por herencia: no sería posible crear una simple variante de alguno de ellos. Ciertamente una posibilidad adecuada sería hacerlos “protegidos”, solamente accesibles por la propia clase y sus descendientes. Pero, en cualquier caso, está claro que este enfoque no es más que un remedo del enfoque basado en crear

múltiples funciones o una función muy general y compleja.

En realidad todo lo planteado para los posibles algoritmos de generación de cada distribución (y planteable para otras muchas situaciones en Estadística: algoritmos o métodos de estimación de los parámetros de una distribución o de alguna clase de modelos, algoritmos de optimización diversos, …) es muy común en muchas otras

áreas. Un patrón de diseño adecuado para esta clase general de problemas es el denominado “Estrategia” (Strategy en GoF) y esquematizado en la Figura 2. En este diagrama se introducen tres nuevos aspectos de la notación: por una lado las interfaces, conjuntos de métodos que especifican a qué mensajes será capaz de responder una clase. Se indican igual que una clase con un “estereotipo”, una etiqueta con un significado especial en UML, que señala que se trata de una interfaz. Este concepto se discute un poco más abajo. Por otro lado, la relación que une determinada clase con una interfaz a la que “implementa” se indica con una flecha similar a la que representa la herencia, pero con la línea discontinua. Finalmente, la relación de “composición” entre un objeto compuesto (en este caso, una distribución) y una de sus partes (en este caso, un algoritmo de generación) que se representa con una línea terminada con un rombo, situado siempre en el lado del objeto compuesto3.

3.2. Elección de algoritmo en tiempo de ejecución: patrón Estrategia En este patrón, los posibles algoritmos alternativos se implementan en una jerarquía de clases independiente de la jerarquía de clases de posibles clientes de estos algoritmos. Todos los algoritmos deben responder a una misma interfaz, deben de ser capaces de responder a determinado mensaje, implementado en un método adecuado, por ejemplo execute –ejecuta el algoritmo. Las clases que van a ser “clientes” de estos algoritmos, deben tener en su definición un atributo que sea una referencia al objeto algoritmo concreto que en un momento dado está activo, vigente para aquel cliente. El método de la clase cliente que realiza la operación concreta que depende del algoritmo, operation en la Figura 2, en general se limitará a activar el método execute del objeto algoritmo que en aquel momento tiene asignado como activo.

3 Dado que, tal como se comentará más adelante, la duración del objeto que representa el algoritmo de generación debería estar limitada por la duración del objeto distribución, la relación es una verdadera “composición”, que se representa mediante un rombo en negro, relleno, y no una “agregación” que se representa mediante un rombo vacío, en blanco.

Figura 3. El patrón Estrategia en la generación de variables aleatorias

En la Figura 3 se ilustra el patrón Estrategia adaptado a la situación planteada anteriormente, la elección dinámica del algoritmo de generación. Otro ejemplo muy interesante, en el que los autores “redescubren” (o se adelantan a GoF) el patrón Estrategia, se halla en Hitz y Hudec (1994) para la estimación del elipsoide de volumen mínimo en la determinación la localización y la forma multivariante. En la Figura 3 se ponen de manifiesto algunos aspectos adicionales, presentes en general en la realización concreta de este patrón:

Gracias a la herencia, basta con que una superclase general de la jerarquía de clases cliente (UnivariateDistribution en el ejemplo) tenga definida la referencia, genMethod, a los objetos que implementan algoritmos de generación, y tenga definido el método, nextRand, que activa el algoritmo. Nótese que esta referencia es genérica, se define como de tipo GenerationMethod, lo cual no impide que los objetos-algoritmo concretos a los que hace referencia sean en la práctica de clases más especializadas (AhrensDieter1988, BucketsInversion,…). La validez del sistema queda asegurada por el hecho de que todas estas clases concretas implementan la interfaz especificada por GenerationMethod, son capaces de responder al mensaje adecuado, en este caso nextRandom. Los detalles finales dependen del lenguaje de programación concreto. En C++ GenerationMethod será una verdadera clase, seguramente abstracta, y los algoritmos concretos deberán estar representados por clases que, obligatoriamente, desciendan de ella. Hay más flexibilidad en Java o en Object Pascal-Delphi, en los que existe el concepto diferenciado de tipo interfaz (interface), de manera que las clases que corresponden a algoritmos no deben pertenecer, forzosamente, a una misma jerarquía de herencia, basta con que implementen la interfaz GenerationMethod, es decir, que sean capaces de reaccionar a los mensajes definidos en ella. En realidad es un principio básico del diseño orientado a objetos que, siempre que sea posible, es preferible programar pensando en interfaces y no en clases, mucho más ligadas a una

implementación concreta. Con algunos cambios, se podría especificar un diseño todavía más general y flexible en el que UnivariateDistribution, FiniteUnivariate, Normal, Binomial, etc serían interfaces.

Al igual que las distribuciones deben tener una referencia al algoritmo de generación empleado, es conveniente que los algoritmos de generación tengan una referencia a la distribución concreta (al objeto) que están generando, por ejemplo para poder consultar los valores de sus parámetros. A diferencia de la referencia distribución algoritmo que es genérica (genMethod es de tipo GenerationMethod), la referencia algoritmo distribución es conveniente que sea más específica, lo cual puede ayudar a controlar posibles errores de empleo de algoritmos de generación inadecuados para determinada distribución. Por ejemplo, así se especifica que el algoritmo AhrensDieter1988 solamente es válido para la clase Normal (o descendientes) mientras que BucketsInversion es válido para cualquier objeto de una clase descendiente de FiniteUnivariate.

La principal ventaja de este patrón de diseño es la mayor modularidad que proporciona. Las clases correspondientes a las distribuciones (o a otros conceptos estadísticos) y las clases correspondientes a los algoritmos concretos de generación (o de estimación, de búsqueda en un árbol…) se pueden mantener y crecer de forma independiente. Su principal inconveniente (como de la mayoría de patrones) es que añade un nivel de direccionamiento adicional, que puede implicar una cierta carga de computación adicional. Dependiendo de la naturaleza de lo realizado por los algoritmos, este precio puede ser, en términos relativos, despreciable. Por ejemplo, así será normalmente si el algoritmo de generación no genera un solo valor, sino muchos, cada vez que se activa.

3.3. Patrones de diseño relacionados con Estrategia Es común que al mismo problema o situación sea aplicable más de un patrón de diseño, ya sea por que son posibles soluciones alternativas, ya sea por que intervienen en partes distintas de la solución. Como mínimo dos patrones adicionales, cuya descripción no detallaremos, pueden intervenir en la situación concreta que estamos analizando, la generación de variables aleatorias. Se trata de los patrones “Factoría” y “Objeto nulo”.

Nótese que es indispensable que los objetos distribución y los objetos algoritmo de generación estén siempre sincronizados, en el sentido de que si, por ejemplo, a un objeto que representa una distribución se le modifican los valores de los parámetros, este cambio también tiene que afectar al objeto que representa el algoritmo de generación asociado –que posiblemente almacena internamente valores que será preciso actualizar en función de los parámetros de la distribución. Existen diferentes maneras de manejar esta necesidad de coherencia entre objetos. Una solución completa la proporciona el patrón “Observador” (Observer) aunque parece demasiado complicado para la situación tratada. Dado que en la relación entre un objeto distribución y el algoritmo de generación asociado solamente intervienen dos objetos, parece bastar con que estos objetos estén fuertemente vinculados, en concreto que solamente se pueda acceder al algoritmo de generación (incluyendo el hecho de crear el correspondiente objeto algoritmo) desde el objeto distribución. La duración de los objetos algoritmo también tiene que estar limitada por la duración de los objetos distribución, no pueden existir algoritmos “libres” no asociados a una distribución. Para ello basta con que las referencias citadas en párrafos anteriores (campo genMethod) sean privadas y que los algoritmos de generación no tengan constructores accesibles, públicos. Ello nos conduce a la necesidad de utilizar alguna de las variantes del patrón de diseño

denominado comúnmente Factoría (Factory), que en cierta manera independiza la creación de objetos en un cierto punto de un programa, del empleo de sus constructores, que obligan a hacer una referencia explícita a la clase concreta. De las variantes de este patrón, la más adecuada parece la denominada “Factoría polimórfica” (Polymorphic Factory) descrita en Eckel(2003). Mediante la implementación de este patrón es posible crear objetos de determinada clase simplemente llamando un método de una clase general (la factoría, en nuestro ejemplo bajo control de las clases distribución) y especificando una cadena de caracteres con el nombre de la clase deseada para el objeto, como información de entrada a dicho método. Este enfoque de creación de algoritmos de generación está implementado en la librería de clases Montecarlo, escrita en Java por el primer autor y a cuya versión preliminar se puede acceder en <<falta: referencia a pagina web>>.

La necesidad de sincronización entre objeto distribución y objeto algoritmo de generación puede representar cierta carga computacional extra (si reinicializar el algoritmo de generación implica cálculos complejos), que convendría evitar si va a ser frecuente que se modifiquen los parámetros de un mismo objeto distribución sin que durante el mismo intervalo de tiempo se vaya a producir ninguna generación aleatoria. Una posibilidad es hacer que (siempre al crearlo, y también cuando así se indique explícitamente) el objeto distribución no haga referencia a ningún algoritmo de generación. Pero este enfoque puede provocar algunos problemas de coherencia, y en particular que, en muchos lugares, los métodos de la distribución tengan que incluir sentencias condicionales (if…) para verificar si, en un momento dado, aquella distribución tiene asociado o no un algoritmo de generación, lo cual perjudica la simplicidad y legibilidad del código, como mínimo. Una solución sencilla la proporciona el patrón de diseño “Objeto nulo” (Null object), véase por ejemplo Henney(1999), que se basa en la definición de una clase que responda a la interfaz de los algoritmos pero dotada de métodos nulos, que no realicen ninguna acción. La librería Montecarlo, citada antes, incluye una variante de este patrón en la que el generador nulo sí que realiza una tarea importante: ante un mensaje en el que se solicite generación aleatoria, el generador nulo construye un generador “de verdad” que queda asociado a la distribución y que captura la petición de generación.

4. ¿DEBO INCLUIRLO TODO EN LA INTERFAZ?

4.1. Introducción Un error de diseño muy habitual al crear una librería de clases es pretender que éstas incluyan métodos e información para manejar todos los conceptos de la correspondiente área de aplicación. Por ejemplo, al crear una librería de clases de distribuciones de probabilidad, una estrategia comúnmente seguida es incluir, ya de entrada en las superclases iniciales, gran cantidad de métodos o funciones miembro, todo aquello que el autor prevea que va a ser solicitado de dichas clases: cálculo de la función de densidad, de la función de distribución, media, varianza, momentos (¿función general o hasta que orden?), moda, cuantiles,… Procediendo de esta forma se pretende conseguir que todos los objetos creados a partir de las clases descendientes, más concretas, respondan a una misma interfaz, muy general y amplia. Este diseño contradice el principio de que es deseable la máxima modularidad. Las interfaces deberían ser cuanto más concisas mejor: toda clase debería tener unas atribuciones muy concretas, asociadas a una interfaz lo más concisa posible. En cualquier caso, implementar en la práctica una tal librería puede representar una cantidad de trabajo enorme y no exento de

curiosidades como tener que “desactivar” métodos que no tengan sentido para determinadas clases concretas. Evidentemente existen varias posibilidades aceptables, pero ¿qué hay que hacer con el método que calcula la media, la varianza, etc de una distribución de Cauchy?

Un enfoque alternativo al anterior es partir de unas superclases muy escuetas e ir ampliando su interfaz mediante el uso de la herencia. El resultado final son interfaces de las clases concretas muy amplias pero que no corresponden a una interfaz común, lo cual no facilita la programación genérica –recuérdese lo indicado para el patrón de diseño “método prototipo”. Ahora no es posible referirse en determinados lugares a un objeto de una clase muy general (como ProbabilityDistribution o UnivariateDistribution) contando con que aquel código funcionará para objetos de clases mucho más específicas, Poisson, Gamma, etc, clases que incluso pueden no existir en el momento de la programación del código genérico.

Figura 4. Multiplicación de clases al extender por herencia

En cualquier caso, ninguno de los dos enfoques anteriores permite una extensión fácil, especialmente si no disponemos de (o no queremos modificar) el código fuente de la librería de clases ya creada. La Figura 4 trata de reflejar la multiplicación de clases que provocaría la necesidad de incluir un método como interQuartilicRange que, inexplicablemente (tal vez por agotamiento) había sido omitido en la jerarquía de clases original. La jerarquía tiene omisiones evidentes para hacerla sencilla, pero al mismo tiempo pretende ilustrar que de nada serviría añadir el método en una superclase como UnivariateDistribution, si esto tuviese sentido. Las clases concretas Binomial, Poisson,

etc no descenderían de ella y por lo tanto no heredarían el método.

Una vía de solución más elegante se basa en crear, independientemente de la jerarquía de clases inicial que se desea ampliar, nuevas clases que representen las nuevas funciones que se desean incorporar, es decir, alguna variante de “objeto función” o “comando”, citado anteriormente. Así se podría crear una clase denominada InterquartilicRange con una función miembro eval, que devolviese valores reales (por

ejemplo double de Java) y que esperase recibir un argumento de entrada de una clase general como UnivariateDistribution. En Java se especificaría algo como: public class InterquartilicRange { public double eval (UnivariateDistribution distri) { // cálculos para determinar el recorrido intercuartílico … } … }

Se supone que eval(UnivariateDistribution distri) sería suficientemente general como para evaluar el recorrido intercuartílico para todas las distribuciones univariantes. Pero aún en este caso, parece lógico pensar que para algunas distribuciones concretas el mismo cálculo podría ser mucho más eficiente, de manera que parecería muy razonable ampliar (ya de entrada o en un descendiente) la interfaz de la clase InterquartilicRange añadiendo métodos para manejar el caso de distribuciones más específicas4. Además, si de una manera genérica se desea poder cambiar la “función estadística”, entendida aquí como función de un objeto que represente una distribución, seguramente la clase InterquartilicRange tendría que descender de una clase más general, llamémosla por ejemplo StatFunction, con, como mínimo, el método general, de “firma” public double eval (UnivariateDistribution distri) definido en su interfaz: public class InterquartilicRange extends StatFunction{ public double eval (UnivariateDistribution distri) { // cálculos para determinar el recorrido intercuartílico // en general … } public double eval (Normal distri) { // cálculos para determinar el recorrido intercuartílico // para la distribución normal … } … }

Concretando más, consideremos el siguiente fragmento de código genérico: StatFunction operator; UnivariateDistribution distr; double valor = operator.eval(distr);

Sintácticamente, lo anterior es correcto. Pero es posible que no haga lo que se podría pensar en principio. Si la auténtica clase de operator es InterquartilicRange y la de distr es Normal, incluso alguien con experiencia en la programación en Java, podría pensar que la tercera línea del ejemplo ejecutaría el método, más eficiente y adecuado en este caso, de “firma” public double eval (Normal distri). La realidad es que, en este caso, siempre se ejecutará el método más general. Esto se debe a que, en tiempo de ejecución, se decide la clase concreta para la que se ejecutarán los métodos (siempre que se trate de un método virtual, en Java todos los métodos son virtuales). Por lo tanto, correctamente, se buscarán los métodos disponibles en la clase InterquartilicRange. Pero la sobrecarga de métodos, en la mayoría de lenguajes (C++, Java,…) es aplicable al momento de la compilación, no a la ejecución del programa. Cuando está analizando las tres líneas de código anteriores, el compilador desconoce la clase concreta del objeto que recibirá como valor de entrada el método eval, método asociado a un objeto del cual, lo único

4 En este caso este proceder no complica demasiado la interfaz (como mínimo en lenguajes que permiten “sobrecarga” de métodos), ya que todos los métodos se llaman igual, eval. Simplemente se proporcionan opciones más específicas y eficientes, cuando las hay.

que se puede asegurar, es que será de clase StatFunction o de alguna de sus descendientes (totalmente desconocida en ese momento). Como la interfaz de StatFunction incluye el método public double eval (UnivariateDistribution distri), el código resulta correcto sintácticamente y queda establecido que, en su momento, se ejecutará un método con aquella firma, definido en la clase que corresponda, pero no un método de firma public double eval (Normal distri), posibilidad ni siquiera considerada en el momento de la compilación.

4.2. Métodos múltiples y el patrón de diseño Visitante En la discusión previa se ilustra un concepto importante, tanto en general como en computación estadística. Se trata del concepto de “método múltiple” o “multimétodo”, véase, por ejemplo, Saar(2000). La mayoría de lenguajes orientados a objetos (incluyendo los más comunes, como Smalltalk, Java y C++) implementan un “mecanismo de resolución” simple. Los métodos son “despachados” o “resueltos” (el sistema decide ejecutar una u otra versión de los mismos) en función de la clase del objeto que activa el método –la clase real, no necesariamente la declarada en una parte de código donde interviene, que puede ser una superclase de la primera. Un multimétodo, en cambio, tiene un mecanismo de “resolución múltiple” (por traducir de una manera que no suene muy mal el término comúnmente empleado en inglés multiple dispatch): se decide qué método ejecutar en función no solamente de la clase del objeto que activa el método sino también de la clase real de los restantes argumentos –o de parte de ellos.

Este útil concepto ha sido elegantemente incorporado en el lenguaje S, a partir de su versión 4 (que no hay que confundir con el número de versión de un programa basado en S, como S-plus), véase Chambers(1998). En un lenguaje como Java, utilizando el mecanismo de reflexión (por el cual, los objetos pueden conocer cosas de si mismos o de otros objetos, como su auténtica clase), o por otras vías, es posible simular el concepto de método múltiple (Boyland y Castagna, 1997). Si solamente es necesario implementar métodos de “resolución doble” (entendidos como métodos que se ejecutan en función del objeto que los activa y de uno solo de sus parámetros) y si la necesidad de extensión mediante un mecanismo de este tipo ya estaba prevista en la jerarquía de clases original, una solución limitada pero clara y fácil de implementar en lenguajes que no dispongan de multimétodos la proporciona el patrón de diseño “Visitante” (Visitor).

La Figura 5 trata de esquematizar este patrón de diseño. En Visitante, la necesidad de resolución doble se implementa mediante una doble llamada a métodos de resolución simple, la única disponible. Todas las clases de la jerarquía cuyo repertorio de métodos o funciones se quiere ampliar (en este caso las clases de la jerarquía que parte de VisitableClass) deben implementar un método, denominado accept en la Figura 5, que haga que los correspondientes objetos “acepten” ser visitados por objetos de clases que implementen la interfaz Visitor. Estas últimas clases (las Visitor) son las que realmente implementan las nuevas funciones (cada clase un tipo específico de función), en el ejemplo mediante los métodos actionOver. Dado que la llamada a la correspondiente función actionOver se realiza dentro de un método de la clase “visitada”, lugar en el que está perfectamente clara cuál es la clase del propio objeto que activa el método (la referencia this en Java) se activará la versión de actionOver más apropiada, la que escogerá el mecanismo de sobrecarga de métodos ya en tiempo de compilación.

Figura 5. Estructura general del patrón de diseño Visitante

Como consecuencia de la aplicación del patrón Visitante, con la incorporación de un único método, accept, en todas las clases de una determinada jerarquía, la interfaz de todas estas clases se puede ampliar con un número ilimitado de nuevas funciones, funciones encapsuladas en clases que implementen una interfaz dada, Visitor en las

definiciones anteriores.

El punto más débil de este patrón es el hecho de que la interfaz Visitor debe incluir métodos para todas las clases de la jerarquía de VisitableClass que tengan que ser visitables directamente, es decir, para las cuales se vaya a definir un método actionOver específico. Si se amplía la jerarquía de VisitableClass y se desea crear métodos actionOver cuya firma incluya las nuevas clases, será necesario ampliar y recompilar la interfaz Visitor y las clases que la implementan. Dentro de lo que cabe, esto no es tan grave ya que la jerarquía de VisitableClass será normalmente más estable que la de Visitor. Esta última estará bajo el control del usuario, que la utilizará como vía de ampliación de la primera.

La Figura 6 ilustra el patrón Visitante para ampliar las funciones ejecutables sobre una jerarquía de distribuciones que sólo incluye cuatro distribuciones “concretas”, Binomial, Poisson, Normal y Exponential, y tres superclases más generales, UnivariateDiscrete, UnivariateAbsolutelyContinuous y UnivariateDistribution. Esquemáticamente, las correspondencias entre la definición general del patrón y este ejemplo son:

Caso general (Figura 5) Ejemplo (Figura 6)

Jerarquía VisitableClass y descendientes Jerarquía UnivariateDistribution y descendientes

Método accept en la jerarquía de VisitableClass

Método evaluate en la jerarquía de UnivariateDistribution

Interfaz Visitor Interfaz StatFunction

Clases FirstVisitor, SecondVisitor que Clase InterquartilicRange que implementa

implementan la interfaz Visitor la interfaz StatFunction

Método actionOver Método eval

La jerarquía con raíz en la clase UnivariateDistribution se amplía con una nueva función estadística (un posible “visitante”), representada por la clase InterquartilicRange, que implementa la interfaz StatFunction. El método evaluate, presente en todas las clases de la jerarquía de distribuciones, siempre tiene la misma forma, en Java sería: public double evaluate (StatFunction func) { return func.eval (this); }

Si la auténtica clase del objeto unaDistr es Binomial y la de unaFunc es InterquartilicRange, la última línea de UnivariateDistribution unaDistr; StatFunction unaFunc; … double val = unaDistri.evaluate(unaFunc);

Figura 6. El patrón Visitante en una jerarquía de distribuciones de probabilidad

se traducirá, en última instancia, en una llamada del método eval(Binomial …) de

InterquartilicRange, que se supone que es lo que se pretendía.

Las tres superclases UnivariateDiscrete, UnivariateAbsolutelyContinuous y UnivariateDistribution intervienen en el patrón (constan en la interfaz StatFunction y en las clases que la implementan) ya que, posiblemente, para ellas se crearán versiones generales del método eval, que serán aplicables a sus clases descendientes que no hayan sido explícitamente incluidas en el patrón. Por ejemplo, supóngase que descendiendo de

UnivariateDiscrete, se crea una nueva clase NegativeBinomial a la que no se incluye en el patrón, es decir, no se añade un método eval específico para esta clase en la interfaz StatFunction y en la clase InterquartilicRange y similares. Si unaDistr es en realidad un objeto de clase NegativeBinomial y unaFunc sigue siendo un objeto de clase InterquartilicRange, en la última línea del código anterior el cálculo correspondiente será finalmente realizado por el método eval(UnivariateDiscrete …) de la clase InterquartilicRange, tal como ocurriría también en el caso de la extensión por herencia.

No es necesario el patrón Visitante, asociado a las limitaciones comentadas anteriormente, si realmente se utiliza una verdadera implementación de los multimétodos. En un lenguaje como Java, como ya se ha comentado anteriormente, es posible simular este mecanismo, en general utilizando el mecanismo de la reflexión, y por lo tanto pagando un precio considerable en eficiencia.

5. PATRONES DE DISEÑO EN SIMULACIÓN ESTADÍSTICA

En esta sección final se discute con algo más de detenimiento el diseño de programas que implementen métodos de Montecarlo en Estadística. Este ejemplo, algo más complejo que los anteriores, es un escenario en el que son aplicables algunos de los patrones introducidos en las secciones precedentes, además de motivar el empleo de algunos patrones adicionales.

5.1. Un proceso de simulación se puede representar como un “objeto compuesto” La Figura 7 refleja la estructura habitual de los procesos de simulación de Montecarlo en Estadística. En ellos hay una serie de elementos (¿objetos?) y subprocesos que casi siempre están presentes:

• Un modelo probabilístico, completamente especificado –p.e. tipo de distribución y valores de los parámetros,

• un proceso de muestreo sobre el modelo anterior –p.e. generación de muestras con observaciones independientes e idénticamente distribuidos, iid, de un tamaño prefijado, a partir de la distribución anterior,

• un estadístico que se evalúa sobre cada una de las muestras generadas y que produce un resultado de cierto tipo –p.e. cierta prueba de significación (“test”) que produce un resultado final de tipo 0 (se acepta la hipótesis nula) o 1 (se rechaza la hipótesis nula, y

• otro estadístico que se evalúa sobre la muestra finalmente obtenida de valores del estadístico anterior –p.e. la frecuencia relativa de 1, para estimar la potencia o el nivel de significación “real” del test anterior.

Nótese que esta es también la estructura de la fase de simulación del método bootstrap, con la salvedad de que el modelo probabilístico “completamente especificado” (en caso contrario no sería posible generar nada a partir de él) es la distribución empírica asociada a una muestra “real” –o más generalmente, un modelo probabilístico estimado a partir de la muestra real.

( ) ( )( ) ( )

( ) ( )

1 11 1 1 1

2 21 2 2 2

1

, ,

, ,

, ,

n

n

m m mn m m

F

x x t t

x x t t

x x t t

= =

= =

= =

↓x

x

x

xx

x

( )2p.e. formadas por réplicas ,n N iidµ σ

Modelo probabilístico,

completamente especificado (gran) muestra

de m valores del

estadístico

Generación de nsim=m muestras

independientes (o no) según F Estadísticos “resumen”

( )

( )

2

1

1 ( ) var1

ˆ ; , etc.

m

j Fj

t t tm

G G F=

− ≅−

“Leyes de los grandes

números”

Figura 7. Esquema general de una simulación de Montecarlo en Estadística

La mayoría de simulaciones de Montecarlo en Estadística se ajustan a una estructura (¿patrón?) como la descrita anteriormente. La gran diversidad de posibilidades procede de las muchas combinaciones posibles en las clases de los objetos (distribución, estadístico, proceso de muestreo…) que intervienen.

Figura 8. Interfaz RandomSampler

Lógicamente, sería posible representar toda esta diversidad utilizando (abusando de) el mecanismo de la herencia, lo cual daría lugar a una jerarquía muy intrincada de clases. La discusión anterior sugiere que a este tipo de situaciones es aplicable uno de los patrones de diseño más utilizados y generalmente bien comprendidos, denominado habitualmente “objeto compuesto”. Un objeto que define una simulación es un compuesto de objetos de diversas clases. En gran medida, cada familia concreta de

experimentos de simulación viene definida por las clases de los objetos que constituyen este objeto compuesto, es decir, por la distribución concreta utilizada para generar las muestras, por el estadístico concreto calculado sobre cada muestra, etc. Una estructura de datos, un compuesto, que resulta muy flexible como elemento constructivo en este ámbito está reflejada en las figuras 8 y 9, todas ellas tomadas directamente del diseño de la librería de clases Montecarlo, citada anteriormente.

La Figura 8 es la definición de la interfaz RandomSampler. Toda clase que implemente esta interfaz, en esencia, debe ser un objeto compuesto del que dependa una función stat (un “objeto-función” de clase Statistic) y debe ser capaz de generar muestras (método sample) de tamaño prefijado o del tamaño (n) que se indique. El valor devuelto por sample siempre tiene la estructura (stat(x1), …, stat(xn)), resultado de evaluar stat sobre cada uno de los elementos de la muestra generada. Si stat es nulo se supone que es la identidad, el resultado de sample es directamente (x1, …, xn). La Figura 9 muestra dos clases importantes que implementan RandomSampler: IIDDistributionSampler y StatSimulator. Aunque no es un detalle importante, ambas especializan una clase, SamplerImplementation, que contiene algunas definiciones comunes y en concreto una referencia al estadístico, stat, que manejan.

La clase IIDDistributionSampler, tal como sugiere la firma de sus constructores, es capaz de producir muestras aleatorias simples a partir de la distribución de probabilidad que se especifique, con la función stat (que puede ser la identidad) evaluada sobre cada elemento de la muestra.

Figura 9. Clases que implementan RandomSampler

La misión de la clase StatSimulator es “muestrear otro muestreador”, utiliza las muestras generadas por otra clase que implemente RandomSampler para construir el resultado final de la evaluación de su método sample. La posibilidad de que un objeto compuesto pueda formar parte a su vez de otro compuesto, con total transparencia (en el sentido de que los objetos resultantes siempre obedecen a la misma interfaz) proporciona una gran flexibilidad, adecuada para representar un gran número de situaciones. Como muestra de la potencia de este concepto, considérese la siguiente situación: Statistic t = new OneSampleTTest (m0); Statistic fr = new RelativeFrequency(); ProbabilityDistribution distr = new Normal(m, s); int n = 10; int nsim = 10000; … RandomSampler simulator = new StatSimulator(

new IIDDistributionSampler( distr, n), t, nsim);

double p = fr.eval( simulator.sample()); …

Omitiendo bastantes detalles accesorios, supóngase que la primera línea del ejemplo anterior crea un objeto función t de clase Statistic que evalúa el test t de Student para una sola muestra, para contrastar la hipótesis nula de que la media poblacional es el valor contenido en m0 frente a la alternativa de que no es este valor. Un método de t, seguramente denominado eval por coherencia con los nombres usados hasta ahora, devuelve finalmente el valor 0 en caso de aceptar la hipótesis nula y el valor 1 en caso contrario. La segunda línea construye un estadístico, fr, que evalúa la frecuencia relativa de 1 a partir de un vector de valores 0 ó 1. La tercera línea construye un objeto que corresponde a una distribución normal con media y desviación estándar especificadas en

m y s. La cuarta y quinta líneas definen el tamaño muestral de las muestras sobre las que se evalúa t y el tamaño muestral del estudio de simulación, respectivamente.

Las dos últimas instrucciones pueden estar en un método separado de las primeras, que habrá recibido aquellos objetos como argumentos. La penúltima instrucción construye un simulador, un objeto adecuado para generar un vector de 10000 valores 0 ó 1, cada uno de ellos obtenido a partir de una muestra de tamaño 10 sobre la que se ha evaluado el test t. La última instrucción realiza realmente la simulación (instrucción simulator.sample( )) y evalúa la potencia estimada del test. En efecto:

El objeto creado en la instrucción new IIDDistributionSampler( distr, n)

será capaz de producir muestras iid de tamaño n a partir de la distribución distr, que en este caso concreto es una normal. Puesto que no se ha especificado ninguna función al construirlo, su método sample producirá directamente valores de la forma x = (x1, …, xn), vectores de tamaño n generados a partir de una distribución normal. Puesto que el objeto anterior ha sido pasado como primer argumento para la construcción del simulador, new StatSimulator( new IIDDistributionSampler( distr, n), t, nsim);

este último (el simulador) generará valores en su propio método sample a partir del resultado de sample del objeto que ha recibido, sobre los que habrá evaluado el estadístico t. Por lo tanto, el resultado de simulator.sample()

tendrá la forma (t(x1), …, t(xnsim)).

Supóngase ahora que se desea realizar un estudio de simulación sobre la robustez del test anterior, por lo que está previsto generar las muestras según diversas distribuciones, alternativas a la normal. Toda la estructura anterior sería válida, con la única diferencia de que el objeto distr tendría que ser de otra clase, por ejemplo ProbabilityDistribution distr = new Exponential( rate);

para que las muestras generadas correspondiesen, en realidad, a una distribución exponencial.

5.2. Ampliar las atribuciones de una clase: el patrón Decorador La estructura descrita en la subsección anterior es un ejemplo, en verdad algo complejo, de algo que se podría adaptar a un patrón de diseño cuyo objetivo principal es aumentar las atribuciones de una clase. Se trata del patrón denominado comúnmente “Decorador” (Decorator) y en ocasiones “Envoltura” (Wrapper). En la sección 4 también se plantearon vías para aumentar la interfaz, y por lo tanto las atribuciones, de una clase. El tipo de incremento que planteamos en esta sección es más general, puede referirse tanto a la ampliación de su interfaz (perfectamente posible con Decorador) como a un incremento (o simplemente modificación) de sus capacidades internas de proceso, sin alterar la interfaz. La Figura 10 muestra la estructura general del patrón Decorador.

Supóngase que en determinada aplicación son importantes clases que implementan determinada interfaz, que hemos denominado Decorable. DecorableClass1 y DecorableClass2 son ejemplos concretos de clases que implementan esta interfaz. La clase Decorator contiene un atributo interno, decoratedObject, que es una referencia a un objeto de cualquier clase que implemente Decorable (por ejemplo una referencia a

Figura 10. Estructura general del patrón de diseño Decorador

un objeto de clase DecorableClass2 sería perfectamente admisible como valor de este atributo) e implementaciones de los métodos de la interfaz Decorable que se limitan a activar los mismos métodos del objeto especificado por decoratedObject. Por lo tanto, un objeto de clase Decorator, de existir, se comportaría exactamente igual que el objeto referido en decoratedObject. Las clases FirstDecorator y SecondDecorator descienden de Decorator. La primera de ellas redefine el método method2, añadiéndole cierta capacidad de proceso adicional. La segunda redefine method1 y añade un nuevo método a su interfaz, additionalMethod. Nótese que, al construir objetos de estas clases, el constructor debe recibir una referencia a un “objeto decorado”, que pasará a ser el valor del atributo decoratedObject. Un objeto creado como Decorable d = new FirstDecorator( new DecorableClass2( ));

se comportaría exactamente igual que un objeto de clase DecorableClass2, excepto en su método method2, que realizaría cierto procesamiento adicional, previo al realizado por method2 en la clase DecorableClass2. De la misma manera, un objeto creado como Decorable d = new SecondDecorator( new DecorableClass1( ));

respondería a la misma interfaz que un objeto de clase DecorableClass1 excepto en su método method1, además de disponer de un método adicional en su interfaz –en este caso una vez realizado el “molde de tipo” (typecasting) adecuado, ya que Decorable no incluye el método additionalMethod: (SecondDecorator)d.additionalMethod( );

Nótese la potencia de este mecanismo, que permite creaciones de objetos como: new SecondDecorator( new FirstDecorator( new DecorableClass2( )));

para dotar de nuevas características o ampliar la interfaz, de forma transparente y sin necesidad de recurrir a la creación de complicadas jerarquías que añadan todas estas funcionalidades mediante el mecanismo de la herencia5.

Figura 11. Implementación de cálculos en servidores remotos y técnicas de reducción de lavarianza como decoradores

La Figura 11 ilustra la posible utilidad del patrón Decorador en simulación estadística. Suponiendo que se ha creado una jerarquía de clases del estilo de la descrita en la subsección anterior, es posible concebir numerosas variantes sobre la misma, que podrían ser fácilmente implementadas mediante este patrón.

Una posibilidad la sugiere la necesidad de realizar simulaciones muy intensivas computacionalmente, que impliquen una gran carga de cálculo. Es posible que, en este caso, se desee delegar gran parte de la computación necesaria en un ordenador remoto, se supone que muy potente. El decorador Remote haría los preparativos necesarios para que sucesivas llamadas a los métodos del objeto decorado fuesen redirigidas a un objeto remoto, posiblemente una copia exacta de éste (del objeto decorado), mediante algún mecanismo de invocación remota, como RMI en Java o la especificación CORBA, más general.

Otra posibilidad la sugieren algunos métodos de reducción de la varianza de aplicación casi automática, como el método de las variables antitéticas. Recuérdese que si en una

5 Curiosamente, el sistema de entrada/salida de Java hace un uso intensivo del patrón Decorador, precisamente ¡para prevenir una explosión de clases dedicadas a la gestión de la entrada/salida!

simulación se desea estimar un parámetro interpretable como la esperanza de un estadístico t, E(t), a partir de una muestra de valores simulados del estadístico, (t1, t2, …, tnsim), un esquema de muestreo que produzca observaciones iid del estadístico no es necesariamente la mejor opción. En el caso iid la varianza de la media muestral de las observaciones simuladas es, como es bien sabido,

( ) ( )varvar .

tt

nsim=

En cambio, si las observaciones obtenidas por simulación están correlacionadas (pero siempre respetando la distribución marginal del estadístico), la varianza será:

( ) ( ) ( ),

varvar cov ,i j

i j

tt t

nsim= + ∑ t

donde el sumatorio se extiende a aquellos pares de observaciones ij que tengan correlación no nula. Si este sumatorio es negativo, la varianza de la estimación será menor que la correspondiente al caso de observaciones incorrelacionadas. Una posibilidad fácil de implementar es hacer que las observaciones estén organizadas en pares consecutivos independientes pero con correlación negativa dentro de cada par:

( ) ( ) ( )( )( )1 2 3 4 1

2 1 2

, ; , ; ; ,

, 0, 1, ,nsim nsim

i i

t t t t t t

t t i nsimρ−

− < =

… 2

suponiendo, lógicamente, que nsim es par. Dada una función de distribución univariante G, la distribución bivariante con marginales G con la correlación negativa más extrema posible (la cota de Frechet H−) es la asociada al vector aleatorio (G−1(U), G−1(1−U)), donde U es una variable aleatoria con distribución uniforme sobre (0,1).

Normalmente no se generará directamente t2i-1 = G−1(U2i-1), t2i = G−1(U2i) ya que la obtención de los valores del estadístico requerirá la generación previa de una muestra de tamaño n de cierta variable aleatoria, t = t(x1, …, xn). Pero si el estadístico es una función (aproximadamente) monótona de las observaciones de la muestra y los elementos de la muestra se pueden generar por inversión y manteniendo la sincronización entre las secuencias de valores uniformes:

( ) ( ) ( )( ) ( ) (

1 1 11 1 2 2

1 1 11 1 2 2

muestra 2i 1: , , ,

muestra 2i : 1 , 1 , , 1n n

n n

x F U x F U x F U

)x F U x F U x F U

− − −

− − −

− = = =

= − = − = −

es de esperar que los pares de valores del estadístico estén negativamente correlacionados.

En resumen, una implementación sencilla de la técnica de las variables antitéticas (que no siempre funcionaría, dependería de la forma concreta del estadístico y del algoritmo utilizado para generar las variables aleatorias), la proporcionaría una clase decoradora, Antithetic en el ejemplo, que, antes de generar la muestra mediante el objeto RandomSampler correspondiente, alternase entre dos posibles secuencias de números aleatorios, una complementaria de la otra. Esto se podría conseguir utilizando y alternando entre dos objetos de una clase asimilable a Random de Java tales que produjesen secuencias de números pseudoaleatorios complementarias. La simulación sobre la potencia de un test, descrita anteriormente, utilizaría variables antitéticas si el simulador se construyese de la manera siguiente: RandomSampler simulator = new StatSimulator(

new Antithetic( new IIDDistributionSampler( distr, n)),t, nsim);

6. CONSIDERACIONES FINALES Todo lo expuesto en las secciones anteriores pretendía demostrar que la utilización de un enfoque orientado a objetos en computación estadística (y en general) es una mejora significativa sobre otros enfoques, pero no necesariamente una garantía de calidad de los programas producidos. La discusión de los inconvenientes de algunos diseños ante determinados problemas (conseguir flexibilidad en la elección del algoritmo para solucionar determinado problema o realizar determinado proceso, ampliar las operaciones posibles sobre una jerarquía de clases estadísticas dada y diseñar programas de simulación estadística) ilustra las limitaciones del enfoque orientado a objetos. Por otra parte, sin proporcionar en general soluciones mágicas, determinados patrones de diseño proporcionan soluciones aprovechables en el ámbito de la computación estadística.

Los patrones considerados en este trabajo, y muchos otros que podrían ser útiles en el desarrollo de programas estadísticos, proporcionan soluciones elegantes a determinados problemas y hacen que, en general, el software desarrollado sea más fácil de mantener y de extender. Como contrapartida, normalmente implican algún nivel adicional de direccionamiento, que en casos concretos puede implicar una cierta pérdida de eficiencia.

Se usen o no patrones, nuestra conclusión final es que el tiempo dedicado a realizar un cierto análisis y diseño previo, tratando de formalizar (en cierta forma) los conceptos a plasmar en forma de programas mediante una notación como UML, no es un tiempo perdido, siempre que no se caiga en una “análisis-parálisis”, el extremo contrario.

REFERENCIAS Ahrens, J.H., Dieter, U. Efficient table-free sampling methods for the exponential, Cauchy and normal distributions. Computing, Commun. ACM, 31, 1330-1337, 1988.

Box, G.E.P., Muller, M.E. A note on the generation of random normal deviates. Ann. Math. Statist. 29, 610-611, 1958.

Boyland, J., Castagna, G. Parasitic methods: An implementation of multi-methods for Java. OOPSLA ’97, Proceedings, Atlanta. SIGPLAN Notices, 32, 66-76, 1997.

Chambers, J.M. Programming with Data. A Guide to the S Language. Springer, 1998.

Chambers, J.M. Users, programmers, and statistical software. Journal of Computational and Graphical Statistics, 9, 403-422, 2000.

Eckel, B. Thinking in Java. Prentice Hall, 2002.

Eckel, B. Thinking in Patterns with Java. Libro en preparación, la versión preliminar es accesible por Internet: http://www.mindview.net/Books/TIPatterns/.

Gamma, E., Helm, R., Johnson, R., Vlissides, J. Dessign Patterns: Elements of Reusable Object-Oriented Software. Addisson-Wesley, 1994.

Garrido, J.M. Object-Oriented Discrete-Event Simulation with Java. A Practical Introduction. Kluwer, 2001.

Hallman, B. Are classes necessary? The Journal of Object-Oriented Programming, 10, 16-21, 1997.

Henney, K. Something for nothing. Java Report, 4, 74-80, 1999.

Hitz, M., Hudec, M. Applying the Object Oriented paradigm to statistical computing. Proceedings in Computational Statistics, COMPSTAT 94, 389-394, 1994.

OMG. Unified Modeling Language Specification. Version 1.3, June 1999

Saar, R. Extensions of software components using multimethods. The Journal of Object-Oriented Programming, 13, 12-16, 2000.

Stevens, P., Pooley, R. Using UML: Software Engineering with Objects and Components. Addison-Wesley, 2000.