Clase 20. Estrategia de diseño La presente clase reúne varias de las ideas ya vistas en clases anteriores: modelos de objeto de problemas y código, diagramas de dependencia de módulos y patrones de diseño. El objetivo que en ella se persigue es ofrecer algunos consejos de carácter general sobre cómo enfrentarse al proceso de diseño de software.
20.1 Descripción general del proceso y pruebas Los principales pasos del proceso de desarrollo son los siguientes: • Análisis del problema: da como resultado un modelo de objeto y una lista de operaciones. • Diseño: da como resultado un modelo de objeto de código, un diagrama de dependencia de módulos y especificaciones de módulos. • Implementación: da como resultado un código ejecutable. Lo ideal sería que las pruebas se llevaran a cabo a medida que se realiza el proceso de desarrollo, de modo que los errores aparezcan lo antes posible. En un conocido estudio sobre proyectos desarrollados por TRW e IBM, Barry Boehm llegó a la conclusión de que el coste de corregir un error puede llegar a multiplicarse hasta por 1.000 cuando se detecta tardíamente. Hemos empleado el término "pruebas" exclusivamente para describir la evaluación de códigos, pero se pueden aplicar técnicas parecidas a descripciones de problemas y diseños cuando se han registrado en una notación que tenga asociada una semántica. (En mi grupo de trabajo hemos desarrollado una técnica de análisis para modelos de objetos). En este curso, el estudiante deberá basar su trabajo en la meticulosidad de las revisiones y en el uso de escenarios manuales para evaluar las descripciones y los diseños del problema. Por lo que a probar las implementaciones se refiere, el estudiante debe marcarse como objetivo que las pruebas se realicen tan pronto como sea posible. La programación extrema (XP), una metodología de desarrollo muy extendida en la actualidad, aboga por escribir las pruebas antes incluso de haber escrito el código al que se van a aplicar éstas. Se trata de una idea muy interesante, en primer lugar porque significa que es menos probable que una selección de pruebas quede expuesta a sufrir los mismos errores conceptuales que esas pruebas tratan precisamente de detectar. Además, estimula al usuario a pensar en las especificaciones por adelantado. Es un enfoque ambicioso, aunque no siempre factible. En vez de probar el código de una manera ad hoc, es más conveniente crear una base sistemática de pruebas que no requieran la interacción del usuario para su ejecución y validación. Este sistema reporta numerosos beneficios: por ejemplo, permite, cuando se introducen cambios en un código, detectar rápidamente los nuevos errores que se han producido al volver a ejecutar estas "pruebas de regresión". Es aconsejable hacer un uso liberal de las certificaciones en tiempo de ejecución y comprobar las constantes de representación.
20.2 Análisis del problema El resultado principal del análisis de un problema es un modelo de objeto que describe las entidades fundamentales del mismo y sus relaciones con otro problema. (El libro de texto del curso utiliza el término "modelo de datos" para referirse a esto). Conviene escribir descripciones breves para cada uno de los conjuntos y cada una de las relaciones del modelo de objeto, explicando lo que significan. Aunque nos parezcan evidentes en el momento de escribirlas, es fácil olvidar más tarde el significado de algún término. Además, muchas veces una descripción que nos parecía clara resulta no serlo tanto cuando la vemos por escrito. Por ejemplo, en mi grupo de trabajo estamos diseñando un nuevo componente de control de tráfico aéreo, y hemos descubierto que en nuestro modelo de objeto el término Flight resulta bastante confuso y que es importante describirlo con claridad. También resulta útil escribir una lista de las operaciones primarias que el sistema proporciona. Con ello se aprende a controlar la funcionalidad global y se puede comprobar que el modelo de objeto es capaz de soportar las operaciones. Por ejemplo, un programa pensado para llevar un seguimiento de precios de valores en Bolsa puede incluir operaciones para crear y suprimir carteras, añadir acciones a carteras, actualizar el precio de un valor, etc. 20.3 Propiedades del diseño La fase de diseño produce como resultado principal un modelo de objeto de código que muestra la forma en la que se implementa el estado del sistema, y un diagrama de dependencia de módulos que representa la división del sistema en módulos y el modo en que éstos se relacionan entre sí. En el caso de módulos que presenten complicaciones, resulta también conveniente disponer de un esquema con las especificaciones del módulo antes de comenzar la codificación. ¿En qué se basa un buen diseño? Obviamente, no hay un modo sencillo y objetivo de decidir si un diseño es mejor o peor que otro, pero sí que hay ciertas propiedades clave que permiten evaluar su calidad. Lo ideal sería un diseño que funcionara bien en todos los aspectos, pero en la práctica normalmente es necesario sacrificar algún aspecto a cambio de otro. Estas propiedades clave son: • Extensibilidad. El diseño debe ser capaz de soportar nuevas funciones. Aunque sea perfecto en todos los demás aspectos, un sistema que no muestre disposición a integrar el más ligero cambio o perfeccionamiento resulta inservible. Quizás no haya necesidad de añadir nuevas funciones, pero siempre es posible que se produzcan alteraciones en el dominio del problema que exijan introducir cambios en el programa. • Fiabilidad. El sistema debe tener un comportamiento fiable, lo que no significa solamente que no se bloquee ni corrompa los datos; debe además realizar todas sus funciones correctamente y de la forma prevista por el usuario. (Lo que, por cierto, quiere decir que tampoco basta con que el sistema satisfaga una especificación confusa; debe satisfacer una que el usuario comprenda fácilmente, de forma que
éste pueda predecir su comportamiento). La disponibilidad es una característica importante si el sistema es distribuido, mientras que en los sistemas en tiempo real tiene más importancia el tiempo: no tanto que el sistema sea rápido, sino que complete las tareas en el tiempo previsto. La forma de contemplar la fiabilidad varía mucho de un sistema a otro. Así, la falta de precisión a la hora de presentar imágenes es menos grave en un navegador que en un programa de edición electrónica. Los conmutadores telefónicos, por su parte, deben cumplir estándares de disponibilidad extraordinariamente altos, si bien ello ocasiona de vez en cuando errores de desvío de llamadas. Los pequeños retrasos quizás no importen mucho cuando se trata de un cliente de correo electrónico, pero son inaceptables en el caso del controlador de un reactor nuclear. Eficiencia. El consumo de recursos por parte del sistema debe ser racional, lo que una vez más depende del contexto. Una aplicación que se ejecuta en un teléfono móvil no puede asumir la misma disponibilidad de memoria que la que se ejecuta en un ordenador de consola. Los recursos más específicos son el tiempo y el espacio que consume el programa que se ejecuta. Pero no hay que olvidar que, como ha demostrado Microsoft, el tiempo empleado en el desarrollo del programa puede tener idéntica importancia, al igual que otro recurso que no se debe pasar por alto: el dinero. Un diseño que se implemente de modo económico puede ser preferible a otro que funcione mejor conforme a otros parámetros pero que resulte más caro.
•
20.4 Estrategia: visión general ¿Cómo se obtienen estas propiedades?
20.4.1 Extensibilidad •
•
Suficiencia del modelo de objeto. El modelo de objeto del problema debe ser capaz de describir éste de un modo suficientemente fiel. Uno de los obstáculos habituales a la hora de extender un sistema es la falta de espacio para añadir una nueva función, debido a que sus nociones no se hallan expresadas en el código. Microsoft Word nos muestra un ejemplo de este tipo de problemas. Word se diseñó asumiendo que los párrafos eran la noción clave de la estructura de los documentos, sin que se incluyera la noción de flujos de texto (los espacios físicos del documento a través de los que se hilvana el texto) ni ningún tipo de estructura jerárquica. A consecuencia de ello, Word no admite fácilmente la división en secciones y no es capaz de ubicar figuras. Es importante tener mucho cuidado de no optimizar el modelo de objeto del problema y eliminar subestructuras que no parezcan necesarias a primera vista. No se debe introducir una abstracción para sustituir nociones más concretas a menos que se esté totalmente seguro de que se halla bien fundada. Como se suele decir, toda generalizaciones suele conllevar errores. Localización y desacoplamiento. Aunque el código consiga reunir suficientes nociones que permitan la adición de nuevas funcionalidades, puede resultar complicado realizar el cambio que se desea introducir sin alterar el código en todo el sistema. Para evitar esto, el diseño debe proporcionar localización: cuestiones distintas deben estar separadas, en la medida de lo posible, en distintas regiones
del código. Asimismo, los módulos deben hallarse desacoplados todo lo posible unos de otros, de modo que un cambio no provoque alteraciones en cascada. Ya hemos visto ejemplos de desacoplamiento en la clase sobre espacios de nombres y, más recientemente, en las clases sobre patrones de diseño (por ejemplo, al hablar de Observer). Estas propiedades se ven con mayor claridad en el diagrama de dependencia de módulos, razón por la que construimos éste. Las especificaciones del módulo son también importantes para obtener localización; en este sentido, una especificación debe ser coherente, con una colección de comportamientos claramente definida (sin características ad hoc especiales) y una división terminante entre los métodos, de modo que éstos sean ampliamente independientes unos de otros. 20.4.2 Fiabilidad •
•
Esmero en el modelado. No es fácil dotar de fiabilidad a un sistema ya existente. La clave para lograr que un software sea fiable radica en desarrollarlo con el mayor cuidado, manteniendo ese esmero durante todo el proceso de modelado. Los problemas más graves en sistemas críticos no provienen de errores de código, sino de fallos en el análisis del problema; normalmente porque el analista no ha tenido en cuenta alguna propiedad del entorno en el que el sistema se halla insertado. Un ejemplo de ello es el fallo mecánico del Airbus en el aeropuerto de Varsovia. Revisión, análisis y prueba. Por mucho cuidado que se ponga, es inevitable cometer errores. Por ello, en todo proceso de desarrollo es preciso decidir por adelantado cómo se van a solucionar éstos. En la práctica, uno de los métodos más eficaces desde el punto de vista del coste a la hora de detectar errores de software, sea cual sea el modelo, la especificación o el código utilizados, es el de la revisión por pares. Hasta ahora, usted solamente habrá podido explorar este método con el profesor auxiliar y en las clases de laboratorio; en el proyecto de final de curso le conviene aprovechar la oportunidad de trabajar en equipo para analizar el trabajo de sus compañeros. De esta forma, tanto usted como ellos ahorrarán tiempo a largo plazo.
Análisis y pruebas más específicos permiten hallar aquellos errores que hayan pasado inadvertidos en el análisis de pares. Un análisis útil y fácil de realizar consiste simplemente en comprobar la coherencia de los modelos. ¿Soporta el modelo de objeto del código todos los estados del modelo de objeto del problema? ¿Se combinan adecuadamente las multiplicidades y mutabilidades? ¿Tiene en cuenta el diagrama de dependencia de módulos todas las restricciones del modelo de objeto? Otra posibilidad es comprobar el código con los modelos. La herramienta Womble, que se puede descargar desde el sitio http://sdg.lcs.mit.edu, construye automáticamente modelos de objetos a partir de Codi-bait (Byte-code). Hemos descubierto numerosos errores en nuestro código examinando modelos extraídos y comparándolos con los modelos planeados. Es conveniente, por tanto, que usted compruebe las propiedades esenciales de su modelo de objeto preguntándose si está seguro de que mantiene las propiedades. Suponga, por ejemplo, que su modelo dice que un vector nunca es compartido por dos objetos de cuenta bancaria. En tal caso, usted debería ser capaz de explicar por qué el código garantiza esta afirmación. Siempre que su modelo de objeto contenga una restricción que no se haya podido expresar gráficamente es especialmente aconsejable verificarla, ya que
es probable que comprenda relaciones que sobrepasen los límites del objeto 20.4.3 Eficiencia •
•
•
•
Modelo de objeto. La elección del modelo de objeto del código es esencial, ya que una vez elegido resulta muy difícil de cambiar. Por ello es conveniente considerar los objetivos de rendimiento crítico al comenzar el diseño. Más adelante veremos algunos ejemplos de transformaciones que se pueden aplicar al modelo de objeto para mejorar su eficiencia. Evitar el sesgo. Al desarrollar el modelo de objeto del problema deben dejarse de lado las cuestiones relativas a la implementación. Cuando un modelo de problema contiene detalles sobre su implementación se dice que está sesgado, ya que favorece un tipo de implementación en perjuicio de otro. Ello supone reducir prematuramente el espacio a posibles implementaciones, entre las que podría hallarse la más eficiente. Optimización. La palabra "optimización" es engañosa: invariablemente significa una mejora del rendimiento, pero en detrimento de otras cualidades (como la nitidez de la estructura). Y si no se tiene cuidado con la optimización se corre el riesgo de que el sistema acabe siendo peor en todos los aspectos. Antes de introducir un cambio para mejorar el rendimiento, asegúrese de que tiene suficientes pruebas de que las alteraciones tendrán probablemente un efecto muy positivo. En general es aconsejable resistir la tentación de optimizar y concentrarse en lograr que el diseño sea sencillo y claro. En cualquier caso, los diseños que cumplen estas premisas suelen ser los que muestran mayor eficiencia y, en caso de que no la muestren, siempre son los más fáciles de modificar. Elección de representaciones. En vez de perder el tiempo en lograr pequeñas mejoras del rendimiento, es mejor centrarse en las clases de mejora positiva que se pueden obtener eligiendo una representación diferente para un tipo abstracto, por ejemplo, capaz de cambiar una operación de tiempo lineal a tiempo constante. Muchos de ustedes lo habrán podido comprobar en su proyecto de MapQuick: cuando se elige una representación para grafos que requiere un tiempo proporcional al tamaño de todo el grafo para obtener vecinos de un nodo, la búsqueda resulta totalmente impracticable. Asimismo, no se debe olvidar que compartir objetos puede tener efectos positivos, por lo que hay que considerar la posibilidad de utilizar tipos invariables y hacer que los objetos compartan una estructura. Por ejemplo, en MapQuick, Route es un tipo invariable; si se implementa compartiendo estructura, cada extensión de la ruta por un nodo durante la búsqueda exige solamente situar un único nodo en vez de una copia entera de la ruta.
Ante todo, recuerde que lo más importante es la sencillez. Piense en lo fácil que resulta terminar embrollado en una masa de complejidades, incapaz de alcanzar ninguna de estas propiedades. Lo más sensato es diseñar y construir primero un sistema mínimo, lo más sencillo posible, y sólo entonces comenzar a añadir recursos. 20.5 Transformaciones del modelo de objeto En modelos de objetos de códigos y problemas, hemos visto dos usos distintos de la misma
notación. ¿Cómo puede un modelo de objeto describir un problema a la vez que describe una implementación? Para responder a esta pregunta resulta útil pensar en la interpretación de un modelo de objeto como un proceso en dos fases. En la primera, se interpreta el modelo en función de conjuntos y relaciones abstractas. En la segunda fase, se asocian estos conjuntos y relaciones, bien a las entidades y relaciones del problema, o bien a los objetos y campos de la implementación. Supongamos, por ejemplo, que tenemos un modelo de objeto con una relación employs (contrata) que asocia Company (empresa) a Employee (empleado).
Matemáticamente, vemos esto como una expresión de dos conjuntos y de una relación entre ambos. La restricción de multiplicidad nos dice que cada employee se asocia, conforme a la relación employs, a una company como máximo. A la hora de interpretarlo como un modelo de objeto de problema, contemplaremos el conjunto Company como un conjunto de empresas del mundo real (real world), y Employee como un conjunto de personas que son contratadas por las empresas. La relación employs relaciona c con e cuando la compañía c contrata a la persona e. Si lo interpretamos como un modelo de objeto de código, en cambio, contemplaremos el conjunto Company como un conjunto de objetos situados en una pila (heap) de la clase Company, y Employee como un conjunto de objetos situados en una pila de la clase Employee. Aquí, la relación employs pasa a ser un campo de especificación que asocia c y e cuando el objeto c mantiene una referencia a una colección (oculto en la representación de Company) que contenga la referencia e. Nuestra estrategia consiste en partir de un modelo de objeto de problema y transformarlo en uno de código. Por lo general, uno y otro serán considerablemente distintos, dado que un modelo que proporciona una descripción clara del problema no suele ofrecer una buena implementación.
¿Cómo se obtiene esta implementación? Un método de trabajo bastante apropiado consiste en realizar una sesión de brainstorming y, a partir de ella, jugar con diferentes fragmentos de modelos de código hasta que encajen. Es necesario comprobar que el modelo de objeto de código se corresponde fielmente al modelo de objeto del problema. Debe ser capaz de representar al menos toda la información sobre los estados del modelo de problema, de forma que sea posible, por ejemplo, añadir una relación, pero que no sea posible eliminarla. Otra forma de llevar a cabo la transformación es mediante la aplicación sistemática de una serie de pequeñas transformaciones. Cada una de ellas se elige de entre un repertorio de transformaciones que preservan el contenido de los datos del modelo. De esta forma, como cada paso mantiene el modelo intacto, toda la serie se mantendrá también invariable. Hasta el momento, nadie ha propuesto un repertorio completo de tales transformaciones (lo que representa un problema de investigación), pero sí que hay varias que se pueden identificar como las más útiles. Antes de seguir adelante, veamos un ejemplo. 20.6 Ejemplo de Folio Tracker Supongamos que queremos diseñar un programa para realizar el seguimiento de una cartera de valores. El modelo de objeto nos da la descripción de los elementos del problema. Folio es el conjunto de carteras, cada una de ellas con un Name (nombre), que contiene un conjunto de posiciones Pos. Cada posición corresponde a un Stock (valor) concreto, del que se mantiene algún número. Un stock puede tener un valor (cuando se haya obtenido recientemente una cotización), y tiene asignado un símbolo de registro de cotización que permanece invariable. Estos símbolos identifican únicamente a los valores. Se puede situar un observador (Watch) en una cartera, lo que hace que el sistema muestre la información relativa a la cartera cuando se produzcan determinados cambios en ésta.
20.7 Catalogo de transformaciones 20.7.1 Introducción de una generalización Si A y B son conjuntos con relaciones p y q, de la misma multiplicidad y mutabilidad, al conjunto C, podemos introducir una generalización AB y sustituir p y q por una única relación pq de AB a C. La relación pq puede no tener la misma multiplicidad fuente que p y q.
20.7.2 Inserción de una colección Si una relación r de A a B tiene una multiplicidad objetivo que permite más de un elemento, podemos interponer una colección, como un vector o un conjunto, entre A y B, y sustituir r por una relación a dos relaciones, una desde A a la colección y otra desde la colección a B.
En nuestro ejemplo de Folio Tracker, podríamos sustituir/interponer un vector en la relación posns entre Folio y Pos. Obsérvense las marcas de mutabilidad; la colección es generalmente construida y reorganizada con su contenedor.
20.7.3 Inversión de una relación Dado que la dirección de una relación no implica su capacidad para recorrerla en esa dirección, siempre cabe la posibilidad de invertirla. Al final, naturalmente, interpretaremos las relaciones como campos, por lo que es habitual invertir relaciones para orientarlas en la dirección en que se espera que sean recorridas. En nuestro ejemplo, podríamos invertir la relación name, ya que posiblemente querremos recorrerla desde nombres (names) a carteras (folios), obteniendo una relación folio, por ejemplo. 20.7.4 Traslado de una relación A veces es posible trasladar el objetivo o la fuente de una relación sin que ello suponga pérdida de datos. Por ejemplo, una relación de A a C se puede sustituir por una relación de B a C si A y B se hallan en una correspondencia de uno a uno.
En nuestro ejemplo, podemos sustituir la relación val entre Stock y Dollar por una relación entre Ticker y Dollar. Resulta conveniente utilizar un mismo nombre para la nueva relación, aunque técnicamente se trate de una relación diferente.
20.7.5 Relación a una tabla Una relación de A a B que tenga una multiplicidad objetivo igual a exactamente uno o cero-o-uno, puede sustituirse por una tabla. Dado que solamente se necesita una tabla, puede utilizarse el patrón de instancia única (singleton), de manera que la tabla se pueda referenciar por un nombre global. Si la multiplicidad objetivo de la relación es cero o uno, la tabla debe ser capaz de admitir correlaciones a valores nulos.
En FolioTracker, por ejemplo, podríamos convertir la relación folio a una tabla que permitiera hallar carteras mediante una operación de verificación constante. Así, tendríamos:
Sería interesante convertir también en una tabla la relación val que vincula Ticker a Dollar, ya que ello haría posible que la búsqueda de valores para símbolos de registro de cotización se encapsulara en un objeto distinto de la cartera de valores. En este caso, debido a la multiplicidad cero-o-uno, necesitaremos una tabla capaz de almacenar valores nulos.
20.7.6 Adición de estados redundantes Suele ser útil añadir componentes de estado redundantes a un modelo de objeto. Dos casos comunes de ello son la adición del traslado de una relación y la adición de la composición de dos relaciones. Si p asocia A a B, podemos añadir el traslado q de B a A. Si p asocia A a B, y q asocia B a C, podemos añadir la composición pq de A a C. 20.7.7 Descomposición de relaciones mutables Supongamos que un conjunto A tiene relaciones de salida p, q y r, de las cuales p y q son estáticas. Si se implementa directamente, la presencia de r hará que A sea mutable. Por tanto, sería conveniente descomponer la relación r utilizando, por ejemplo, la transformación Relación a Tabla, e implementar a continuación A como un tipo de datos inmutable. Volviendo a nuestro ejemplo, la descomposición de la relación val encaja en este patrón, ya que hace inmutable a la relación Stock. La misma idea subyace en el patrón de diseño Flyweight. 20.7.8 Interpolación de una interfaz Esta transformación sustituye el objetivo de una relación R entre un conjunto A y un conjunto B por un superconjunto X de B. Por regla general, A y B pasarán a ser clases y X se convertirá en una clase o interfaz abstracta. Gracias a ello, la relación R se podrá ampliar para asociar elementos de A a elementos de un nuevo conjunto C, implementando C como una subclase de X. Dado que X descompone las propiedades compartidas de sus subclases, tendrá una especificación más simple que B; la dependencia de A en X es, por lo tanto, menos rígida que su dependencia anterior en B. Para compensar la pérdida de comunicación entre A y B, se puede añadir (mediante una nueva transformación) una relación adicional desde B de regreso a A.
El patrón de diseño Observer (observador)es un ejemplo del resultado de esta transformación. En nuestro ejemplo, podríamos convertir los objetos Watch en observadores de los objetos Folio:
20.7.9 Eliminación de conjuntos dinámicos No es posible implementar como subclase un subconjunto que no sea estático: los objetos no pueden migrar entre clases en tiempo de ejecución, por lo que es necesario transformarlos. Una clasificación en subconjuntos puede transformarse en una relación del subconjunto a un conjunto de valores de clasificación.
Cuando solamente hay uno o dos subconjuntos dinámicos, los valores de clasificación pueden ser valores booleanos primarios. La clasificación se puede también transformar en varios conjuntos únicos, uno para cada subconjunto.
20.8 Modelo de objeto final El siguiente gráfico muestra el resultado en el ejemplo de Folio Tracker de la secuencia de transformaciones que hemos comentado. Llegados a este punto, debemos comprobar que nuestro modelo es capaz de soportar las operaciones que el sistema debe realizar, y utilizar los escenarios de estas operaciones para construir un diagrama de dependencia del modelo que nos permita verificar la viabilidad del diseño. Tendremos que añadir módulos para la interfaz de usuario y para cualquier otro dispositivo que haya que utilizar para obtener las cotizaciones de las acciones. Asimismo, nos conviene añadir un mecanismo para almacenar carteras en disco de modo permanente. Para algunas de estas tareas, necesitaremos volver sobre nuestros pasos y construir un modelo de objeto del problema, pero para otras partes habrá que trabajar en el nivel de implementación. Así, por ejemplo, si queremos que los usuarios puedan dar nombre a los archivos para almacenar carteras en ellos, es prácticamente seguro que necesitaremos un modelo de objeto del problema. Sin embargo, la construcción de un modelo no resultará posiblemente eficaz para resolver cuestiones relativas al modo de analizar una página Web para obtener cotizaciones de acciones.
20.9 Lenguaje unificado de modelado (UML) y métodos Existen varios métodos que describen detalladamente estrategias para el desarrollo orientado a objetos, indicando qué modelos hay que crear y en qué orden. En un escenario industrial, establecer un método como estándar puede servir de ayuda a la hora de coordinar el trabajo de diversos equipos. Aunque en este curso no se enseña ningún método en concreto, las nociones que usted ha asimilado son la base de la mayoría de ellos, por lo que no debería tener problema en aprender cualquier método específico. Casi todos los métodos utilizan modelos de objeto, y algunos utilizan también diagramas de dependencia de módulos. Si desea conocer más sobre la materia, le recomiendo introducir en Google una búsqueda con los términos "Catalysis", "Fusion" y "Syntropy"; que le dirigirá a libros y materiales online. En los últimos años se han producido diversos intentos de estandarización de las notaciones. El Object Management Group (grupo de administración de objetos) ha adoptado como notación estándar el lenguaje unificado de modelado (UML), que es en realidad una amplia colección de notaciones diversas, en la que se incluye una notación de modelado de objetos similar a la nuestra (aunque mucho más compleja).