Diseño orientado a objetos y software estadístico

Anuncio
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 artículo se analizan algunos aspectos del diseño de programas estadísticos. La
idea central es que bastantes de los “patrones de diseño” comúnmente empleados en
otras áreas del desarrollo informático, son aplicables en la creación de programas
estadísticos, y podrían contribuir a mejorar algunas de sus características, 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”, “Objeto función” y
“Visitante”, junto con el concepto de multimétodo, y se apunta la posible utilidad de
otros patrones. La exposición se basa en parte en la utilización de diagramas UML, y en
parte en ilustraciones de código (similar a) Java.
Palabras clave: Generación de variables aleatorias, Framework, Computación estadística,
Estrategia, Factoría, Multimétodo, Visitante, Objeto función
ABSTRACT
In this paper, we discuss some questions with respect to the design of statistical
software. The central point is that many “design patterns” are relevant in the
development of statistical software, and may contribute to improve some of its
characteristics, like the readability, the maintainability and the extensibility. More
precisely, we describe and discuss to some extent the “Strategy”, “Function object” and
“Visitor” patterns, jointly with the concept of “multimethod”. We also suggest the
possible applicability of other patterns. We illustrate the concepts using UML diagrams
and also some Java (alike) code.
Keywords: Random variate generation, Framework, Statistical computing, Strategy, Factory,
Multimethod, Visitor, Function object
1. INTRODUCCIÓN
Hoy en día, la Estadística aplicada no se concibe sin la Informática, y no solamente en
el caso evidente del usuario de una técnica estadística. También en el desarrollo de
nuevos métodos estadísticos surge la necesidad de plasmarlos en forma de programas, o
de realizar estudios de simulación. En parte, un metodólogo estadístico es también un
desarrollador informático. Alguien que repite tareas elementales con un programa puede
querer automatizarlas, un primer paso hacia la programación. Puede surgir la necesidad
de “ampliar” un método ya programado, como añadir un nuevo índice a los
inicialmente previstos para determinado método de clasificación jerárquica. Chambers
(2000) propone cinco condiciones deseables en todo entorno informático 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 consecuencia casi obligada de los puntos anteriores es la necesidad de un entorno
de trabajo “orientado a objetos”. Pero este paradigma informático no proporciona una
garantía segura de que el material desarrollado vaya a ser “extensible” o de “calidad”.
Frecuentemente, los programas desarrollados por los estadísticos, aún utilizando
lenguajes orientados a objetos como S o Java, reflejan una de dos estrategias de
desarrollo típicas: o bien prescindir de la orientación a objetos, o bien abusar de las
características más llamativas de la misma, en especial de la herencia. En el primer
caso, el resultado es una librería de funciones como la que hace unas décadas se habría
desarrollado, por ejemplo, en FORTRAN –salvo que las funciones están agrupadas en
unos contenedores llamados “clases”, que más bien molestan. En el segundo caso todo
tiene que acabar encajando en grandes y complejas clases, derivadas por herencia de
otras complejas clases generales. En este trabajo discutiremos aspectos generales del
diseño de programas estadísticos, obviando consideraciones sobre herramientas
concretas y sobre eficiencia, para ver que algunos “patrones de diseño” (Gamma y col.,
1994, Losilla, 2003, y también http://www.mindview.net/Books/TIPatterns/) son de
directa utilidad en la correcta realización de esta tarea.
2. ¿QUÉ ALGORITMO UTILIZAMOS? EL PATRÓN “ESTRATEGIA”
La elección del algoritmo más adecuado para resolver determinado problema es crucial
en Estadística. En ocasiones, conceptualmente la solución obtenida es la misma al
margen del algoritmo concreto, cuyas diferencias repercuten en la velocidad, o en la
estabilidad numérica. Para una distribución dada podemos disponer de varios
algoritmos exactos de generación aleatoria, pero teóricamente las propiedades
estadísticas de los valores generados son las mismas. Otras veces la solución no es la
misma conceptualmente, si bien es una de las aceptables. Tal es el caso de la elección
del método de estimación para ajustar los parámetros de un modelo. Lo más común es
que no exista “el mejor” algoritmo. Un algoritmo de generación aleatoria puede ser
rápido sólo para determinado rango de parámetros, o siempre más veloz pero
inadecuado para utilizarlo junto con determinada técnica de reducción de varianza.
Considérese el diseño de un entorno de generación variables aleatorias. Será necesario
decidir qué métodos de generación programar y en general cómo organizar todos los
conceptos implicados, como distribuciones o métodos de generación. En el lenguaje S (o
R), y en la mayoría de los paquetes estadísticos, existe una lista fija de distribuciones.
Mediante llamada a una función o un menú se pueden generar valores según la
distribución elegida, sin control del método de generación empleado ni información al
respecto. Así son fáciles las cosas básicas, no una utilización más avanzada.
Una librería de clases parece una opción mejor. Lo más común es organizar todos los
conceptos en una jerarquía de clases que represente las distribuciones de probabilidad.
Supongamos que la raíz de dicha jerarquía es una clase general, ProbabilityDistribution.
De ella descenderán clases como UnivariateDistribution, que se especializará en clases
como UnivariateDiscrete o UnivariateAbsolutelyContinuous, de las cuales descenderán
clases más concretas como Poisson o Normal. Las clases más generales serán abstractas.
No se instanciarán nunca (no se crearán objetos de aquella clase) pero permitirán
definir partes comunes de la interfaz, y posiblemente de la implementación. Aparte de
otros métodos o funciones miembro1 (como cdf, para la función de distribución o pdf
para la densidad) todas las clases contarán con una función como nextRand, encargada
de generar valores aleatorios.
Para un objeto de clase Normal está claro el significado de una llamada a cdf o a
nextRand, que tendrá un efecto distinto a la llamada a las mismas funciones para un
objeto de clase Binomial, etc. La implementación de los posibles algoritmos de
generación para una misma distribución se puede llevar a cabo especializando por
herencia otras clases. Por ejemplo, para implementar el algoritmo de generación de
1
A partir de ahora, al describir clases procuraremos emplear el término “función miembro” que,
aunque tal vez menos general, no inducirá a confusión con la palabra “método”, que se reservará
para designar conceptos más de carácter estadístico. De todas maneras, sí que se hablará de
“multimétodo”, “sobrecarga de métodos”, etc. puesto que estos términos tienen un sentido muy
preciso.
Ahrens y Dieter(1988), aplicable a la distribución normal, una posibilidad es crear una
nueva clase, NormalAhrensDieter1988, especializando una clase general Normal,
sobrescribiendo su método nextRand –pero aprovechando (heredando) las otras partes
de la misma. Posiblemente Normal implementará un método sencillo, como el de Box y
Muller(1958), o simplemente será abstracta y sin ninguna implementación de nextRand.
El enfoque anterior deriva en una jerarquía de clases intrincada, con árboles de gran
profundidad, difícil de entender y de mantener. La Figura 1 trata de reflejar este hecho,
empleando notación UML (véase Jiménez y col., 2003).
Alternativamente, en cada clase distribución se pueden definir funciones miembro
distintas para cada algoritmo de generación. Por ejemplo, Normal tendrá las funciones
boxMullerNextRand y ahrensDieter1988NextRand. Esto conduce a clases con interfaces
complicadas 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. En última
instancia, se tiene una librería de funciones, agrupadas en clases relativamente inútiles.
La discusión anterior refleja un problema de diseño común en Estadística y en otras
áreas. Una solución general la proporciona el patrón de diseño denominado “Estrategia”
(Strategy en Gamma y col., 1994). En él, 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 ser capaces de
responder al mensaje “ejecuta el algoritmo”, implementado en una función miembro
adecuada. Las clases que utilizarán estos algoritmos deben definir un atributo que sea
una referencia al objeto algoritmo activo en un momento dado. Para realizar la
operación que depende del algoritmo, delegarán esta tarea en el objeto algoritmo.
En la Figura 2 se ilustra el patrón Estrategia adaptado a la situación planteada
anteriormente, la elección dinámica del algoritmo de generación. Cuando un objeto
distribución precisa generar valores aleatorios, delega esta función en el objeto
algoritmo que tiene asociado en aquel momento. Otro claro ejemplo del patrón
Estrategia se halla en Hitz y Hudec (1994) para la estimación del elipsoide de volumen
mínimo en la determinación de la localización y la forma multivariante.
La Figura 2 ilustra 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 en la Figura 2) a los objetos que implementan algoritmos de
generación, y tenga definido la función miembro, 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. De la misma manera que las
distribuciones tienen una referencia al algoritmo de generación empleado, los algoritmos
de generación tienen 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 más específica, para
controlar posibles errores de empleo de algoritmos de generación inadecuados para
determinada distribución. 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 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 en la mayoría de los patrones) es que implica un nivel de
direccionamiento adicional, que añade una cierta carga de computación.
Es común que al mismo problema o situación sea aplicable más de un patrón de diseño,
bien por que son posibles soluciones alternativas, bien por que intervienen en partes
distintas de la solución. En este sentido los objetos distribución y los objetos algoritmo
de generación del ejemplo deben estar siempre sincronizados. Si a un objeto que
representa una distribución se le modifican los valores de los parámetros, este cambio
también debe afectar al objeto algoritmo de generación –que posiblemente almacena
internamente valores que será preciso actualizar en función de los parámetros de la
distribución. Una solución completa a esta necesidad la proporciona el patrón
“Observador” (Observer), descrito y utilizado en Jiménez y col.(2003). En realidad,
dado que en la relación entre una distribución y el algoritmo de generación asociado
solamente intervienen dos objetos, basta con que estén fuertemente vinculados:
solamente se accederá al algoritmo de generación desde el objeto distribución.
Esta
restricción incluye la creación flexible (y destrucción) de objetos algoritmo, que no
pueden existir “libremente”, no asociados a una distribución. Esto último sugiere
alguna de las variantes del patrón Factoría (Factory), que independiza la creación de
objetos de la necesidad de referencias explícitas a su clase concreta. El package Java
Montecarlo, cuya versión preliminar se puede obtener del primer autor, implementa
la “Factoría polimórfica” descrita en http://www.mindview.net/Books/TIPatterns/.
3. ¿TODO EN LA INTERFAZ? OBJETO FUNCIÓN Y MULTIMÉTODOS
Un error de diseño común es que las clases incluyan funciones miembro para manejar
todos los conceptos de la correspondiente área de aplicación. Por ejemplo, en una
librería de clases de distribuciones de probabilidad, que las superclases iniciales tengan
previsto el cálculo de la función de densidad, de la función de distribución, la media, la
varianza, y en general todo lo que se le pueda ocurrir al desarrollador o a un posible
usuario. De esta forma todos los objetos responderán a una misma interfaz, muy
general y amplia. Este diseño contradice el principio de máxima modularidad y de
concisión: toda clase debería tener unas atribuciones muy concretas, asociadas a una
interfaz lo más concisa posible. En cualquier caso, la implementación de tal librería
puede ser una tarea ardua y casi seguro que siempre inacabada.
Un enfoque alternativo parte de unas superclases muy escuetas y va ampliando su
interfaz mediante el uso de la herencia. El resultado final son interfaces muy amplias
pero no comunes a todas las clases, lo cual no facilita la “programación genérica”,
asunto del que trataremos con más detalle en los párrafos que siguen. Ahora no es
posible referirse, en determinados lugares de un programa, 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, que incluso pueden no existir en el momento de la programación del código
“genérico”, el que hacía referencia a clases generales, como UnivariateDistribution.
Ninguno de los enfoques anteriores permite una extensión fácil. La Figura 3 trata de
reflejar la multiplicación de clases asociada a la inclusión de una función miembro como
interQuartilicRange, omitida en la jerarquía de clases original.
Una solución alternativa se basa en crear, independientemente de la jerarquía de clases
inicial, nuevas clases que representen las nuevas funciones a incorporar. Esta es la idea
básica
del
patrón
“Objeto
función”
o
“Comando”.
La
clase
denominada
InterquartilicRange ilustra esta idea. Dispone de una función miembro eval con un
argumento de una clase general como UnivariateDistribution. En Java se definiría:
public class InterquartilicRange {
public double eval (UnivariateDistribution distri) {
// cálculos para determinar el recorrido
// intercuartílico
}
}.
Donde eval sería suficientemente general como para evaluar el recorrido intercuartílico
para todas las distribuciones univariantes. Pero para la mayoría de las distribuciones
específicas el mismo cálculo podría ser mucho más eficiente y simple. Parecería muy
razonable ampliar la interfaz de la clase InterquartilicRange añadiendo funciones para
manejar
el
caso
de
distribuciones
más
específicas.
Seguramente
la
clase
InterquartilicRange tendría que descender de una clase general, como StatFunction, con
la función de “firma” public double eval (UnivariateDistribution distri) 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);
La última línea podría corresponder al cálculo del valor de un parámetro cualquiera,
para cualquier distribución. Sintácticamente, lo anterior sería correcto. Pero no haría lo
que se podría pensar en principio. Si la auténtica clase de operator fuese
InterquartilicRange y la de distr, Normal, se podría pensar que la tercera línea del
ejemplo ejecutaría la función miembro, más eficiente y adecuada en este caso, de
“firma” public double eval (Normal distri). La realidad es que en la mayoría de los
lenguajes ciertamente se ejecutaría eval de InterquartilicRange, pero en su versión más
general, no la propia de la distribución normal. Esto se debe a que la clase concreta
para la que se ejecuta una función miembro virtual (en Java todas las funciones
miembro son virtuales) se decide en tiempo de ejecución. Por lo tanto, correctamente,
se buscan las disponibles en la clase InterquartilicRange. Pero en la mayoría de
lenguajes, como C++ y Java, la sobrecarga de métodos se resuelve en el momento de la
compilación, no durante la ejecución del programa. Cuando está analizando las tres
líneas de código anteriores, el compilador desconoce la clase concreta del objeto, distr,
que
eval
recibirá
como
parámetro,
lo
único
seguro
es
que
será
de
clase
UnivariateDistribution o de alguna de sus descendientes, desconocida. La función
miembro public double eval (UnivariateDistribution distri) de StatFunction es correcta
sintácticamente y es la que se adapta mejor a la información que tiene el compilador.
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 Saar, 2000). La mayoría de los lenguajes orientados a objetos (incluyendo
Smalltalk, Java y C++) implementan un “mecanismo de resolución” simple. Los
métodos o funciones miembro 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 los
activa –la clase real, no necesariamente la declarada en el 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” (multiple dispatch): se decide qué método ejecutar
en función no solamente de la clase del objeto que activa la función miembro sino
también de la clase real de los restantes argumentos –o de parte de ellos.
Este útil concepto ha sido incorporado (Chambers, 1998) en el lenguaje S, a partir de
su versión 4. En un lenguaje como Java, mediante el mecanismo de reflexión, o por
otras vías, es posible simular el concepto de método múltiple (Boyland y Castagna,
1997). Si solamente es necesario implementar “resolución doble” (funciones miembro
que se ejecutan en función del objeto que los activa y de uno solo de sus parámetros), el
patrón de diseño “Visitante” (Visitor) proporciona una solución limitada pero sencilla.
La Figura 4 trata de esquematizar este patrón de diseño para el caso de la jerarquía de
distribuciones y la de funciones objeto que realizan cálculos específicos sobre ella. En
Visitante, la necesidad de resolución doble se implementa mediante una doble llamada
a funciones de resolución simple, la única disponible. Todas las clases distribución
deben implementar una función, denominada evaluate en la Figura 4, que haga que los
correspondientes objetos “acepten” ser visitados por objetos de clases que implementen
la interfaz StatFunction. Estas últimas clases son las que realmente realizan los cálculos
adicionales, en el ejemplo mediante eval. El código de evaluate siempre sería algo como:
return func.eval (this);
La llamada a eval se realiza dentro de la función evaluate, definida en la clase
distribución “visitada”. En este lugar está perfectamente claro para el compilador cuál
es la clase del objeto distribución que activa evaluate, que es el mismo objeto que recibe
eval como parámetro -la referencia this en Java. Por lo tanto, el mecanismo de
sobrecarga de métodos escoge la versión de eval más específica, durante la compilación.
Como consecuencia de la aplicación del patrón Visitante, con la incorporación de un
único método, evaluate, en todas las clases distribución, la interfaz de todas ellas se
puede ampliar con un número ilimitado de nuevas funciones, funciones encapsuladas en
clases que implementen una interfaz dada, StatFunction en el ejemplo anterior.
El punto más débil de este patrón es el hecho de que la interfaz “visitante”,
StatFunction, debe incluir funciones miembro para todas las clases de la jerarquía de
distribuciones que tengan que ser “visitables” directamente, es decir, para las cuales se
vaya a definir una función miembro evaluate específica. Si se amplía la jerarquía de
distribuciones y se desea crear funciones miembro eval cuya firma incluya las nuevas
clases, será necesario ampliar y recompilar la interfaz StatFunction y las clases que la
implementan. Ésto no es tan grave si la jerarquía “visitable”, las distribuciones, es
mucho más estable que la “visitante”, los cálculos sobre distribuciones. Esta última
estará en mayor medida bajo control del usuario, como vía de ampliación de la primera.
La Figura 4 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 generales, UnivariateDiscrete,
UnivariateAbsolutelyContinuous y UnivariateDistribution. La jerarquía con raíz en la
clase UnivariateDistribution se amplía con una nueva función estadística, representada
por la clase InterquartilicRange, que implementa la interfaz StatFunction.
Ahora, si la auténtica clase del objeto distr es Normal y la de operator es
InterquartilicRange, la última línea de
StatFunction operator;
UnivariateDistribution distr;
double valor = operator.eval(distr);
se traducirá en una llamada de la función eval(Normal …) de InterquartilicRange.
UnivariateDiscrete, UnivariateAbsolutelyContinuous y UnivariateDistribution constan
en la interfaz StatFunction ya que, posiblemente, para ellas se crearán versiones
generales de eval, adecuadas para el caso discreto en general, para el absolutamente
continuo, etc., aplicables a nuevas clases derivadas que no hayan sido explícitamente
incluidas en el patrón. Por ejemplo, supóngase que se ha creado una nueva clase,
NegativeBinomial, descendiente de UnivariateDiscrete. Para ella no se crea una eval
específica en la interfaz StatFunction y en la clase InterquartilicRange y similares. Si
distr es un objeto de clase NegativeBinomial y operator sigue siendo un objeto de clase
InterquartilicRange, en la última línea del código anterior el cálculo correspondiente
será finalmente realizado por la función miembro eval(UnivariateDiscrete …) de la clase
InterquartilicRange, tal como ocurriría también en el caso de la extensión por herencia.
4. CONCLUSIONES
La orientación a objetos en computación estadística es deseable, pero no necesariamente
una garantía de calidad del código producido. Sin ser soluciones mágicas, determinados
patrones de diseño proporcionan soluciones aprovechables en el ámbito de la
computación estadística. Los considerados en este trabajo, y posiblemente otros, pueden
hacer que el software estadístico sea más fácil de mantener y de extender. Como
contrapartida pueden implicar cierta pérdida de eficiencia.
Se usen o no patrones, la realización de un cierto análisis y diseño previo, con la ayuda
de herramientas como la notación UML, no es una perdida de tiempo, siempre que no
se incurra en una “parálisis del análisis”. Lo anterior corresponde al extremo contrario
de la costumbre, tan extendida en Estadística y áreas afines, de realizar el análisis y el
diseño en segundo plano (mental) mientras se teclea el programa.
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.
Gamma, E., Helm, R., Johnson, R., Vlissides, J. Dessign Patterns: Elements of
Reusable Object-Oriented Software. Addisson-Wesley, 1994.
Hitz, M., Hudec, M. Applying the Object Oriented paradigm to statistical computing.
Proceedings in Computational Statistics, COMPSTAT 94, 389-394, 1994.
Jiménez, R, Losilla, J.M., Vives, J. Modelado orientado a objetos de variables de
recuento. Trabajo Presentado al VIII Congreso de Metodología de las Ciencias Sociales
y de la Salud. Valencia. Programa y Libro de Resúmenes, 58-59, 2003.
Losilla, J.M. El proceso unificado de desarrollo de software estadístico. Trabajo
Presentado al VIII Congreso de Metodología de las Ciencias Sociales y de la
Salud.Valencia. Programa y Libro de Resúmenes, 55-56, 2003.
Saar, R. Extensions of software components using multimethods. The Journal of
Object-Oriented Programming, 13, 12-16, 2000.
Figura 1. Elección del método de generación basada, exclusivamente, en la herencia
Figura 2. El patrón Estrategia en la generación de variables aleatorias
Figura 3. Multiplicación de clases al extender por herencia
Figura 4. El patrón Visitante en una jerarquía de distribuciones de probabilidad
Descargar