00_IntrodProg.qxd
23/7/07
20:01
Page 1
Introducción a la Programación Preparado para rendir la estrella inicial del examen DCE
por José
María Selesan
00_IntrodProg.qxd
23/7/07
20:01
Page 2
TÍTULO > Introducción a la Programación AUTOR > José María Salesan FORMATO > 13,5 x 19 cm PÁGINAS > 96
Copyright © GRADI S.A. 2007. Hecho el depósito que marca la ley. Reservados todos los derechos de autor. Prohibida la reproducción total o parcial de esta publicación por cualquier medio o procedimiento y con cualquier destino. Primera impresión realizada en agosto de 2007. Kollor Press, Capital. Todas las marcas mencionadas en este libro son propiedad exclusiva de sus respectivos dueños.
Selesan, José María Introducción a la programación. - 1a ed. - Banfield Lomas de Zamora : Gradi S.A., 2007. v. 11, 96 p. ; 19x14 cm. (Pocket users) ISBN 978-987-1347-45-2 1. Informática. I. TítuloCDD 005.3
00_IntrodProg.qxd
23/7/07
20:01
Page 3
Introducción El objetivo del libro es brindar al lector un panorama general sobre el desarrollo de software para computadoras y servir como punto de partida, tanto para rendir el examen cero del programa Desarrollador Cinco Estrellas de Microsoft, como para adentrarse en esta fascinante profesión. A lo largo de los cinco capítulos que lo integran, nos introduciremos en algunos de los principales temas que todo programador debe conocer. El primer capítulo trata sobre los fundamentos del software y los algoritmos. Si bien pueden parecer un tanto triviales, son de fundamental importancia, porque un programador debe estar preparado y capacitado para pensar cualquier solución a un problema en forma de algoritmo. Estudiaremos, también, distintas maneras de expresar algoritmos, necesarias como herramientas de comunicación y documentación. En los capítulos dos y tres, abordaremos temas relacionados con la programación estructurada. A lo largo de ellos, aprenderemos muchos conceptos, la mayoría de los cuales se utilizan a diario, porque forman la base de cualquier programa. Aprenderemos los
conceptos de variable, estructuras de control, buenas prácticas de codificación, y más. Es importante que el lector medite en profundidad cada uno de estos temas, realice prácticas y consulte más material para asimilar correctamente cada uno de ellos y establecer una base de conocimiento sólida que le permita enfrentar cualquier tipo de desarrollo. Luego, el capítulo cuatro presenta una introducción a la Programación Orientación a Objetos (POO). Este paradigma, si bien no es nuevo, presenta un avance importante en la manera de desarrollar software, y recién en los últimos años se lo está considerando como el paradigma por defecto para todo nuevo desarrollo. Si bien este capítulo no ahonda en detalles, tiene como objetivo familiarizar al lector con cada uno de los conceptos clave de la POO. Finalmente, el capítulo cinco introduce algunos de los conceptos del Lenguaje Unificado de Modelado (UML). UML se basa fuertemente en los conceptos de POO, como clases y herencia, por lo que será necesario comprender los temas del capítulo cuatro para aprovechar al máximo éste. 3
00_IntrodProg.qxd
23/7/07
20:01
Page 4
CONTENIDO ‹‹ Tipos de datos
Capítulo 1
Sentencias
FUNDAMENTOS ¿Qué es el software?
8
Un poco de historia
9
Operadores y expresiones Tipos de operadores Estructuras de control Conclusiones
34
Capítulo 3 ELEMENTOS DE PROGRAMACIÓN Modularización Los lenguajes de programación
11
Los lenguajes en la actualidad
13
Compilación e interpretación
14
Algoritmos
15
Expresión de algoritmos Conclusiones
36
Procedimientos y funciones Librerías
44
Arreglos y matrices
45
15 18
Capítulo 2 PROGRAMACIÓN ESTRUCTURADA Mejorar el código
20
Hacia la programación estructurada Conceptos de programación estructurada
22
Arreglos Matrices El estilo de programación
50
Tabulaciones Comentarios Conclusiones
54
Capítulo 4 PROGRAMACION ORIENTADA A OBJETOS Los objetos
4
Identificadores y palabras clave
Origen
Variables
Ventajas de la POO
56
00_IntrodProg.qxd
23/7/07
Clases y objetos
20:01
Page 5
58
Pensar en objetos
60
Patrones de diseño Relaciones entre clases
Capítulo 5 UML
Propiedades y métodos
63
Relación de uso
¿Qué es UML?
76
Diagrama de clases
77
Clases Relaciones entre clases
Relación de agregación
Diagrama de secuencia
83
Elementos del diagrama de secuencia Diagramas de casos de uso
85
Conclusiones
86
Apéndice Herencia
66
Redefinir comportamiento Extender clases Tipos de herencia Clases abstractas
EL EXAMEN Algoritmos
88
Lenguajes de programación
88
Programación estructurada
89
Programación orientada
Interfaces Polimorfismo
72
Conclusiones
74
a objetos
91
Desarrollador 5 Estrellas
92
5
00_IntrodProg.qxd
23/7/07
20:01
Page 6
Prólogo Cuando un músico escucha una melodía que acaba de componer o cuando un escultor retoca el último detalle de su obra maestra, está ante un logro personal, pero que fue hecho pensando en los demás. Está ante el fruto de su trabajo, que tuvo que realizar para pasar de una idea o modelo que estaba sólo en su imaginación, a algo perceptible por los otros. Y ese logro causa una gran satisfacción. El desarrollo de software –al igual que la música, la escultura o la pintura– es una actividad creativa y, hasta si se quiere, artística. Es una actividad en la que una persona (el programador) debe plasmar una idea en un programa que alguien usará luego. Y es muy probable que esa idea sea algo totalmente novedoso, algo que nadie antes ha visto. Y ver esa idea traducida en software produce una sensación que sólo un programador puede entender. Ser programador no es fácil, pero es divertido. Desarrollar software es, definitivamente, una tarea compleja. Deberemos ser capaces de interpretar al usuario, de entender qué es lo que necesita (aunque muchas veces él mismo no sabe qué es lo que necesita). Pasaremos largas horas pensando un algoritmo que resuelva el problema de nues6
tro cliente de la mejor manera. Muchas veces nos iremos a casa y seguiremos pensando en ese dichoso algoritmo. Nos iremos a dormir (probablemente tarde, porque habremos dedicado parte de nuestro tiempo libre a aprender nuevas tecnologías) y despertaremos pensando en el algoritmo o, incluso, con la solución en mente. Pero cuando escribamos la última línea del código que implementa ese algoritmo y lo veamos funcionando, obtendremos una gran satisfacción. Una satisfacción que sólo puede entender otro programador. Cuando escribí mi primera línea de código, debo haber tenido unos 11 años. Ese día, al ver que una secuencia de caracteres casi ilegible dibujaba un círculo en la pantalla del televisor (la época de las PCs recién comenzaba), me di cuenta de que eso era lo que quería hacer el resto de mi vida. Creo que cada persona que quiere dedicarse al desarrollo de software, antes de nada, debe experimentar qué sensación le produce ver su programa en ejecución. Aquel que no sienta nada jamás podrá ser un programador, porque la mayor recompensa es, justamente, esa sensación, aunque sean las 4 de la madrugada y llevemos 20 horas codificando.
01_IntrodProg.qxd
23/7/07
20:04
Page 7
CAPÍTULO 1
Fundamentos En este primer capítulo, veremos los fundamentos básicos del software, su funcionamiento, su uso y su creación. A su vez introduciremos los principales conceptos sobre la programación moderna.
ATENCIÓN AL LECTOR >
[email protected]
01_IntrodProg.qxd
23/7/07
20:04
Page 8
FUNDAMENTOS
¿QUÉ ES EL SOFTWARE? > Desde siempre, las computadoras han sido máquinas con la única capacidad de llevar a cabo instrucciones, como imprimir un texto en un dispositivo de salida o sumar dos números. Un programa es un conjunto de instrucciones y datos que juntos y, de manera sistemática, permiten resolver problemas. Podemos entonces definir el software como el conjunto de programas que funcionan en una computadora y que permiten realizar una o varias tareas específicas. Es importante resaltar que, al hablar de computadoras, no nos referimos sólo a las personales (PC), sino a cualquier dispositivo capaz de leer instrucciones de una memoria, y ejecutarlas. Por lo tanto, podemos encontrar software en un lavarropas, en un respirador artificial y hasta en automóviles modernos. Según su uso, el software se puede clasificar en dos grandes grupos: el software de sistema y el software de aplicación. El software de sistema es el conjunto de programas básicos para el funcionamiento de la computadora, como por ejemplo el sistema operativo (Windows o Linux), los drivers, etcétera., mientras que el software de aplicación son los programas para realizar tareas específicas, como un procesador de texto, un juego o un compilador. Los términos software y sistema se utilizan para referirse a lo mismo. Sin embargo, la palabra sistema por sí sola no tiene nada que ver con el software. Un sistema es un conjunto de elementos que interactúan de alguna manera, como puede ser el sistema digestivo o el sistema solar. Otro ejemplo de sistema, son los sistemas de información, sin que esto tampoco implique un software. Por ejemplo, un sistema contable es un conjunto de métodos y de herramientas que permiten mantener la información sobre los movimientos económicos y los bienes de una empresa, pero se pueden utilizar libros en papel para alcanzar el objetivo. Finalmente, un sistema de información basado en computadora es la implementación
› EL HARDWARE El otro componente importante de una computadora es el hardware, que incluye todos los elementos físicos o materiales que la conforman. La diferencia entre el 8
hardware y el software es que este último es intangible (lo vemos, pero no podemos tocarlo), mientras que el hardware es totalmente palpable.
01_IntrodProg.qxd
23/7/07
20:04
Page 9
› ¿QUÉ ES EL SOFTWARE?
con herramientas computacionales (programas y datos) de un sistema de información. Por lo tanto, como sinónimo de software podemos utilizar sistema de información basado en computadora. Por otro lado, los términos programa y sistema suelen utilizarse indistintamente, pero no está bien. Generalmente, podemos decir que un sistema es un grupo de programas que interactúan para realizar ciertas tareas. Un programa es una unidad mucho más pequeña, independiente y sencilla que un sistema. UN POCO DE HISTORIA
Muchos autores coinciden en afirmar que la idea de programa como secuencia de instrucciones se remonta a principios del siglo XIX y no tiene nada que ver con la computación. Efectivamente, en 1801 un francés llamado Joseph Marie Jacquard ideó un mecanismo de tarjetas perforadas para controlar los dibujos que formaban los hilos en una máquina para tejer. De esa manera, lograba programar las puntadas de la máquina para obtener tramas y figuras repetibles. En 1843, Ada Augusta Lovelace, hija del poeta inglés Lord Byron, planteó la idea de usar tarjetas perforadas para controlar la Máquina Diferencial de Babbage (ver recuadro) y lograr que repita ciertas operaciones. Unos años más tarde, su idea fue tomada para desarrollar un sistema de cómputo para la oficina de censos de los Estados Unidos. Las tarjetas estaban diseñadas de tal modo que los agujeros representaban la edad, raza, sexo, etcétera. Este desarrollo permitió que el tiempo en obtener los resultados del censo de 1890 fuera de 5 años menos que el censo anterior. La idea de lady Ada tuvo tal repercusión que, al día de hoy, se la considera como la primera programadora, y las tarjetas perforadas fueron utilizadas en centros de cómputos hasta no hace mucho tiempo.
› LA MÁQUINA DIFERENCIAL En el año 1812, Charles Babbage, preocupado por los errores de cálculo en las tablas matemáticas, pensó que sería útil poder calcularlos de forma automática. Ideó, entonces, una máquina capaz de obtener aproximaciones al resultado de una fun-
ción matemática, mediante un método llamado de las diferencias. Si bien por limitaciones propias de la época nunca llegó a terminar su construcción, su denominada Máquina diferencial sentó las bases de la computación moderna. 9
01_IntrodProg.qxd
23/7/07
20:04
Page 10
FUNDAMENTOS
Ya en el siglo XX, el físico estadounidense John Atanasoff, conocedor de las teorías de Babbage y consternado por la cantidad de cálculos que debía realizar, pensó en construir una máquina de cálculo que, a diferencia de las mecánicas, sería digital, y su funcionamiento se basaría en el sistema binario. Su aparato fue conocido como ABC Atanasoff-Berry-Computer, y por eso es considerado el iniciador de la computación digital. Luego, durante la Segunda Guerra Mundial, se construyó y comenzó a funcionar en instalaciones militares de los Estados Unidos una máquina llamada ENIAC (Electronic Numeric integrator and Computer). Su funcionamiento se basaba en tubos de vacío, interruptores y relés para hacer operaciones matemáticas utilizando el sistema binario. Por su tamaño, ocupaba una habitación entera.
Figura 1. La ENIAC fue una de las primeras computadoras del siglo XX. Su poder de cálculo era menor al de una calculadora de bolsillo actual.
A partir de la ENIAC, las computadoras fueron evolucionando año tras año, a un ritmo cada vez más vertiginoso hasta llegar a las computadoras que conocemos en la actualidad. Sin embargo, a pesar de esta evolución, las computadoras mantienen dos características esenciales: están basadas en el sistema binario y necesitan que se les provea de una secuencia ordenada de instrucciones para poder funcionar.
› LA MÁQUINA DE TURING Creada por Alan Turing, consistía en una cinta y un cabezal que podía leer y escribir caracteres en dicha cinta, como también moverse hacia la izquierda y hacia la derecha. La cinta podría verse como el conjunto de entradas y salidas de un programa. Turing demostró 10
que su máquina podría resolver cualquier problema representable por un algoritmo y que, si no era capaz de resolverlo, entonces resultaba, insoluble en una computadora. Su trabajo se convirtió en el tema central de estudio de la Teoría de la Computación.
01_IntrodProg.qxd
23/7/07
20:04
Page 11
› LOS LENGUAJES DE PROGRAMACIÓN
LOS LENGUAJES DE PROGRAMACIÓN > Ahora que sabemos qué es un programa, podemos decir que la programación
es el proceso de construir programas. Para escribir programas, necesitamos conocer la lista de las posibles instrucciones que debemos proporcionar a la computadora, y cómo combinarlas para lograr los resultados deseados. De acuerdo con el primer párrafo, para confeccionar programas, debemos escribir una secuencia de instrucciones que puedan ser entendidas por la computadora, pero como vimos anteriormente, hasta las computadoras más modernas trabajan con el sistema binario, por lo que, para programarlas, deberíamos proporcionarles secuencias de unos y ceros formando las instrucciones de nuestro programa. Si bien el sistema binario es muy simple, para un ser humano, resultaría imposible recordar las combinaciones de unos y de ceros que forman cada una de las instrucciones. Para solucionar este problema, existen los lenguajes de programación, una forma más sencilla y legible de representar las instrucciones. Sin entrar en detalles formales, podemos decir que un lenguaje de programación es un conjunto de reglas que determinan, de forma clara, precisa y sin ambigüedades, la forma en que se le imparten las instrucciones a una computadora para construir un programa. Estas reglas se dividen en reglas sintácticas y reglas semánticas. Las reglas sintácticas especifican cuáles son los caracteres válidos del lenguaje y cómo se pueden agrupar en palabras también válidas. Las reglas semánticas determinan cuál es el significado de las palabras, es decir, qué se espera que la computadora haga cuando recibe una palabra o una instrucción.
› ARQUITECTURAS CISC Y ARQUITECTURAS RISC El conjunto de instrucciones que puede entender una computadora depende de su arquitectura (por ejemplo, un procesador Intel tiene un conjunto de instrucciones diferente del de un procesador Motorola). La cantidad de instrucciones permiten clasi-
ficar a los procesadores en dos grandes grupos: CISC (Complex Instruction Set Computer, computadora con conjunto de instrucciones complejo) y RISC (Reduced Instruction Set Computer, computadora con conjunto de instrucciones reducido). 11
01_IntrodProg.qxd
23/7/07
20:04
Page 12
FUNDAMENTOS
Los lenguajes de programación fueron creados para facilitar la escritura de programas, proveyendo una abstracción de las instrucciones reales de la computadora (formadas por unos y ceros) y reemplazándolas por palabras que fueran más fáciles de recordar por las personas. Según el nivel de abstracción que proveen, los lenguajes de programación se clasifican en tres niveles (Tabla 1). Cuanto más alto sea el nivel, más cercanas al lenguaje humano serán sus instrucciones y, generalmente, más poderosas, ya que cada instrucción del lenguaje puede representar operaciones complejas formadas por muchas instrucciones de la máquina. TIPOS DE LENGUAJES
…
NIVEL DEL LENGUAJE CARACTERÍSTICAS Bajo Las instrucciones del lenguaje están muy relacionadas con las instrucciones de la computadora, por lo tanto, el programador debe tener un buen conocimiento del funcionamiento del equipo. Al programar directamente (o casi) con instrucciones de la máquina, se obtienen resultados eficientes y se puede lograr cualquier cosa que haga la computadora, aunque con un esfuerzo más grande que con otros niveles de abstracción. Como lenguaje de bajo nivel, podemos mencionar el Assembler. Medio Si bien algunos autores ignoran este nivel, existe un grupo de lenguajes con una abstracción un poco más alta que los de bajo nivel, pero aún bastante cercana al hardware. Su sintaxis es más sencilla que la de los lenguajes de bajo nivel, pero permiten escribir programas cercanos al lenguaje de la computadora. Un ejemplo típico de lenguaje de nivel medio es el lenguaje C. Alto Este lenguaje está formado por palabras comunes en algún idioma (como por ejemplo el inglés), por lo que resultan fáciles de recordar y de interpretar. Generalmente, son lenguajes más poderosos en cuanto a expresividad que los de bajo nivel, pero puede ocurrir también que resulte difícil implementar programas que tengan que hacer un uso muy específico del hardware. Como ejemplo de lenguajes de alto nivel, podemos mencionar a C# o Java. Tabla 1. Clasificación de los lenguajes según su nivel de abstracción.
Es muy importante tener en cuenta que, aunque los lenguajes de más alto nivel puedan parecerse al lenguaje natural de los seres humanos, deberán ser más 12
01_IntrodProg.qxd
23/7/07
20:04
Page 13
› LOS LENGUAJES DE PROGRAMACIÓN
estrictos y limitados, ya que es necesario que permitan definir las instrucciones de una forma entendible y sin ningún tipo de ambigüedad. LOS LENGUAJES EN LA ACTUALIDAD
A lo largo del tiempo, los lenguajes han evolucionado y se adaptaron a las necesidades y a las posibilidades computacionales de cada momento. Por eso, actualmente disponemos de una gran cantidad de lenguajes de programación entre los cuales elegir a la hora de comenzar un desarrollo o de aprender una nueva tecnología. Para tomar una buena decisión y elegir el lenguaje que más nos conviene, es importante conocer sus características, sus capacidades y debilidades. Además, un aspecto fundamental para tener en cuenta es el paradigma o enfoque del lenguaje, es decir, de qué forma hay que pensar y escribir el programa para resolver un problema. Los paradigmas más conocidos son el imperativo, el declarativo y el orientado a objetos. Mediante el paradigma imperativo, debemos indicar explícitamente los pasos por seguir para resolver el problema, es decir, debemos indicar el cómo. Por otro lado, el paradigma declarativo permite escribir el programa describiendo las características del problema, es decir, especificando el qué. Por último, el paradigma orientado a objetos permite modelar y escribir los programas a partir de la abstracción de los objetos reales que forman parte del dominio del problema. Actualmente, el paradigma declarativo es usado en el ambiente académico y con fines de investigación, mientras que, en ambientes productivos y de negocios, se utilizan los lenguajes imperativos y, en mayor medida, los orientados a objetos.
› ¿QUÉ ES EL ANÁLISIS LÉXICO? La compilación de un programa es una tarea bastante compleja, que involucra una serie de pasos conducentes a la generación del código de máquina. El primer paso se denomina análisis léxico. Consiste en leer el texto del programa (normalmente almacenado en uno o en varios archi-
vos) y aplicar ciertas reglas para identificar las palabras clave y las expresiones válidas del lenguaje de programación. Durante el análisis léxico, ya pueden identificarse algunos errores básicos, frecuentes en la mayoría de los programas, como por ejemplo, los identificadores mal formados. 13
01_IntrodProg.qxd
23/7/07
20:04
Page 14
FUNDAMENTOS
COMPILACIÓN E INTERPRETACIÓN > Cuando escribimos un programa en algún lenguaje, nuestro propósito final siem-
pre será ejecutarlo en una computadora para realizar una tarea específica. Ahora bien, al utilizar un lenguaje de programación estamos escribiendo instrucciones que no son exactamente las que la computadora entiende. Para que nuestro programa pueda ser ejecutado, será necesario traducir las instrucciones que hemos escrito en forma más sencilla para que la computadora pueda entenderlas. Esta traducción resulta siempre necesaria para poder ejecutar un programa escrito en cualquier lenguaje. El tipo de traducción que se haga, permitirá clasificar los lenguajes en lenguajes compilados y lenguajes interpretados. En los lenguajes compilados, la traducción se realiza por única vez, almacenando las instrucciones ya traducidas a lenguaje máquina en un archivo. De este modo, al momento de ejecutar el programa, la computadora lee una a una las instrucciones del archivo generado durante el proceso de compilación, y las ejecuta. Para realizar la traducción o compilación, se utiliza un programa llamado compilador, que se encarga de tomar cada una de las instrucciones escritas en un lenguaje de programación y traducirlas a una o más instrucciones del lenguaje de la máquina. Durante la compilación, el compilador además valida que las instrucciones que hemos escrito sean correctas y que se respeten todas las reglas del lenguaje. Por otro lado, con los lenguajes interpretados, no existe un paso previo de compilación, sino que la traducción se realiza mientras se ejecuta el programa. Para ello, se utiliza un software denominado intérprete, que a medida que el programa se ejecuta, lee cada una de las instrucciones y las traduce a instrucciones de la máquina, para que sean ejecutadas. Al igual que los compiladores, los intérpretes deben validar que las instrucciones estén bien escritas para poder traducirlas sin problemas. Generalmente, usando lenguajes compilados, se logra mayor rendimiento, ya que el proceso de validación y de traducción se realiza una sola vez (al momento de la compilación). Luego, una vez que el programa está compilado, cada ejecución se hace sobre una secuencia de instrucciones de la computadora. Al usar lenguajes interpretados, la validación y traducción se realiza cada vez que se ejecuta el programa. Es más, si una misma instrucción se repite varias veces, el intérprete la validará y traducirá cada vez. Sin embargo, los lenguajes interpretados proveen cierto grado de flexibilidad a la hora de realizar cambios en nuestro programa ya que, modificando cualquier instrucción, estará lista para que el intérprete la tome y la traduzca. 14
01_IntrodProg.qxd
23/7/07
20:04
Page 15
› ALGORITMOS
ALGORITMOS > Según el diccionario, un algoritmo es un conjunto ordenado de operaciones
sistemáticas que permite hacer un cálculo y hallar la solución de un tipo de problemas. Por ejemplo, cualquiera de los métodos que aprendimos en la escuela para multiplicar dos números reales es un algoritmo. El método o la receta para preparar una comida no es un algoritmo, ya que el orden de algunos pasos, muchas veces, no importa o ni siquiera está claramente especificado (por ejemplo, podemos batir las claras a nieve antes o después de mezclar la harina con él azúcar, y aún así vamos a poder hacer una torta). Ahora que conocemos los algoritmos, podemos redefinir el término programa como implementación de un algoritmo determinado en un lenguaje de programación. El conocimiento del concepto de algoritmo es fundamental para todo programador ya que, en la tarea diaria de escribir programas para resolver problemas, antes debemos descubrir y entender cuál es el algoritmo que los resuelve. Muchas veces, como programadores, nos encontraremos siguiendo los pasos de un algoritmo con lápiz y papel para entender su funcionamiento o probar su eficacia. EXPRESIÓN DE ALGORITMOS
Como los algoritmos no están relacionados únicamente con la programación de computadoras, es necesario contar con métodos independientes para expresarlos y, por lo tanto, transmitirlos a otras personas. En la actualidad, existen varias formas de expresar un algoritmo, como ser, el lenguaje natural, el pseudocódigo, los diagramas de flujo, algunos diagramas del lenguaje de modelado UML (que veremos en el último capítulo), etcétera. En el caso del lenguaje natural, el algoritmo es expresado por medio de oraciones en un idioma determinado. A modo de ejemplo, se puede indicar el al-
› LIBROS ÚTILES Hay una gran cantidad de libros para introducirse en el mundo de la programación de software y de los algoritmos. Recomendamos, Metodología de la Programación. Algoritmos, diagra-
mas de flujo y programas, de Osvaldo Cairo (ISBN 9701509404). En el cual, se proponen distintas técnicas de análisis para enfrentar la solución de un problema y llevarla a un algoritmo. 15
01_IntrodProg.qxd
23/7/07
20:04
Page 16
FUNDAMENTOS
goritmo, en lenguje natural, para ver una película en DVD de la siguiente manera: presione la tecla Open/Close para abrir la bandeja del reproductor; luego, inserte el disco y presione la tecla nuevamente. En su televisor, seleccione la opción DVD y por último presione la tecla Play. Pseudocódigo
Uno de los problemas del lenguaje natural es que suele resultar excesivamente verborrágico, ya que debe respetar en cierta medida las reglas gramaticales del lenguaje. Para evitar este problema, se puede utilizar un pseudocódigo, que consiste en una serie de normas sintácticas y gramaticales, parecidas a las de un lenguaje de programación, pero sin tanta rigidez, y con cierta libertad en el uso y en la combinación de las palabras. Por ejemplo, supongamos que debemos escribir el algoritmo de Euclides para encontrar el máximo común divisor entre dos números enteros. Usando pseudocódigo, podemos consignar: Entrada: A, B enteros Salida: el máximo divisor común entre A y B Mientras A > 0 hacer: Si A > B
› CARACTERÍSTICAS DE LOS ALGORITMOS El estadounidense Donald Knuth, autor del libro El arte de programar computadoras, señaló cinco propiedades que deberá tener todo algoritmo: Finito: un algoritmo debe tener un número finito de pasos, tras los cuales debe terminar. Preciso: cada paso debe estar definido con precisión, rigurosidad y sin ambigüedades. Entradas: todo algoritmo debe tener 16
cero o más entradas, que son los datos que se le proporcionan como información variable y específica de la instancia del problema que resuelve. Salida: un algoritmo tiene una o más salidas, que son el resultado del problema que intenta resolver, y que dependen de las entradas provistas. Eficacia: los pasos deben ser suficientes para lograr el cometido del algoritmo, es decir, el algoritmo debe ser eficaz.
01_IntrodProg.qxd
23/7/07
20:04
Page 17
› ALGORITMOS A := A – B Si no B := B – A El Máximo Común Divisor es el contenido de A
Esto significa que la entrada al programa consiste de números enteros (A y B) y, mientras el valor de A sea mayor que cero, se le asigna a A la resta entre A y B, si A es mayor que B; si no, a B se la asigna la diferencia entre B y A. El resultado es el valor que queda en A luego de las sucesivas restas. Como puede verse en el ejemplo, se especifican claramente cuáles son los datos de entrada y de salida. Además se utilizan algunos símbolos ya conocidos (como el signo mayor a) y otros propios del pseudocódigo (como el signo := para representar que el elemento de la izquierda toma como valor el resultado de la operación de la derecha). Diagrama de flujo
Los diagramas de flujo son una representación gráfica de los pasos de un algoritmo. En un diagrama de flujo, cada tipo de figura tiene su significado. Su nombre se debe a que las figuras se conectan con flechas que indican la secuencia o flujo de operación. Si bien muchas personas utilizan sus propios símbolos y figuras al momento de crear sus diagramas de flujo, actualmente está definido de manera clara y estándar cuáles son las figuras válidas y cuál es su significado, de manera que cualquiera que las conozca pueda interpretar el diagrama. Los símbolos más utilizados son: • Flecha: indica el sentido del proceso, es decir, hacia dónde hay que dirigirse para encontrar el siguiente paso.
› PARA TENER EN CUENTA Al hacer un diagrama de flujo, hay que tener muy presente estos aspectos: • Debe haber un único punto de inicio del proceso.
• Debe haber siempre un camino para llegar a la solución. • Debe haber un único punto de fin del proceso. 17
01_IntrodProg.qxd
23/7/07
20:04
Page 18
FUNDAMENTOS
• Rectángulo: se usa para representar un paso determinado del algoritmo. • Rombo: representa un punto de decisión sobre la base de una condición. De un rombo salen siempre dos flechas: una en un sentido, si se cumple la condición y otra en otro sentido, si la condición no se cumple. Para ilustrar el concepto, vamos a especificar el algoritmo de Euclides que usamos anteriormente, mediante un diagrama de flujo.
Sí
A>0
No
Sí
Devolver A
A>B
A En el Capítulo 1, vimos que un programa es la implementación de un algoritmo determinado, es decir, una secuencia de pasos o instrucciones, mediante un lenguaje de programación. Cuando escribimos un programa, es normal que necesitemos alterar la secuencia de instrucciones siguiendo determinadas pautas. Si recordamos el algoritmo que analizamos en el capítulo anterior, podemos ver que en cada vuelta, la instrucción por ejecutar depende de si A es mayor que B, o B es mayor que A. En los primeros lenguajes de programación (como el BASIC original), la única forma que existía de variar las instrucciones por ejecutar según una condición era mediante un salto a otra parte del programa (generalmente, a otra posición de memoria u otro número de línea). Es decir, debíamos evaluar la condición y, dependiendo de si ésta era verdadera o falsa podíamos seguir en la línea de abajo o hacer que la próxima instrucción por ejecutar fuera diferente, ubicada en otra parte del programa. Por ejemplo, para resolver el algoritmo de Euclides usando saltos, deberíamos escribir algo así (con pseudocódigo): 1: Si A 0 Ir a línea 5 3: B := B – A 4: Ir a línea 1 5: A := A – B 6: Ir a línea 1 7: Devolver A
Esta implementación tiene dos grandes problemas. El primero es que su escritura se vuelve un poco dificultosa, porque hay que conocer los números de línea. Por ejemplo, al escribir la línea 1, ya hay que saber que vamos a saltar a la línea 7. Si bien esto podría solucionarse usando números de línea que vayan, por ejemplo, de 10 en 10 y hacer los saltos a números más bien lejanos, a medida que necesitemos modificar el algoritmo y escribir más líneas de código (incluso entre dos líneas ya existentes), los números disponibles se irían acercando más y más, y podría llegar el momento en que necesitáramos renu20
02_IntrodProg.qxd
23/7/07
20:05
Page 21
› MEJORAR EL CÓDIGO
merar las líneas para tener más espacio. El segundo problema reside en la poca claridad del código. Para alguien que lo lee por primera vez, puede resultar extremadamente difícil entender. Dados los constantes saltos hacia atrás y hacia adelante a través de las instrucciones del programa, este tipo de código inevitablemente enredado se conoce como código spaghetti, y la instrucción más famosa para hacer saltos de línea es GOTO (del inglés go to, ir a). HACIA LA PROGRAMACIÓN ESTRUCTURADA
Con el paso del tiempo, los programadores vieron que el código spaghetti resultaba muy difícil de mantener, ya sea para agregar una nueva característica al programa o para corregir un error. Así, en la década del 60, un científico de los Países Bajos, Edsger Dijkstra, formuló un teorema en el que demostró que cualquier programa informático puede escribirse usando sólo tres tipos de instrucciones básicas: la secuencia, la evaluación de condiciones y la repetición o bucle de instrucciones, y sin utilizar instrucciones GOTO. Este teorema, conocido como el Teorema de Dijkstra, sentó las bases de lo que hoy conocemos como programación estructurada y de una gran familia de lenguajes de programación basados en este concepto. Al escribir un programa utilizando las tres estructuras propuestas por Dijkstra, se obtienen programas mucho más claros, fáciles de entender y de mantener ya que, en todo momento, la lógica está a la vista, y resulta fácil encontrar cuál es la siguiente instrucción por ejecutar.
Iteración
{
Mientras A > 0 hacer: Si A > B
Condicional
A: = A - B Si no B: = B - A Imprimir A Imprimir B
}
Secuencia
Devolver A
Figura 1. En esta figura, vemos las tres estructuras de control propuestas por Dijkstra en el algoritmo de Euclides. 21
02_IntrodProg.qxd
23/7/07
20:05
Page 22
PROGRAMACION ESTRUCTURADA
CONCEPTOS DE PROGRAMACIÓN ESTRUCTURADA > En las siguientes secciones, veremos los conceptos básicos de la programación estructurada, junto con su implementación utilizando lenguajes de la plataforma .Net. IDENTIFICADORES Y PALABRAS CLAVE
Como vimos antes, un programa está formado por instrucciones. En los lenguajes de programación, sobre todo en los de nivel medio y alto, las instrucciones están formadas por palabras que pueden pertenecer o no a algún idioma humano. Como también vimos, los lenguajes de programación son más estrictos que los lenguajes naturales y no se permite el uso de cualquier palabra ni tampoco cualquier combinación de ellas. Toda palabra que se pueda usar en un programa, ya sea parte del lenguaje de programación o definida por nosotros, recibe el nombre de identificador. En todos los lenguajes de programación, los identificadores deben respetar ciertas reglas para que sea más sencillo el trabajo del compilador o del intérprete. La forma más común de representar un identificador es por una secuencia de uno o más caracteres consecutivos (no se permiten los es-
› C# Y VB.NET Los dos lenguajes más importantes en .Net (y los únicos provistos por Microsoft desde la discontinuidad de J#) son C# y Visual Basic .Net. C# es un lenguaje orientado a objetos, que fue diseñado especialmente para la plataforma .Net (incluso gran parte de las clases del framework están escritas en C#). Como su nombre lo indica, C# está basado en el leguaje C, del que se han eliminado algunos componentes para hacerlo más seguro en cuanto al 22
modelo de ejecución y de administración de memoria. Visual Basic .Net es una adaptación para .Net del conocido lenguaje Visual Basic (que a su vez es una adaptación del BASIC original de los años 60) y como tal conserva muchas de las características (y problemas) del leguaje original. Al igual que C#, VB.Net (como también se conoce a Visual Basic .Net) está basado en el paradigma de Orientación a Objetos que estudiaremos en el Capítulo 4.
02_IntrodProg.qxd
23/7/07
20:05
Page 23
› CONCEPTOS DE PROGRAMACIÓN ESTRUCTURADA
pacios ni los saltos de línea) seguidos de caracteres o de números. Generalmente, no se permite que los identificadores comiencen con números. Hay una familia de identificadores que forman parte del lenguaje de programación y que sólo pueden usarse con los fines para los que fueron diseñados. Estos identificadores reciben el nombre de palabra clave. Por ejemplo, el lenguaje C# tiene la palabra clave using, con una semántica determinada, y al ser palabra clave, no podremos usarla en otro contexto. VARIABLES
En cualquier programa que hagamos, necesitaremos manejar datos: el nombre del usuario, el saldo de una cuenta corriente, la cantidad de veces que iteramos sobre una línea de código. Y es natural que estos datos cambien de valor durante la vida del programa. Así, nos encontramos con uno de los conceptos más básicos de la programación, el concepto de variable. Una variable es una referencia a un dato cuyo valor puede cambiar durante la ejecución de un programa. Siendo un poco más formales, podemos decir que una variable es una posición en la memoria de la computadora, donde reside un dato significativo para el programa. Para que resulte más fácil la lectura y la comprensión de los programas, las variables reciben nombres formados por identificadores válidos. Así, por ejemplo,
› LA PLATAFORMA .NET Microsoft .Net es una plataforma para desarrollo y ejecución de software preparada para enfrentar los nuevos desafíos en el mundo de la programación de aplicaciones. Esta plataforma consiste de una familia de lenguajes de programación, una librería de clases común a todos los lenguajes de la plataforma y un entorno de ejecución controlado. Una de las características más importantes de .Net es que el código no se traduce directamente a instrucciones máquina, sino a un lenguaje in-
termedio que luego, durante le ejecución del programa, es traducido a código máquina. Esto brinda la posibilidad de escribir programas independientes de la plataforma de hardware y del sistema operativo. Además, la biblioteca de clases comunes, junto con una especificación común a todos los lenguajes (conocida como CLS o Common Laguage Specification, Especificación de Lenguaje Común), permite una perfecta y fácil interacción entre diferentes lenguajes de la plataforma. 23
02_IntrodProg.qxd
23/7/07
20:05
Page 24
PROGRAMACION ESTRUCTURADA
en el caso de la implementación del algoritmo de Euclides que hemos estudiado, usamos dos variables, denominadas A y B. Es una buena práctica de programación dar nombres significativos a las variables, de manera de hacer evidente la intención que tenemos al momento de definirla. Si en un programa llamamos a una variable c, alguien que lea luego el programa (incluso podríamos ser nosotros mismos) no podrá entender rápidamente cuál es el dato que representa; mientras que si usamos nombreCliente, está claro que, en la ubicación de memoria referenciada por la variable, estamos almacenando el nombre de un cliente.
Figura 2. Según Google Trends, existe una tendencia a usar más el lenguaje C# que VB.Net, además de haber más noticias en Internet sobre C# (fuente www.google.com/trends?q=visual+basic%2Cc%23). TIPOS DE DATOS
Un tipo de dato es el conjunto de valores que toma una variable, junto con las operaciones que pueden realizarse sobre esos valores. En muchos lenguajes, es necesario asociar una variable con un tipo de dato, de manera que el compilador o intérprete pueda validar que las operaciones que hemos escrito realmente corresponden al tipo de dato que manejamos. Un ejemplo de tipo de datos es el número entero. El conjunto de valores que puede tomar es el de los números enteros (0, 1, 2, -1, etcétera), y las operaciones que podemos realizar sobre ellos son la suma, la resta, la multiplicación y la división de enteros. 24
02_IntrodProg.qxd
23/7/07
20:05
Page 25
› CONCEPTOS DE PROGRAMACIÓN ESTRUCTURADA
Los lenguajes que exigen que definamos el tipo de dato de las variables antes de usarlas se denominan fuertemente tipados. Desde el punto de vista de la detección de errores, estos lenguajes presentan una gran ventaja frente a los no tipados ya que permiten detectar, durante la compilación (en el caso de lenguajes compilados), algunos errores muy habituales, como intentar asignar a una variable un dato que no le corresponde. Por ejemplo, si definimos la variable numerador como de tipo entero, pero más adelante le asignamos el texto Hola Mundo, el compilador de un lenguaje fuertemente tipado detectará el problema y nos lo informará antes de compilar. Si el lenguaje no es fuertemente tipado, esto no se detectará y podría ocurrir que, luego, cuando queramos hacer una multiplicación entre la variable numerador y el valor 5, la operación resulte en un error. Al no ser detectado por el compilador, este error surgirá durante la ejecución del programa. En la plataforma .Net, C# es un lenguaje fuertemente tipado, mientras que en VB.Net existe la posibilidad de indicarle al compilador que exija o no la declaración de los tipos de datos de las variables. SENTENCIAS
Las sentencias describen acciones que serán ejecutadas por la computadora como parte del funcionamiento del programa. Éstas se diferencian de las instruc-
› LOS COMENTARIOS EN C# Y EN VB.NET Los comentarios son un caso especial de sentencia no ejecutable que permiten escribir un texto libre con el objetivo de aumentar la legibilidad del código. En cada lenguaje, la forma de escribir comentarios varía, como así también los tipos de comentarios que se pueden escribir (de una sola línea o de muchas líneas). En C#, los comentarios de una sola línea se escriben comenzando con los caracteres // (por ejemplo: //esto es un comentario), mientras que
los comentarios de muchas líneas se escriben comenzando con los caracteres /* y finalizando con */. Entre ambos caracteres, puede haber texto y saltos de línea. En VB.Net, los comentarios de una sola línea se escriben comenzando con la comilla simple (‘), y no existen los comentarios múltiple línea (aunque se los puede simular escribiendo líneas consecutivas de comentarios, todas comenzadas con comilla simple).
25
02_IntrodProg.qxd
23/7/07
20:05
Page 26
PROGRAMACION ESTRUCTURADA
ciones en que una sentencia puede ser traducida por el compilador o por el intérprete en cero o más instrucciones de máquina. Es decir, una sentencia posee mayor abstracción y expresividad que una instrucción. Las sentencias pueden clasificarse en ejecutables y no ejecutables. Las ejecutables son las que se traducen en instrucciones de la máquina y se utilizan para realizar acciones concretas (como sumar dos números o imprimir un texto en la pantalla del monitor). Por el contrario, las sentencias no ejecutables no se traducen a instrucciones, y su única funcionalidad reside en aumentar la legibilidad del código fuente, ya que se pueden usar como comentarios para explicar detalles de la implementación. Al mismo tiempo, las sentencias pueden ser clasificadas en simples y compuestas. Una sentencia simple es una sentencia en sí misma y no contiene a ninguna otra. Por ejemplo, la sentencia de asignación utilizada para asignar un valor a una variable es una sentencia simple, ya que comprende únicamente la acción de asignar el valor a la variable. Por otro lado, las sentencias compuestas están formadas por dos o más sentencias que se ejecutan de acuerdo con alguna estructura de control, tal como veremos más adelante. OPERADORES Y EXPRESIONES
Anteriormente, vimos que un tipo de datos define los posibles valores que puede tomar una variable con el conjunto de operaciones o de acciones que se realizan sobre esos valores. Podemos definir un operador como el símbolo utilizado para indicar una operación sobre elementos llamados operandos. Por ejemplo, para el tipo de dato número entero, está definida la operación de suma, que puede representarse mediante el signo +. Así, si escribimos 2 + 3, indicamos que queremos aplicar la operación de suma a los valores (operandos) 2 y 3. Del mismo modo, si escribimos saldo + 100, mostramos que queremos sumar 100 al valor almacenado en la variable identificada como saldo. Cuando unimos dos o más constantes o variables mediante un operador, definimos una expresión. Las expresiones pueden ser simples o compuestas. Una expresión simple es aquélla sólo formada por un operador con los operandos necesarios. Una expresión compuesta está formada por dos o más operaciones simples. En el caso de las expresiones compuestas, es necesario especificar con exactitud el orden en que se resuelven las expresiones simples que las componen. En el caso de operadores matemáticos, generalmente, el orden en que se resuelven es el 26
02_IntrodProg.qxd
23/7/07
20:05
Page 27
› CONCEPTOS DE PROGRAMACIÓN ESTRUCTURADA
tradicional (es decir, primero el producto y la división, y luego la suma y la resta). Si al momento de escribir la expresión no tenemos del todo claro cuál es el orden de prioridad de los operadores, podemos utilizar paréntesis para alterar o hacer implícito el orden de resolución. TIPOS DE OPERADORES
Para no entrar en detalles muy matemáticos, podemos identificar tres grandes tipos de operadores: los lógicos, los aritméticos y los relacionales. Operadores lógicos
Los operadores lógicos relacionan uno o más valores de verdad (es decir, valores que pueden ser verdadero o falso), y el resultado es otro valor de verdad en función de los operandos. Dentro de los lenguajes de programación, los operadores lógicos se utilizan para evaluar condiciones y modificar así el flujo de control de un programa. Los operadores lógicos más conocidos son no (NOT), o (OR), y (AND). En la Tabla 1, pueden apreciarse los valores devueltos por los operadores lógicos más conocidos (usamos los nombres en inglés para ir familiarizándonos con los lenguajes de programación). En la jerga de la programación llamamos operadores booleanos a los operadores lógicos, por su relación con el álgebra de Boole.
…
OPERADORES BOOLEANOS A F F V V
B F V F V
NOT A V V F F
A AND B F F F V
A OR B F V V V
A XOR B F V V F
Tabla 1. Tabla de verdad de los cuatro operadores lógicos más comunes. Operadores aritméticos
Como hemos visto, los operadores aritméticos se corresponden con las operaciones matemáticas tradicionales: la adición, la sustracción, la potenciación. Operadores relacionales
Los operadores relacionales, tal como su nombre lo indica, establecen una relación entre sus operandos. Un ejemplo clásico de operador relacional es el ope27
02_IntrodProg.qxd
23/7/07
20:05
Page 28
PROGRAMACION ESTRUCTURADA
rador mayor que, notado con el símbolo >. En general, los operadores relacionales devuelven un valor de verdad, por lo que son usados en expresiones lógicas (por ejemplo, el valor de verdad de la expresión 7 > 9 es falso). Figura 3. George Boole fue el precursor en el álgebra de Boole, de donde se tomó el concepto de Operador lógico o booleano. Tipos de operadores
Según la cantidad de operadores con los que trabaja un operador puede clasificarse en unario o binario. Un operador unario se aplica sólo a un operador. El ejemplo más común es el signo -, utilizado para indicar números negativos (por ejemplo, -15). Los operadores binarios poseen dos operandos, como por ejemplo, el signo +. El caso de los operadores binarios puede extenderse y generalizarse para una cantidad cualquiera de operadores, en cuyo caso se denominan operador n-arios, donde n es la cantidad de operandos. Como ejemplo, podemos imaginar un operador 4-ario llamado M que devuelve el máximo entre cuatro números. Una operación con este operador podría escribirse como M 7 2 0 9. Según el lugar donde están los operadores con respecto a los operandos en una operación, un operador puede clasificarse en infijo, prefijo o sufijo. Un operador prefijo se coloca delante de los operadores. El ejemplo que veíamos antes para el signo -, usado como operador unario, es un caso de operador
› EL LENGUAJE PASCAL El lenguaje Pascal fue creado por Niklaus Wirth con el objetivo de lograr un lenguaje fácil de aprender, para usar en sus clases de programación, y lo bautizó en honor a Blaise Pascal, inventor de la primera calculadora mecánica. Pascal es un lenguaje es-
28
tructurado y fuertemente tipado, además de contar con características que hacen posible un buen grado de encapsulamiento. Actualmente, muchas universidades lo siguen utilizando para introducir los conceptos de programación estructurada.
02_IntrodProg.qxd
23/7/07
20:05
Page 29
› CONCEPTOS DE PROGRAMACIÓN ESTRUCTURADA
prefijo. Si el operador se coloca entre los operandos, entonces de denomina infijo (es el tipo más común entre los operadores matemáticos). Por ejemplo, el signo de adición (+) es un operador infijo, ya que se lo escribe entre los operandos (2 + 4). Por último, los operadores sufijos son aquellos que se colocan detrás de los operandos. Un ejemplo no tan conocido es el operador ++ del lenguaje C#. Este operador devuelve el valor del operador y luego le suma uno (podemos escribir por ejemplo: a++). ESTRUCTURAS DE CONTROL
Normalmente, el flujo de ejecución de un programa es secuencial, porque se ejecuta una sentencia tras otra. Las estructuras de control permiten modificar el flujo secuencial, alterando el orden de las sentencias bajo determinadas circunstancias. En los lenguajes estructurados, existen dos estructuras de control básicas: la selección y la repetición o iteración. Según el teorema de Dijkstra, estas dos estructuras, junto con la secuencia, son suficientes para escribir cualquier programa. La estructura de selección
La estructura de selección permite determinar si una sentencia (simple o compuesta) será ejecutada o no a partir de la evaluación de una determinada condición o expresión booleana. En la mayoría de los lenguajes actuales, la selección se realiza mediante el uso de la estructura if (si). La forma más básica de esta estructura es la siguiente (en pseudocódigo): Si (expresión booleana) Entonces ejecutar sentencia A Si no ejecutar sentencia B
La segunda parte (la del si no) es optativa y permite ejecutar una acción alternativa para cuando el resultado de la expresión lógica es falso. En C#, la estructura de selección se escribe mediante la palabra if seguida de la expresión entre paréntesis y debajo la sentencia por ejecutar en caso de que la expresión sea verdadera. En forma opcional, puede haber un else (si no) para especificar una acción en caso de que la expresión lógica sea falsa. 29
02_IntrodProg.qxd
23/7/07
20:05
Page 30
PROGRAMACION ESTRUCTURADA
// C# if( saldo > montoExtraccion ) RealizarExtraccion(montoExtraccion); else MostrarMensaje(“No tiene suficiente saldo”);
En Visual Basic .Net, la estructura de selección es muy similar, aunque con algunas diferencias: la expresión no necesita ir entre paréntesis y debe ser seguida por la palabra reservada THEN (entonces). Además, VB.Net provee la posibilidad de escribir la acción en la misma línea del if o en la línea siguiente. En caso que se escriba debajo, la estructura debe ser cerrada usando end if. ‘ VB.Net If saldo > montoExtraccion Then RealizarExtraccion(montoExtraccion) Else MostrarMensaje(“No tiene suficiente saldo”) End If ‘ Tambien podemos hacer esto: If b > 0 Then c = a / b else c = 0
En muchos lenguajes, existe otra estructura de selección alternativa, denominada comúnmente case o select case, que permite especificar varias alternativas por ca-
› CLASIFICACIONES DE LOS TIPOS DE DATOS Los tipos de datos pueden clasificarse en tres grupos: los ordinales, los no ordinales y los compuestos. Los tipos de datos ordinales son aquellos cuyos valores pueden ser ordenados (se establece una relación de orden entre cualquier par de valores), como por ejemplo, los números enteros o
30
los caracteres de la tabla ASCII. Los valores de los tipos no ordinales no pueden ordenarse, como es el caso de los números decimales de gran precisión o los punteros a memoria. Los tipos de datos compuestos incluyen las cadenas de caracteres y las estructuras de datos.
02_IntrodProg.qxd
23/7/07
20:05
Page 31
› CONCEPTOS DE PROGRAMACIÓN ESTRUCTURADA
sos, según el valor de una variable o de una expresión. Si bien el select case puede escribirse perfectamente como una secuencia de estructuras if, en pseudocódigo, podemos escribir la selección múltiple de esta manera: SegunEsElValorDe expresión hacer: C1: sentencia 1 C2: sentencia 2 … Sino: sentencia n
El funcionamiento de esta estructura es así: se evalúa la expresión, y su valor se compara con la lista de constantes C1, C2, etcétera. Si el valor de la expresión está comprendido en la lista de constantes, se ejecuta la sentencia correspondiente (por ejemplo, si el valor de la expresión es C2, se ejecuta la sentencia 2). Si el valor de la expresión no está en la lista de constantes, y hay un sino, se ejecuta la sentencia asociada a éste, caso contrario, la ejecución continúa en la línea siguiente. En C#, la estructura de selección múltiple cumple la misma función, pero se denomina switch, y su sintaxis es la siguiente: // C# switch(variable) { case 10: HacerAlgo(); break; case 20: HacerOtraCosa(); break; default: HacerAlgoDistinto(); break; }
En Visual Basic .Net, la misma estructura selectiva múltiple se denomina Select Case y se escribe así: 31
02_IntrodProg.qxd
23/7/07
20:05
Page 32
PROGRAMACION ESTRUCTURADA
‘ VB.Net SELECT CASE variable CASE 10: HacerAlgo() CASE 20: HacerOtraCosa() CASE ELSE: HacerAlgoDistinto(); END SELECT
Las estructuras de repetición
Muy a menudo, necesitamos repetir una sentencia más de una vez para completar el objetivo del algoritmo. Las estructuras de repetición de los lenguajes estructurados permiten ejecutar una sentencia una cierta cantidad de veces o mientras se cumpla una determinada condición. Por ejemplo, si volvemos a mirar el algoritmo del Capítulo 1, podremos notar que repetimos las sentencias que hacen las restas entre las dos variables mientras que el valor de la variable A sea mayor que cero. Como decíamos en el párrafo anterior, la mayoría de los lenguajes estructurados proveen dos tipos de estructuras de repetición: una para repetir una sentencia mientras se cumpla una condición (es decir, mientras el resultado de evaluar una expresión lógica sea verdadero), y otra para repetir una sentencia una cantidad determinada de veces. La sentencia que se ejecuta repetidas veces se denomina bucle. La estructura de repetición condicional se denomina comúnmente while (mientras). En el lenguaje C#, simplemente se la escribe seguida de la expresión lógica entre paréntesis y debajo la sentencia a ejecutar: // C# While( a > 0 ) a = a - b;
En Visual Basic .Net, es muy similar, con la diferencia que se debe marcar el final de la estructura con las palabras clave End While, y no es necesario escribir la expresión booleana entre paréntesis: ‘ VB.Net While a > 0 a = a - b End While
32
02_IntrodProg.qxd
23/7/07
20:05
Page 33
› CONCEPTOS DE PROGRAMACIÓN ESTRUCTURADA
En algunos lenguajes, existe una variante de esta estructura de repetición, consistente en evaluar la expresión lógica al final. La diferencia radica en que, al evaluar la condición al final, la sentencia por repetir se ejecutará por lo menos una vez, mientras que, si la evaluación se hace al comienzo, la sentencia podría no ejecutarse nunca. Al momento de usar una estructura while, se debe prestar especial atención a la expresión lógica que determina el fin del bucle (también conocida como condición de corte), ya que escribirla mal, podría hacer que el bucle nunca finalizara (bucle infinito). Por ejemplo, si la condición de corte es que el valor de una variable llegue a cero, pero olvidamos modificarlo dentro del bucle, el valor permanecerá constante, y el ciclo nunca finalizará. La otra estructura de control repetitiva con la que disponemos en la mayoría de los lenguajes estructurados es la que se conoce como For. Esta estructura permite especificar que se quiere repetir la sentencia una cantidad finita de veces, usando una variable como contador. A cada iteración, la variable contador se incrementa automáticamente (es decir, no necesitamos preocuparnos por actualizarla como sucede con while). En algunos lenguajes, como Pascal, no se permite modificar la variable contadora dentro del bucle para facilitar la traducción y validación del código durante la fase de compilación. En C#, la estructura for es un poco difícil de comprender al comienzo, pero una vez que nos familiarizamos con ella, vemos que tiene una gran capacidad de expresividad y de flexibilidad para escribir repeticiones complejas. Al escribir un for en C#, debemos especificar el valor con el que comienza la variable contadora, la condición que determina que el contador ha llegado al valor final y la expresión que incrementa el contador. Esto último parece contradecir lo que mencionábamos antes acerca de que no es necesario preocuparnos por modificar el contador. Y es cierto. En ese aspecto, la estructura for de C# es parecida a la estructura while. Veámosla en un ejemplo: // C# for(i=0;i
[email protected]
03_IntrodProg.qxd
23/7/07
20:41
Page 36
ELEMENTOS DE PROGRAMACIÓN
MODULARIZACIÓN > En nuestro trabajo como programadores, nos encontramos a diario con la
obligación de resolver problemas. A medida que la tecnología avanza y las necesidades de los usuarios son más exigentes, los problemas que debemos resolver se tornan más complejos. Esto hace que la escritura de los programas sea bastante difícil, y que su complejidad los vuelva inaccesibles. Tarde o temprano, la cantidad de variables y elementos que debemos manejar superará los límites normales de la mente humana. Una técnica muy común para facilitar la escritura de programas de gran tamaño o que necesiten resolver problemas muy complejos consiste en dividir el problema en problemas más pequeños. De esta manera, cada dificultad será más fácil de entender y, por lo tanto, resultará más sencillo encontrar un algoritmo que lo resuelva. Así, la construcción del programa se reduce a la construcción de pequeñas unidades o subprogramas que se conectan de alguna manera para resolver juntas el problema original. Esta técnica se conoce como Modularización, y a cada subprograma se lo denomina módulo. Un programa escrito usando la técnica de modularización se resume en un conjunto de subprogramas que se llaman unos a otros. Cada programa incluye una lista de sentencias (de cualquiera de los tres tipos que estudiamos en el capítulo anterior) y, eventualmente, llamadas a otros subprogramas. Cuando hablamos de llamar o invocar a un subprograma, lo que queremos decir es que, desde un subprograma (o incluso desde el programa principal), se deriva la ejecución al subprograma llamado. Por ejemplo, en el siguiente pseudocódigo: Programa Principal: 1
A()
2
B()
3
Fin.
Subprograma A: 3
Imprimir la palabra “Hola”
Subprograma B: 4 36
Imprimir la palabra “Mundo”
03_IntrodProg.qxd
23/7/07
20:41
Page 37
› MODULARIZACIÓN
El programa principal llama o invoca al subprograma A y luego al subprograma B. Y aquí surge uno de los primeros conceptos clave de la modularización: un subprograma o módulo debe ser un algoritmo en sí mismo (de acuerdo con la definición que vimos en el Capítulo 1), es decir, que debe consistir de una secuencia finita de pasos tras la cual finaliza. Entonces, cuando se hace una llamada a un subprograma, éste se ejecuta (o sea, se ejecutan sus sentencias) y, al terminar, el control vuelve al punto desde donde se hizo la invocación. Retornando sobre el ejemplo anterior, concluimos que la secuencia de ejecución será: comienza el programa, se deriva la ejecución al subprograma A, se imprime la palabra Hola, el control vuelve a la línea 2, se deriva el control al subprograma B, se imprime la palabra Mundo, el control vuelve a la línea 3, y el programa termina. PROCEDIMIENTOS Y FUNCIONES
Los módulos se pueden clasificar en procedimientos y en funciones. Las funciones devuelven un único valor al programa o subprograma que hizo la llamada. Los procedimientos, generalmente, no devuelven ningún valor, aunque podrían hacerlo. Las funciones se usan para resolver algún cálculo y devolver el resultado, mientras que los procedimientos se utilizan para realizar una tarea concreta, pero que no involucre devolver nada al programa o subprograma que lo llamó. En la mayoría de los lenguajes de programación, los procedimientos y funciones deben recibir un nombre consistente en un identificador válido según las reglas léxicas del lenguaje. Otra de las ventajas de la división en subprogramas reside en que nos permite usar el mismo grupo de sentencias varias veces en el mismo programa sin necesidad de volver a escribirlas. Sin embargo, lo más probable es que en los distintos lugares donde podamos llamar a un procedimiento o función, los datos
› DIVIDE Y VENCERÁS La técnica de dividir un problema en problemas más pequeños que sean más fáciles de resolver se conoce habitualmente como la técnica de Dividir y vencer, en referencia a pensa-
mientos de Nicolás Maquiavelo. En su libro El Príncipe, Maquiavelo habla sobre generar discordia entre los pueblos, de manera que se separen y así poder dominarlos más fácilmente. 37
03_IntrodProg.qxd
23/7/07
20:41
Page 38
ELEMENTOS DE PROGRAMACIÓN
que necesitamos manejar no sean los mismos. Es necesario, entonces, poder pasarle información adicional al subprograma. Esto se realiza mediante el uso de parámetros o argumentos. Un parámetro es una variable que el procedimiento o función recibe como dato de entrada y que puede usar para cambiar su comportamiento sobre la base del valor recibido (usando, por ejemplo, una sentencia condicional). Los parámetros del argumento pueden ser de entrada o de salida. Los de entrada son los que el programa o módulo que llama le pasa al subprograma, mientras que los parámetros de salida son aquellos que el procedimiento o función utiliza para devolver información a quien lo llamó. Es importante no confundir un parámetro de salida con el resultado de una función: los parámetros de salida pueden ser usados tanto en procedimientos como en funciones, y se puede utilizar la cantidad que sea necesaria, mientras que el valor de resultado sólo se puede usar en las funciones, y no puede haber más de uno por función. Para ilustrar el uso de funciones, retomemos el ejemplo del algoritmo de Euclides y convirtámoslo en una función (en pseudocódigo): Funcion MCM(A,B) Mientras A > 0 hacer: Si A > B A := A – B Si no B := B – A Devolver A
En este caso, la función se llama MCM y posee dos parámetros de entrada, de tipo numérico, A y B. La función MCM consiste de una única sentencia de repetición que hace restas sucesivas entre A y B y, finalmente, devuelve el valor de A como resultado de la función. Para entender el uso de una función, vamos a utilizar la función MCM del ejemplo anterior dentro de un procedimiento: 38
03_IntrodProg.qxd
23/7/07
20:41
Page 39
› MODULARIZACIÓN Procedimiento MostrarMCM() Pedir un numero y guardarlo en V1 Pedir un numero y guardarlo en V2 M := MCM(V1, V2) Mostrar valor de M
En el ejemplo, se pide al usuario que ingrese un número y se lo almacena en la variable V1. Se hace lo mismo con la variable V2. Luego, a la variable M se le asigna el resultado de la función MCM llamada con los valores V1 y V2 (los parámetros A y B del ejemplo). Por último, se muestra el valor de la variable M. Ámbito de variables
El ámbito de una variable es el contexto o porción de programa en el que la variable está definida, es conocida y accesible para ser usada. Si bien cada lenguaje de programación tiene sus propias reglas de ámbito (es decir, las reglas que definen dónde es accesible una variable definida en algún punto de un programa), los subprogramas delimitan el ámbito de las variables. Si el ámbito de una variable incluye sólo un subprograma, entonces se dice que es una variable local (local al procedimiento o función), mientras que si abarca todos los módulos, la variable será global. Una variable local sólo existe mientras se ejecuta el subprograma que la contiene, mientras que una variable global existe durante toda la ejecución, y su valor puede consultarse o modificarse desde cualquier punto del programa o subprogramas. Algunos lenguajes de programación como Pascal permiten escribir programas dentro de subprogramas, lo cual agrega niveles extras de visibilidad o ámbito de las variables, ya que una variable local a un procedimiento puede ser acce-
› USO DE PARÁMETROS DE ENTRADA Y DE SALIDA Si bien un procedimiento puede tener parámetros de salida, esto suele no resultar en una buena práctica de programación. A menos que no podamos hacer otra cosa, si necesitamos devol-
ver un valor desde un subprograma, siempre es mejor usar una función. Esto dará un único punto de salida al subprograma, mejorando la claridad y la legibilidad del código.
39
03_IntrodProg.qxd
23/7/07
20:41
Page 40
ELEMENTOS DE PROGRAMACIÓN
dida desde un procedimiento interno (Figura 1). En C# y Visual Basic .Net, las reglas de ámbito determinan que las variables pueden ser globales dentro de una clase (ya veremos qué es una clase en el siguiente capítulo), o locales a un procedimiento o función.
Programa principal Declarar variable A A:=2 Llamar a B()
Al ejecutar el programa, la variable A tendrá el valor 13, como resultado de la asignación global que se hace en el procedimiento C.
Procedimiento (B) Declarar variable B1
La variable A es global al programa.
B1:=A*2
La variable B1 es local al
Llamar a C()
procedimiento B.
Procedimiento C()
La variable B1 es local al procedimiento
Declarar variable C1
B, pero como el procedimiento C
C1:=B1*3
está dentro de B, la variable B1 se
A:=C1+1
convierte en global para C. La variable
Fin procedimiento C
C1 es local al procedimiento C.
Fin procedimiento B
Figura 1. Algunos lenguajes permiten definir procedimientos dentro de otros, extendiendo el ámbito de las variables locales.
Un punto importante por tener en cuenta es que el ámbito de las variables se ve reducido cuando hay un conflicto de nombres. En los casos en que se produzca este conflicto, el ámbito se resuelve asignando prioridad a la variable que se encuentra definida más adentro en el anidamiento de programa y subprogramas. En estos casos, decimos que la variable de más adentro oculta a la de más afuera. Esto significa que, si tenemos una variable visible desde un procedimiento y dentro de ese procedimiento declaramos una variable con el 40
03_IntrodProg.qxd
23/7/07
20:41
Page 41
› MODULARIZACIÓN
mismo nombre, esta última tendrá prioridad sobre la global, y al asignarle un valor, se asignará a la variable local.
Programa principal
Mediante esta llamada se pasa el valor de
Declarar variables A y B
B como parámetro A al procedimiento P1.
A:=2 B:=3 Llamar a P1(B)
El procedimiento P1 tiene un parámetro
Procedimiento P1(A) Declarar variable B B:=A*2 Imprimir B
llamado A, que por estar en un nivel de anidamiento más interno ”oculta” a la variable A del programa principal. La variable B de P1 oculta a la variable B del programa principal, por lo que el procedimiento P1 imprimirá el valor 6.
Figura 2. En este ejemplo, se puede ver el concepto de ocultamiento con variables y con parámetros. Parámetros por valor y por referencia
Existen dos formas de pasar parámetros a un procedimiento o función: por valor o por referencia. Al pasar un parámetro por valor, tal como su nombre lo indica, lo que le estamos pasando al subprograma es el valor de la variable, es decir, que el subprograma recibe una copia de la variable original con el valor de ella. Al pasar
› ¡CUIDADO CON LAS VARIABLES GLOBALES! Muchas veces las variables globales son tentadoras, porque evitan la necesidad de pasar parámetros de un subprograma a otro para utilizar el valor de una variable. Sin embargo, en la práctica, el uso indiscrimi-
nado de variables globales no es recomendado porque dificulta el mantenimiento del programa, dado que es difícil determinar en qué momento una variable global recibió algún valor.
41
03_IntrodProg.qxd
23/7/07
20:41
Page 42
ELEMENTOS DE PROGRAMACIÓN
un parámetro por referencia, lo que se le pasa al subprograma es la dirección de memoria donde se encuentra el valor de la variable. Es decir, se le pasa una referencia al valor. Dado que se pasa la dirección de memoria donde se encuentra el valor, cualquier asignación que se le haga al parámetro se efectuará en realidad sobre la variable original, ya que se escribe en la misma dirección de memoria. Muchos lenguajes de programación (entre ellos Visual Basic .Net) no tienen el concepto de parámetro de salida como parte del lenguaje, por lo que es necesario utilizar parámetros por referencia para devolver un valor al programa o subprograma que hizo la llamada. Procedimientos y funciones en C# y Visual Basic .Net
Obviamente, los dos lenguajes de .Net provistos por Microsoft proveen mecanismos para definir subprogramas. En C#, el concepto de procedimiento no existe como tal, sino que todo son funciones. Sin embargo, es posible definir procedimientos como funciones que no devuelven un resultado, utilizando el tipo de dato void (que es un tipo de datos nulo). En Visual Basic .Net, los procedimientos se definen mediante la palabra clave Sub. Veámoslo con ejemplos. // C# void HolaMundo() { Console.WriteLine(“Hola Mundo”); } ‘ VB.Net Sub HolaMundo() Console.WriteLine(“Hola Mundo”) End Sub
En el caso de C#, el procedimiento (que en realidad es una función) comienza con el tipo de dato devuelto, void, seguido del nombre del procedimiento con la lista de parámetros entre paréntesis (que en el ejemplo está vacía). La secuencia de sentencias que compone el procedimiento en C# está delimitado por las lla42
03_IntrodProg.qxd
23/7/07
20:41
Page 43
› MODULARIZACIÓN
ves de apertura y cierre (los caracteres { y }). Por otro lado, en VB.Net, la definición del procedimiento comienza con la palabra clave Sub, seguida del nombre del procedimiento y la lista de parámetros. A diferencia de C#, en Visual Basic .Net no se utilizan caracteres para marcar el comienzo de las sentencias del procedimiento, pero sí se utiliza End Sub para marcar el fin. La forma de definir una función en C# es igual que en el ejemplo anterior, reemplazando la palabra void por el tipo de dato que se necesite devolver (por ejemplo, int para enteros). En VB.Net, la definición de una función es sustancialmente diferente, ya que debe utilizarse la palabra reservada Function en lugar de Sub, y se le agrega al final el tipo de dato de salida. Además, como con los procedimientos, en VB.Net hay que marcar el fin de la función con End Function. Como requisito extra, en ambos lenguajes es obligatorio colocar la palabra return para devolver el resultado de la función. // C# int Sumar(int a, int b) { return a + b; } ‘ VB.Net Function Sumar(a, b as Integer) As Integer Return a + b End Function
› LAS DLLS EN WINDOWS El sistema operativo Windows está formado por unas cuantas decenas de DLLs. Un detalle interesante sobre esto es que esas DLLs poseen las funciones y procedimientos que Windows utiliza para hacer todo lo que hace, y están lo suficientemente documenta-
das como para que podamos utilizarlas en nuestros programas cuando necesitemos hacer algo que el lenguaje no tiene previsto. Este conjunto de DLLs se denomina API (Application Programmer Interface o Interfaz para el programador de aplicaciones).
43
03_IntrodProg.qxd
23/7/07
20:41
Page 44
ELEMENTOS DE PROGRAMACIÓN
LIBRERÍAS > Una de las grandes ventajas de la modularización reside en que los módulos pueden utilizarse en distintos lugares. Para ello, resulta muy común organizarlos en librerías. Una librería o biblioteca es un conjunto de subprogramas compilados en un único archivo. En algunos lenguajes (sobre todo los más viejos), al compilar un programa se genera un archivo denominado programa objeto, que es el código máquina generado a partir del código fuente del programa, pero sin incluir el código de las librerías utilizadas. Luego, en un segundo paso denominado enlace (linking) se combina el programa objeto con las librerías para producir un único archivo ejecutable. En los lenguajes de programación que permiten compilar código para ejecutar en el sistema operativo Windows, es muy común el uso de librerías de enlace dinámico (los conocidos archivos DLL, acrónimo de dynamic link library). El enlace dinámico consiste en saltear el paso de enlace durante la compilación para hacerlo durante la ejecución de un programa. Así, mientras un programa se ejecuta, si necesita código que se encuentra en una librería, se coloca ésta en memoria, y se transfiere el control al subprograma necesario dentro de la librería.
Figura 3. En .Net podemos crear librerías de clases, que se comportarán como librerías de enlace dinámico. 44
03_IntrodProg.qxd
23/7/07
20:41
Page 45
› ARREGLOS Y MATRICES
ARREGLOS Y MATRICES > Las estructuras de datos son conjuntos de datos relacionados y organizados de alguna manera (la forma de organizarlos depende justamente de la estructura usada). Desde un punto de vista más formal, las estructuras de datos son tipos de datos (complejos), ya que determinan los valores que pueden contener y las operaciones que se les pueden aplicar. De acuerdo a cómo se administra la memoria para almacenar la información, las estructuras de datos se pueden clasificar en estáticas y dinámicas. Las estructuras estáticas ocupan una cantidad fija de memoria durante toda su vida. El tamaño queda establecido al momento de definir la estructura, es decir, cuando definimos una variable del tipo de la estructura de datos. Las estructuras dinámicas tienen un tamaño inicial, pero pueden crecer durante la ejecución, a medida que se incrementa la cantidad de datos que almacenan. ARREGLOS
Los arreglos o vectores (Arrays) son estructuras de datos que permiten almacenar una colección de datos del mismo tipo, de manera que pueden ser accedidos en forma directa. Dicho de otra manera, los arreglos son una sucesión contigua de n datos (donde n es el tamaño del arreglo), cada uno con un índice determinado por la posición. Así, es posible acceder a un elemento del arreglo mediante su índice. Un arreglo tiene un único nombre, y éste abarca a todos los elementos que contiene.
50
0
A[0] A[1]
80
30
55
27
10
Pérez
tue
A[6]
El arreglo es una secuencia de datos contiguos en memoria.
Luego del arreglo pueden seguir datos de cualquier tipo.
Figura 4. Los elementos de un arreglo ocupan posiciones contiguas de memoria. 45
03_IntrodProg.qxd
23/7/07
20:41
Page 46
ELEMENTOS DE PROGRAMACIÓN
Normalmente, los arreglos son estructuras estáticas de datos, pero algunos lenguajes de programación permiten cambiarles el tamaño mientras se ejecutan. Para entender la utilidad de los arreglos, pensemos que necesitamos almacenar el registro de lluvias de cada mes para una ciudad. Sin usar arreglos, deberíamos tener doce variables para contener el registro pluvial de los doce meses del año. Esta alternativa presenta algunas desventajas: es tediosa para programar, ya que hay que declarar cada una de las variables con un nombre diferente. Además, se hace imposible recorrerlas mediante una estructura de repetición. Por último, si queremos hacer que el usuario ingrese el número de mes y mostrar las lluvias de ese mes, deberíamos hacer un select case para mostrar la variable correspondiente al mes ingresado. Si en cambio utilizamos un arreglo, tendremos los doce valores bajo un mismo nombre de variable y podremos acceder al registro pluvial del n-ésimo mes directamente leyendo el elemento del arreglo ubicado en la posición n. Supongamos que la variable se denomina lluvias; entonces, podemos escribir un algoritmo como el que sigue para mostrar el registro de lluvias de un mes seleccionado por el usuario: // Pseudocódigo: Procedimiento MostrarRegistroPluvial Leer un entero y guardarlo en la variable mes Mostrar el valor de lluvias[mes]
Aquí vemos una notación sintáctica que hasta ahora no habíamos visto: lluvias[mes]. Esta expresión significa: el dato que se encuentra en la posición mes
› EL PRIMER ELEMENTO Cada lenguaje de programación tiene diferentes posturas a la hora de definir las dimensiones de un arreglo, como así también de definir cuál es el índice del primer elemento. Lenguajes como Delphi permiten definir los índices de un arreglo comenzando con cualquier número
46
(por ejemplo, podemos indicar que los índices van del 50 al 100). Otros lenguajes permiten definir el tamaño del arreglo, y el primer elemento queda fijado por una convención de diseño. En .Net, todos los índices de arreglos y colecciones comienzan siempre en cero.
03_IntrodProg.qxd
23/7/07
20:41
Page 47
› ARREGLOS Y MATRICES
dentro del arreglo (el valor que está dentro de los corchetes representa el índice al que se quiere acceder). En el ejemplo, hemos usado una variable como índice del arreglo, pero se puede usar también un valor constante, por ejemplo lluvias[2] para acceder a las lluvias de febrero. Arreglos en .Net
Obviamente, los lenguajes de .Net proveen soporte para la creación y uso de arreglos. En C#, los arreglos son estáticos, mientras que, en Visual Basic .Net, pueden ser dinámicos. En ambos lenguajes, los arreglos se definen declarando su tamaño y el tipo de los elementos. El primer índice es siempre el cero. // C# - Defino un arreglo de 2 enteros int[] arreglo = new int[2]; arreglo[0] = 1; arreglo[1] = 20; ‘ VB.Net – Defino un arreglo de 2 enteros Dim arreglo(2) as Integer Arreglo(0) = 1 Arreglo(1) = 2
MATRICES
Si bien las matrices tienen un origen en la matemática, en el contexto de la programación son estructuras de datos que permiten organizar la información en filas y columnas. Cada elemento de una matriz puede ser accedido por un par de índices (la fila y la columna). Al igual que en los arreglos, los elementos de una matriz deben ser todos del mismo tipo de datos.
› ESTRUCTURAS DE DATOS MÁS COMPLEJAS Los arreglos y matrices son estructuras de datos básicas. En muchas ocasiones, necesitamos trabajar con estructuras más complejas. En .Net, disponemos de tipos de datos para manejar estructuras como listas, pilas o co-
las, entre otras. Una pila es similar a un arreglo, en donde sólo podemos colocar elementos al principio y sacar del mismo lugar. En una cola, sólo podemos ubicar elementos al final y quitar elementos del comienzo.
47
03_IntrodProg.qxd
23/7/07
20:41
Page 48
ELEMENTOS DE PROGRAMACIÓN
7
0
49
18
3
4
99
2
14
1
0
17
Fila
Elemento (3,4)
Columna
Figura 5. Una matriz está organizada en filas y columnas. Cada elemento se accede mediante un índice compuesto por el número de fila y por el número de columna.
Desde el punto de vista de su almacenamiento en memoria, la matriz puede ser vista como un arreglo de filas, donde cada fila es un arreglo de elementos. Visto de esta manera, una matriz es, en definitiva, un arreglo de elementos de tipo de dato arreglo. Es decir, un arreglo bidimensional. A la hora de recorrer una matriz, se debe prestar atención a no confundir los índices de los elementos, teniendo bien presente en qué columna y en qué fila está cada uno de los que necesitamos acceder. Dada su estructura de dos dimensiones, la forma de recorrer una matriz es mediante dos estructuras repetitivas anidadas, una para recorrer las filas y la otra para recorrer las columnas. Dependiendo del orden en que se quiera hacer el recorrido, se iterará primero sobre las filas y luego sobre las columnas, o primero sobre las columnas y luego sobre las filas. Para saber si hemos implementado correctamente el recorrido, podemos tomar una matriz de ejemplo y se-
› FXCOP Luego del surgimiento de .Net, Microsoft publicó documentos con estándares de codificación y buenas prácticas de programación, con el objetivo de que todos los programadores escribieran código de la misma manera. Existe un programa llamado FxCop, dispo-
48
nible en www.gotdotnet.com/Team/ FxCop/. Este programa analiza ensamblados ya compilados de .Net y genera una lista con las violaciones a los estándares definidos por Microsoft. Actualmente, FxCop fue incorporado en algunas ediciones de Visual Studio.
03_IntrodProg.qxd
23/7/07
20:41
Page 49
› ARREGLOS Y MATRICES
guir con lápiz y papel cada uno de los pasos del algoritmo, verificando el resultado obtenido (esto se conoce como Prueba de Escritorio). Matrices en .Net
La definición de matrices en los lenguajes de .Net que estamos estudiando resulta muy similar a la definición de arreglos, con el agregado de la segunda dimensión. // C# - Defino una matriz de 2 filas y 3 columnas int[,] matriz = new int[2,3]; matriz [0,1] = 1; matriz [0,2] = 20; ‘ VB.Net – Defino una matriz de 2 filas y 3 columnas Dim matriz (2,3) as Integer matriz (0,1) = 1 matriz (0,2) = 2
En C#, existe una forma alternativa de definir las matrices, basada en la idea de una matriz como un arreglo de arreglos: // C# - Defino una matriz de 2 filas y 3 columnas int[][] mat = new int[2][]{new int[3],new int[3]}; mat [0][1] = 1; mat [0][2] = 20;
› MÁS ALLÁ DE LAS DOS DIMENSIONES Como vimos, una matriz es un arreglo de arreglos, lo cual puede verse como un arreglo de dos dimensiones. Los lenguajes de programación permiten definir arreglos con la cantidad de dimensiones que necesitemos. Por ejemplo, si necesitamos
almacenar los registros de lluvia de cada mes, de los últimos 20 años para 10 ciudades distintas, podemos definir un arreglo de 10x20x12 (10 ciudades por 20 años por 12 meses al año). En este caso, la estructura se conoce como Cubo.
49
03_IntrodProg.qxd
23/7/07
20:41
Page 50
ELEMENTOS DE PROGRAMACIÓN
EL ESTILO DE PROGRAMACIÓN > Una de las premisas fundamentales de la programación consiste en saber que
los programas no se escriben una vez y quedan así para siempre. Cuando se escribe un programa, es normal tener que volver sobre sus instrucciones para hacer una modificación o corregir un error. Y es común que la persona que deba modificar el programa no sea la misma que lo escribió. Sin embargo, dada la complejidad inherente a todo programa, leer el código fuente ya escrito y tratar de entenderlo para hacer la modificación que necesitamos, puede ser toda una odisea. Para que la tarea de mantenimiento del software sea un poco más sencilla, es necesario que, como programadores, tengamos el hábito de escribir el código de la manera más prolija posible y que logremos un estilo estándar. Para lograr un buen estilo de programación, debemos tener en cuenta algunos consejos. TABULACIONES
Las sentencias compuestas pueden estar formadas a su vez por otras sentencias compuestas, así se obtienen anidamientos en el código del programa. Para poder identificar dónde comienza y dónde termina cada nivel de anidamiento, es muy útil tabular hacia la derecha cada sentencia que está dentro de otra (es decir, cada nuevo nivel de anidamiento). Veamos la diferencia entre tabular y no tabular con un ejemplo:
› ARREGLOS
› MODULARIZACIÓN Y OVERLAYS
Visual Basic .Net permite cambiar el tamaño de los arreglos. Para ello, provee la palabra clave Redim. Hay que tener en cuenta, sin embargo, que el Redim destruye el arreglo original y crea uno nuevo, con la consiguiente pérdida de los valores que contenía. Para evitarlo, se puede utilizar Redim Preserve, que luego de crear el arreglo, copia los elementos del viejo.
En épocas en que la memoria de las computadoras era escasa, muchas veces cargar todo el programa era imposible. La solución consistía en colocar los módulos en archivos separados llamados overlays. Cuando se necesitaba ejecutar un módulo que estaba en un overlay, se descargaba otro de memoria y se cargaba el que tenía el módulo necesario.
50
03_IntrodProg.qxd
23/7/07
20:41
Page 51
› EL ESTILO DE PROGRAMACIÓN // C# - Forma incorrecta de anidar sentencias if(a>0) { for(int i=1;i Cuando se analiza un problema para resolverlo mediante las técnicas de programación estructurada, nos enfocamos en identificar los datos que debemos manipular y las transformaciones que sufren como parte de la solución. Con el paradigma de orientación a objetos, la tarea de análisis se centrará en la identificación de objetos, de sus características y de cómo se relacionan entre sí para resolver el problema. Ya dentro del contexto formal de la teoría de Orientación a Objetos, podemos definir un objeto (parafraseando la definición de James Rumbaugh) como un concepto, abstracción o elemento con significado claro dentro del problema en cuestión (por ejemplo, un empleado). Como tal, un objeto se caracteriza por tener un estado, que es el conjunto de valores de sus propiedades en un momento del tiempo (por ejemplo, el nombre de un empleado, o el monto del sueldo que percibe). Además, todo objeto tiene un comportamiento, es decir, las acciones que puede realizar y que modificarían su estado (por ejemplo, un empleado puede ascender o completar tareas). Por último, un objeto se caracteriza por tener identidad propia, esto es, por más que dos objetos tengan el mismo comportamiento y el mismo estado, resultan totalmente diferentes e identificables. Según el diccionario, una clase es un grupo de elementos de un conjunto que tienen características comunes. Las técnicas de orientación a objetos se centran en la identificación de esos elementos comunes entre los objetos para poder agruparlos en clases y, así, manipularlos fácilmente. Podemos decir que una clase es una abstracción de un grupo de objetos, porque no existe por sí sola. Por ejemplo, pensemos en una mesa. Hay mesas rectangulares, mesas redondas, de madera, de metal, incluso de diferentes colores. Todas son mesas distintas e identificables, es decir: objetos. Sin embargo, podemos verificar que todas tienen los mis-
› CLASES E INSTANCIAS En el mundo de la orientación a objetos, hay dos términos muy usados: clases e instancias. Como vimos, una clase es una abstracción de un objeto de la realidad. La clase es un modelo 58
estático, por lo tanto, para poder interactuar con otros elementos, debemos crear instancias que representen a cada uno de los objetos particulares que, agrupados, forman la clase.
04_IntrodProg.qxd
23/7/07
20:47
Page 59
› CLASES Y OBJETOS
mos atributos, aunque con distintos valores. Todas tienen un color, un material, una forma; por lo tanto podemos agruparlas bajo un mismo concepto: la mesa. Desde el punto de vista de la orientación a objetos, podemos decir que mesa es una clase. Clase Mesa Color Material Cantidad de patas Objeto 1
Objeto 2
Figura 2. Una clase es una abstracción de las principales propiedades de los objetos reales. PROPIEDADES Y MÉTODOS
Como decíamos, los objetos poseen atributos y comportamiento. Ya desde el punto de vista de la programación, los atributos de los objetos se traducen en propiedades de las clases que los modelan. El comportamiento en cambio, está representado por procedimientos y funciones dentro de la clase. En la jerga de la programación orientada a objetos, los procedimientos y funciones que creamos dentro de una clase se denominan métodos, y representan el conjunto de actividades que un objeto puede realizar, es decir, su comportamiento. Los libros más puristas dicen que los objetos se relacionan intercambiando mensajes, por lo que el comportamiento de un objeto está determinado por los mensajes que puede enviar y por los que puede aceptar desde otros objetos. Volviendo a la definición de método, justamente, podemos decir que los métodos son los encargados de interceptar y de enviar mensajes desde y hacia otros objetos y, al mismo tiempo, alterar el estado actual del objeto. 59
04_IntrodProg.qxd
23/7/07
20:47
Page 60
PROGRAMACIÓN ORIENTADA A OBJETOS
PENSAR EN OBJETOS > La forma de solucionar problemas utilizando técnicas de Orientación a Objetos es muy distinta de la forma tradicional o estructurada. Cuando nos presentan un requerimiento de una nueva aplicación y queremos emplear los conceptos de la Programación Orientada a Objetos, como primer paso nos concentraremos en identificar los objetos que intervienen en el problema. Deberemos, entonces, analizar cuidadosamente el requerimiento, hablar con los usuarios y prestar especial atención a los elementos que mencionan. Una técnica muy usada consiste en leer el requerimiento escrito (que deberá ser lo más claro y completo posible) y subrayar todos los sustantivos, tanto concretos como abstractos. Luego, de entre todos los sustantivos subrayados, descartamos aquellos que no son propios del problema por resolver. Una vez realizada esta primera actividad, habremos identificado una buena parte de los objetos del dominio de la aplicación. Por ejemplo, imaginemos que necesitamos implementar una aplicación de alquiler de autos, y nos presentan el siguiente requerimiento (resumido): La empresa dispone de autos para alquilar a sus clientes. Cuando se alquila un auto, el cliente firma una póliza de seguro. En un formulario de alquiler, se registra la fecha, el nombre del cliente, el número de su registro de conductor y el número de tarjeta de crédito. Si aplicamos la técnica de los sustantivos, podremos identificar rápidamente tres objetos fundamentales para la aplicación: auto, cliente y formulario de alquiler. Dependiendo del tipo de análisis y diseño que queramos hacer, podríamos considerar como objetos la tarjeta de crédito e, incluso, el alquiler. Como decíamos al principio, los objetos poseen propiedades y comportamiento. Para identificar las propiedades aplicamos una técnica similar a la de los sustanti-
› TOMARLO CON CALMA Muchas veces, no lograremos identificar con claridad todos los objetos en una primera lectura. Será necesario hacer una segunda pasada (incluso más) sobre el texto del reque60
rimiento para encontrar objetos ocultos, despejar ambigüedades o eliminar falsos objetos que no son esenciales para la solución concreta del problema.
04_IntrodProg.qxd
23/7/07
20:47
Page 61
› PENSAR EN OBJETOS
vos, pero buscando cualidades o adjetivos de los objetos que ya hemos identificado. Siguiendo con el ejemplo del alquiler de autos, podemos ver que el formulario de alquiler tiene propiedades como la fecha, el nombre del cliente, etcétera. Una vez que hemos identificado los objetos y sus propiedades, estamos en condiciones de definir clases para agruparlos y abstraer todas las posibles instancias de ellos. Hemos alcanzado así el fin de la primera etapa en el proceso de solución de problemas mediante técnicas de orientación a objetos.
Autos
Auto Color Marca Año
Clientes
Cliente Nombre Número de Registro
Formularios
Formulario Fecha Cliente Nº de póliza
Figura 3. Con las técnicas de análisis, identificamos los objetos del mundo real; luego abstraemos sus propiedades para agruparlos en clases. PATRONES DE DISEÑO
Si bien el proceso de análisis y diseño orientado a objetos se centra en la identificación de los objetos propios del dominio del problema, a menudo es necesario crear objetos ficticios. Éstos colaboran con los objetos reales en la solución del problema o en proveer mecanismos de flexibilidad, extensibilidad o claridad del 61
04_IntrodProg.qxd
23/7/07
20:47
Page 62
PROGRAMACIÓN ORIENTADA A OBJETOS
código, y ayudan a separar los intereses y responsabilidades de cada objeto. Un ejemplo muy común de estos escenarios son los requisitos no funcionales. Para entender un poco mejor este aspecto, retomemos el ejemplo del sistema de alquiler de autos e imaginemos que se nos exige que el sistema valide la tarjeta de crédito, pero que además el código esté preparado para adaptar el sistema a nuevos mecanismos de validación. Una posible solución es definir una clase llamada ValidadorTarjetaCredito, con un método que reciba un número de tarjeta y un importe, y valide que se puede cobrar ese importe a esa tarjeta. Luego, utilizando la herencia y el polimorfismo (que veremos luego), se pueden crear nuevas clases que empleen diversos mecanismos de validación. A lo largo de los años, los desarrolladores reconocieron que muchas de las soluciones que encontraban a los problemas no funcionales con orientación a objetos comenzaban a repetirse o tenían elementos en común y que, además, funcionaban (es decir, cumplían su propósito). Actualmente, a estas soluciones probadas a problemas comunes, se las llama patrones. Hay distintas clasificaciones de patrones según la familia de problemas que atacan, siendo los más comunes los denominados Patrones de diseño. El principal objetivo de los Patrones de diseño es el de proveer un catálogo de elementos de diseño que ayuden a los programadores en su tarea diaria, evitando que tengan que buscar nuevas soluciones cada vez que se enfrentan con un problema común. Además, los Patrones de diseño facilitan la comunicación entre los programadores, como así también la comprensión del código escrito por otro. Obviamente, los patrones no pretenden eliminar el trabajo del desarrollador, ya que éste debe ser capaz de identificar las características de cada problema y tener un dominio de los principales patrones para asociarlos con el problema y aplicarlos.
› LA BANDA DE LOS CUATRO El tema de los patrones ha generado gran cantidad de bibliografía. Quizá el libro más popular a la hora de aprender sobre patrones es Design Patterns. Elements of Reusable ObjectOriented Software. Los patrones que 62
en él se presentan, se conocen como patrones GoF, sigla de Gang of Four (la Banda de los Cuatro), en alusión a sus cuatro autores, autoridades absolutas en este ámbito (Gamma, Helm, Jonson y Vlissides).
04_IntrodProg.qxd
23/7/07
20:47
Page 63
› RELACIONES ENTRE CLASES
RELACIONES ENTRE CLASES > En el mundo real, y dentro del contexto de una aplicación que debemos implementar, los objetos no están solos. Todo objeto se relaciona en cierta medida con alguno de los otros objetos (sea de la misma clase o no). En el ejemplo de alquiler de autos que estudiamos anteriormente, los clientes (que son objetos) se relacionan con los autos (que también son objetos) mediante la operación de alquiler. Del mismo modo, los formularios de alquiler se relacionan con los autos y con los clientes. Una buena regla adicional para la identificación de objetos a partir de un requerimiento es que, si un objeto no se relaciona con ningún otro, es probable que no sea necesario tenerlo en el sistema o deba ser revisado para ver si no nos faltó considerar algún aspecto del requerimiento. Un objeto puede relacionarse con otro de distintas maneras, como por ejemplo, enviándose mensajes entre sí. Durante el análisis, una vez que determinamos los objetos y definimos las clases, debemos prestar atención a las relaciones entre los objetos, ya que deberán ser modeladas para que luego se encuentren disponibles en cada instancia. Entre las relaciones más importantes, podemos mencionar: uso, agregación y herencia. Veamos las dos primeras ahora y dejemos la herencia para después, ya que es muy importante y merece un tratamiento aparte. RELACIÓN DE USO
En una relación de uso, un objeto A usa un objeto B, en el sentido de utilizar algún servicio provisto por B. En cualquier aplicación, es muy común encontrar que un objeto necesita algún servicio de otro, como pedirle que haga una determinada tarea. Si retomamos el ejemplo de la validación de tarjetas de crédito que
› POO PURA Muchos puristas de la orientación a objetos sostienen que Smalltalk y Eiffel son de los pocos lenguajes realmente orientados a objetos. El argumento se basa en que otros lenguajes (como los presentes en la
plataforma .Net) poseen aún muchos conceptos de la programación estructurada (como por ejemplo las estructuras de control) y eso atenta contra la definición formal de orientación a objetos. 63
04_IntrodProg.qxd
23/7/07
20:47
Page 64
PROGRAMACIÓN ORIENTADA A OBJETOS
vimos cuando tratamos el tema de los patrones, podemos observar que el objeto ValidadorTarjetaCredito brinda un servicio que alguien más, por ejemplo un objeto RegistradorDeAlquiler puede usar para completar su función. En la bibliografía de POO, se suele llamar relación Usa-A a la relación de uso
Hacer Tarea Objeto B
Objeto A
Tarea lista
Figura 4. Cuando un objeto A solicita una tarea a un objeto B, se dice que el objeto A usa el objeto B. RELACIÓN DE AGREGACIÓN
Muchas veces, un objeto se compone de otros. Este tipo de relación se denomina agregación. En una relación de agregación entonces, un grupo de objetos (de la misma clase o de clases distintas) se agrupan para formar un objeto más complejo. Por ejemplo, un carrito de compras se compone de uno o más productos que el cliente ha comprado. Si bien los objetos producto son independientes, el objeto carrito necesita de ellos para existir. La relación de agregación puede ser
› RELACIONES Desde el punto de vista de la implementación, tanto las relaciones de agregación como las de composición suelen representarse como propiedades o atributos de las cla64
ses agregadas o compuestas. Por ejemplo, la clase Carrito puede tener una propiedad que sea la lista de objetos de la clase Producto que ella contiene.
04_IntrodProg.qxd
23/7/07
20:47
Page 65
› RELACIONES ENTRE CLASES
recursiva, es decir, un objeto que está compuesto por otros, a su vez, puede ser parte de un objeto más grande, definiendo así una estructura jerárquica. Por ejemplo, una universidad puede estar compuesta de facultades, cada facultad se compone de departamentos, y cada departamento, de profesores y de alumnos. Composición
La composición es un caso especial de agregación en el que los objetos agregados a otro, sólo pertenecen a él. Por ejemplo, un automóvil está compuesto de un motor, un chasis, una carrocería y otros elementos, pero cada uno de ellos sólo puede pertenecer a ese auto. Un motor que está en un auto no puede estar en otro. La distinción entre agregación y composición depende directamente del problema por resolver y, en un buen modelo de objetos, debería reflejar exactamente la realidad. Objeto a
Objeto b1 Objeto c1 Objeto c2 Objeto b2 Objeto c3
Figura 5. En una relación de agregación, los objetos se agrupan para formar un objeto más complejo.
› MÉTODOS Y PROPIEDADES DE CLASE Normalmente, los métodos y propiedades corresponden a cada instancia de una clase (a cada objeto). Sin embargo, muchas veces hay características que son comunes a to-
das las instancias. En estos casos, podemos definir métodos o propiedades compartidos, que pertenecen a la clase y se pueden invocar directamente sobre ella. 65
04_IntrodProg.qxd
23/7/07
20:47
Page 66
PROGRAMACIÓN ORIENTADA A OBJETOS
HERENCIA > La herencia es un tipo muy importante de relación entre objetos. En una re-
lación de herencia, una clase recibe las propiedades y comportamiento de otra como si fuesen suyas y, al mismo tiempo, puede agregar las propias. Veamos esto con un ejemplo. Supongamos que estamos desarrollando una aplicación que necesita hacer cálculos de superficie sobre distintas figuras geométricas, es decir, vamos a tener objetos como Círculo, Cuadrado, Trapecio, etcétera. Dado el requerimiento que tenemos, todos los objetos presentarán un comportamiento en común: calcular su superficie. Lo que podemos hacer entonces es definir una clase llamada Figura y establecer una relación de herencia entre esta clase y cada una de las figuras particulares. Le asignamos el método de cálculo de superficie a la clase Figura y, mediante la relación de herencia, las demás clases tendrán este comportamiento como heredado. Decimos entonces que las clases Cuadrado, Trapecio, etcétera, heredan de la clase Figura. Así como el uso define una relación usa-a, y la composición define una relación se-compone-de, la herencia se conoce como una relación es-un. Siguiendo con el ejemplo de las figuras, decimos que un círculo es una figura, un cuadrado es una figura, y así con todos. Cuando una clase B hereda de una clase A, se dice que la clase A es la clase padre o clase base, y la clase B es la clase hija, heredera o derivada. En el ejemplo anterior, entonces, la clase Figura es la clase base, y las demás, sus clases derivadas. La relación de herencia no se limita a un solo nivel, es decir, que una clase derivada de otra puede a su vez ser la base de una tercera clase, formando así una estructura jerárquica o de árbol. Además, en escenarios de múltiples niveles, la relación de
› DISEÑAR PENSANDO EN OBJETOS La herencia tiene una gran potencia para escribir código reutilizable, flexible y mantenible. Por eso, durante el análisis del problema y durante el posterior diseño de las clases, se debe prestar especial atención a 66
identificar relaciones de herencia para aprovechar sus virtudes. Conocer algunos patrones de diseño puede ayudar también a identificar relaciones de herencia, aun en clases no concretas.
04_IntrodProg.qxd
23/7/07
20:47
Page 67
› HERENCIA
herencia es transitiva, esto es, si una clase C hereda de B, que a su vez hereda de A, todo el comportamiento y las propiedades de A pertenecen también a C. A partir del ejemplo anterior, podemos apreciar que la relación de herencia provee un mecanismo para lograr uno de los objetivos más importantes de la programación orientada a objetos, la reutilización de código. ¿Cómo se entiende esto? Supongamos que de la clase Figura hereda una clase Cuadrilátero, que tiene propiedades para representar la longitud de cada uno de los lados. Si de la clase Cuadrilátero derivan Rombo, Trapecio y Rectángulo, en estas nuevas clases ya no tendremos que escribir el código de la propiedad, es decir, hemos reutilizado código que ya estaba escrito.
Clase Figura CalcularSuperficie()
Clase Cuadrilatero
Clase Circulo
LongitudLadoA LongitudLadoB LongitudLadoC LongitudLadoD
Radio
Clase Cuadrado
Clase Rombo
Figura 6. Mediante la herencia, podemos definir una jerarquía de clases, donde cada clase derivada hereda el comportamiento y las propiedades de su clase base.
Además de la reutilización de código, la herencia provee otras aplicaciones útiles, como son la extensión y la redefinición de clases. Veamos de qué se trata. 67
04_IntrodProg.qxd
23/7/07
20:47
Page 68
PROGRAMACIÓN ORIENTADA A OBJETOS
REDEFINIR COMPORTAMIENTO
Es muy común que una clase que hereda de otra no se comporte exactamente igual, aun para el comportamiento heredado. Veamos un ejemplo real: entre las herramientas que provee el Framework .Net para crear aplicaciones basadas en ventanas, tenemos clases para crear componentes visuales, como botones y listas. Supongamos que debemos crear botones, pero que sean redondos, y no rectangulares como los botones normales. Si no tuviéramos la herencia, deberíamos crear una clase BotonRedondo y escribir absolutamente todo el código. Sin embargo, podemos hacer que nuestra clase herede de la clase Button de .Net y redefina un método llamado OnPaint (que es el encargado de dibujar el botón en la pantalla) para dibujarlo como necesitamos. Tendremos así un nuevo tipo de botón que se dibuja como queremos, pero que posee todo el comportamiento y las propiedades de cualquier otro botón (por ejemplo, reacciona ante un clic con el ratón).
Clase Boton Dibujar: Dibuja un rectángulo y coloca el texto del botón AtenderClick()
Clase BotonRedondo Dibujar: Dibuja un circulo y coloca el texto del botón
La Clase BotonRedondo funciona exactamente igual que la clase Boton pero se dibuja distinto.
Figura 7. Una clase puede redefinir un método heredado para personalizarlo según su propio comportamiento. 68
04_IntrodProg.qxd
23/7/07
20:47
Page 69
› HERENCIA
EXTENDER CLASES
La extensión es un mecanismo para agregar comportamiento y propiedades a una clase, es decir, a todas las características que se heredan de la clase padre, se le agregan otras propias de la clase derivada. Por ejemplo, si tenemos una clase Persona y de ella derivamos una clase Empleado, podemos aplicar extensión agregando las propiedades Jefe y Sueldo. Si miramos este ejemplo pensando en la relación es-un, podemos decir que un Empleado es-una persona que además posee un sueldo y un jefe, es decir, es una extensión. La extensión es una de las aplicaciones más utilizadas y naturales de la herencia, ya que sienta las bases de un buen diseño que derive en código claro, mantenible y robusto. La extensión se puede combinar con la redefinición, para reutilizar aun más el código heredado. Esto es, podemos redefinir un método de manera tal que se comporte exactamente igual al método heredado del padre, pero que además haga algo extra. Volviendo al ejemplo del Botón, imaginemos que queremos hacer un botón tal que, cuando el cursor del ratón pase por encima, cambie de color. Deberemos entonces definir una nueva clase, que herede de Button y redefinir el método OnPaint, pero en vez de escribirlo todo desde cero, llamamos al método de la clase padre para dibujar el botón, y luego, si el cursor está sobre el rectángulo que comprende el botón, le cambiamos el color. TIPOS DE HERENCIA
Hasta hora vimos ejemplos de herencia donde una clase derivada hereda sólo de una clase base. Sin embargo, en la práctica pueden presentarse casos en que una clase herede de más de una clase padre. Por ejemplo, imaginemos que tenemos una clase Empleado, con una propiedad Sueldo, y una clase Músico con una pro-
› HERENCIA MÚLTIPLE La mayoría de los lenguajes actuales no soportan herencia múltiple, ya que en general este tipo de herencia presenta más problemas que soluciones. Uno de los problemas de la herencia múltiple es cómo re-
solver casos en que un método o propiedad existe en más de una clase base, entonces, ¿cuál es el que recibe la clase derivada? Tanto los lenguajes de .Net como Java no permiten herencia múltiple. 69
04_IntrodProg.qxd
23/7/07
20:47
Page 70
PROGRAMACIÓN ORIENTADA A OBJETOS
piedad InstrumentoQueToca y queremos definir una clase EmpleadoDeOrquesta. Como los empleados de orquestas son músicos (tocan un instrumento) y además son empleados (perciben un sueldo), podemos heredar de ambas clases para reutilizar el código de cada una. Este tipo de herencia se denomina Herencia Múltiple. CLASES ABSTRACTAS
En el ejemplo de las figuras geométricas que vimos al comienzo, definimos una clase Figura con un método CalcularSuperficie. Ahora bien, esa clase, es una abstracción de todas las posibles figuras geométricas y, por lo tanto, no le podemos definir el cálculo de superficie, ya que depende de cada tipo de figura. Afortunadamente, para resolver cuestiones como ésta desde el diseño, tenemos la posibilidad de crear métodos abstractos: métodos que están declarados, pero que no tienen implementación. La implementación de un método abstracto queda relegada a las clases derivadas. Una clase que contiene al menos un método abstracto se denomina abstracta. INTERFACES
Las interfaces son un derivado de las clases abstractas. En particular, una interfaz es esencialmente igual a una clase con todos sus métodos abstractos. Una clase que herede de una interfaz (en realidad para hablar correctamente debemos decir que la clase implementa una interfaz) está obligada a implementar
› CLASE ABSTRACTA O INTERFAZ › INSTANCIA Una decisión que muchas veces tendremos que tomar es si optar por una clase abstracta o por una interfaz. La diferencia radica en dos puntos: si usamos una clase abstracta, tendremos la posibilidad de escribir algo de código (métodos no abstractos), para no tener que escribirlo en cada clase descendiente. Con las interfaces, tendremos abierta la posibilidad de heredar de otra clase. 70
En .Net, cuando definimos una variable que es una instancia de una clase, en realidad, lo que estamos guardando en la variable no es el objeto completo sino la dirección de memoria (en el HEAP) donde se encuentra el objeto. Cuando pasamos un parámetro por valor, lo hacemos por referencia, y cualquier modificación del parámetro, la haremos sobre el objeto original.
04_IntrodProg.qxd
23/7/07
20:47
Page 71
› HERENCIA
todos sus métodos y propiedades. Las interfaces se utilizan para proveer una abstracción de algún comportamiento. Además, los lenguajes modernos como Java y los lenguajes de .Net permiten que una clase implemente varias interfaces, proveyendo así una especie de herencia múltiple.
Clase Figura
Interfaz IDibujable
CalcularSuperficie()
Dibujar()
Clase FiguraDibujable Dibujar()
La Clase FiguraDibujable es una figura, pero también tiene la capacidad de dibujarse al implementar la interfaz IDibujable.
Figura 8. Las interfaces permiten abstraer comportamientos y, a la vez, heredar de otra clase, proveyendo un tipo de herencia múltiple.
› PROGRAMADOR .NET Un completísimo portal con información para programadores. Actualmente, hay más de 3500 recursos entre artículos y ejemplos de código. Los artículos y ejemplos cubren la mayoría de los temas y de los lenguajes actuales que todo programador debe conocer. Está organizado en categorías, por lo que resulta sencillo encontrar lo que buscamos. 71
04_IntrodProg.qxd
23/7/07
20:47
Page 72
PROGRAMACIÓN ORIENTADA A OBJETOS
POLIMORFISMO > El polimorfismo es una técnica que permite tratar a un objeto de una clase derivada como si fuese de la clase padre. Polimorfismo significa muchas formas, y justamente se usa este término, porque permite que una variable tenga múltiples formas. La definición misma de la relación de herencia brinda la posibilidad de contar con el polimorfismo, esto es, como una clase derivada es-una clase base, siempre que necesitemos una clase base podremos usar una clase derivada. Por ejemplo, si tenemos un método que recibe un objeto de clase Figura, podremos pasarle un objeto de clase Rectángulo o de clase Círculo, ya que tanto el rectángulo como el círculo son figuras. Los lenguajes actuales permiten polimorfismo tanto por herencia como por implementación de interfaces. La mayor utilidad del polimorfismo radica en que permite programar en un nivel de abstracción superior, ya que podemos especificar los contratos (es decir, qué esperamos en un método o propiedad) utilizando clases abstractas, interfaces o clases base de una gran jerarquía de herencia. Además, como veremos a continuación, mediante el polimorfismo, se logra el máximo nivel de extensibilidad del código, ya que permite encapsular las responsabilidades de los objetos dentro de los objetos mismos y desde afuera trabajar con su abstracción o clase base. Para ilustrar la utilidad del polimorfismo, supongamos que tenemos que escribir un procedimiento que imprima la superficie de una figura, pero sin trabajar con orientación a objetos. Para hacerlo, necesitaremos conocer el tipo de figura y escribir una sentencia de tipo select case para calcular la superficie según el tipo de figura. Veamos algo de código (en C#).
› EARLY BINDING VS LATE BINDING El término Binding hace referencia al enlace entre el tipo de dato de una variable y el de su valor. Cuando el tipo de dato que tendrá un valor queda establecido en tiempo de codificación, se denomina Early Binding, mientras que cuando el tipo de dato real se conoce 72
recién en tiempo de ejecución, hablamos de Late Binding (enlace tardío). Cuando se explota la técnica de polimorfismo, se usa siempre Late Binding, ya que una variable se declara de una clase, pero en ejecución puede ser de una clase derivada.
04_IntrodProg.qxd
23/7/07
20:47
Page 73
› POLIMORFISMO switch(tipoFigura) { case “Cuadrado”: Console.WriteLine(base*altura); break; case “Triangulo”: Console.WriteLine((base*altura)/2); break; case “Circulo”: Console.WriteLine(3.14*radio*radio); break; }
El problema con esta porción de código reside en que, cuando agreguemos un nuevo tipo de figura, tendremos que agregar también el caso para hacer el cálculo correspondiente. Si utilizamos objetos, podemos tener una clase abstracta Figura, con un método abstracto CalcularSuperficie() y sendas clases derivadas para modelar los distintos tipos de figuras. Cada clase derivada es responsable de redefinir el método CalcularSuperficie para adaptarlo a su fórmula de cálculo. De este modo, nuestro procedimiento para imprimir la superficie se reduce a esto:
› LA MAGIA DEL POLIMORFISMO El polimorfismo abre las puertas a un nivel más de abstracción, en el que podemos escribir funcionalidad sin interesar mucho cómo se va a utilizar luego. Un ejemplo claro de esto es el manejo de secuencias de bytes en .Net. El Framework .Net posee una clase llamada Stream, que representa una secuencia de bytes.
Luego, hay clases derivadas para leer y escribir de distintos medios, como archivos y llamadas http, y otros. Lo bueno es que muchos métodos reciben como parámetro un Stream, pero podemos pasarle lo que necesitemos. Éste es un claro ejemplo de Polimorfismo, con una aplicación práctica en el mundo real. 73
04_IntrodProg.qxd
23/7/07
20:47
Page 74
PROGRAMACIÓN ORIENTADA A OBJETOS
public void Imprimir(Figura f) { Console.WriteLine(f.CalcularSuperficie()) } Cuadrado c; ... Imprimir( c ); // un cuadrado es una figura
Con esto, cuando necesitemos agregar un nuevo tipo de figura, bastará con escribir la clase correspondiente (heredando de Figura) e implementar el método CalcularSuperficie como corresponda, y no deberemos modificar el procedimiento de impresión. La técnica de polimorfismo es un poco difícil de entender al comienzo, pero una vez asimilada se convierte en el arma más poderosa de la orientación a objetos. El secreto está en entender que, cuando codificamos la llamada a un método de una clase base, debemos tener presente que en ejecución se llamará al método correspondiente de una clase derivada.
■ CONCLUSIONES La programación orientada a objetos, bien usada, puede ayudarnos a construir software con las características más deseables por todo programador: facilidad de escritura, facilidad de mantenimiento, extensibilidad y reutilización de código. La extensibilidad es la clave para escribir aplicaciones altamente flexibles, ya que permite agregar o modificar comportamiento con muy poco esfuerzo. Es muy importante aprender algunos patrones de diseño e incorporar correctamente los conceptos de abstracción, encapsulamiento y polimorfismo, para no caer en el error de utilizar un lenguaje orientado a objetos para escribir código estructurado. 74
05_IntrodProg.qxd
23/7/07
20:16
Page 75
CAPÍTULO 5
UML Hemos visto distintas formas de expresar algoritmos, como el pseudocódigo y los diagramas de flujo. Estas herramientas resultan muy útiles, pero fueron pensadas para expresar procedimientos y, por lo tanto, tienen su aplicación más popular en la programación estructurada. Con la llegada de las técnicas de POO, se buscaron nuevas formas de expresar detalles del problema por resolver ya que, en la programación orientada a objetos, sólo nos concentramos en los objetos del dominio y en los mensajes entre ellos.
ATENCIÓN AL LECTOR >
[email protected]
05_IntrodProg.qxd
23/7/07
20:16
Page 76
UML
¿QUÉ ES UML? > UML es la sigla de Unified Modeling Language, o Lenguaje Unificado de Mo-
delado. Como su nombre lo indica, UML es un lenguaje. Pero no un lenguaje de programación como los que vimos durante todo el libro, sino uno de modelado: su propósito principal consiste en definir modelos. Por modelo, se entiende una representación simplificada de la realidad. Además, UML es un lenguaje visual, es decir, toda su expresividad se basa en gráficos. El lenguaje UML nació en 1995 como resultado del trabajo en conjunto de Rumbaugh y Booch, dos investigadores en el área de Metodología, que comenzaron a trabajar juntos en Racional, una compañía fundada por Booch. Luego se les sumó Jacobson, quien aportó más ideas para desarrollar lo que se convirtió en la primera versión de UML. Hasta entonces, se habían desarrollado unos cuantos mecanismos de modelado de software, pero como ninguno fue formalizado correctamente ni estandarizado, cada ingeniero de software los aplicaba como sabía y como le convenía. Esto trajo como consecuencia que los modelos desarrollados fueran incomprendidos por los programadores, perdiendo su efectividad y, por lo tanto, su utilidad. UML fue aprobado como un estándar, lo que lo convierte en una herramienta fundamental para los analistas, diseñadores y arquitectos de software, que necesitan transmitir a los programadores, de forma clara y precisa, los detalles del software por implementar. UML consiste en un conjunto de diagramas de distintos tipos. Cada uno está destinado a cubrir alguno de los aspectos del software, y se pretende que con UML se puedan modelar absolutamente todos los elementos de una aplicación. Hay diagramas para modelar los objetos, sus interacciones, las interacciones del sistema con el mundo exterior (ya sean usuarios u otros sistemas). El objetivo principal de UML es el de proveer una herramienta de comunicación precisa y sin ambigüedades. En la actualidad, existen numerosas herramientas que no sólo ayudan a construir los diagramas en UML, sino que además permiten validar el modelo construido contra la especificación del lenguaje e incluso generar código sobre la base del modelo. A lo largo de este capítulo, estudiaremos los principales conceptos de algunos de los tipos de diagrama más importantes de UML y veremos cómo se relaciona con los conceptos de orientación a objetos que ya hemos aprendido. 76
05_IntrodProg.qxd
23/7/07
20:16
Page 77
› DIAGRAMA DE CLASES
DIAGRAMA DE CLASES > El diagrama de clases se utiliza para modelar las clases que intervienen en la solución, con sus propiedades, sus métodos y las relaciones entre ellas. Es un diagrama de tipo estático, en el sentido que modela entidades que se mantienen constantes durante la ejecución. Recordemos que las clases son fijas, y lo que varía son las instancias, o sea, los objetos. El diagrama de clases resulta muy útil tanto para especificar los requerimientos como para documentar los detalles del diseño. Además, mediante técnicas de ingeniería directa, permite generar el código de las clases, no sólo la definición, sino también las propiedades y el esqueleto de los métodos. Veremos a continuación las principales características del diagrama de clases, para empezar, cómo y cuándo utilizarlo. CLASES
Las clases se representan mediante un rectángulo dividido horizontalmente en tres partes. En la primera parte, se coloca el nombre de la clase; en la segunda, las propiedades; y en la tercera, los métodos. Recordemos que UML es un lenguaje y, por lo tanto, debemos respetar sus nomenclaturas y estándares, o el modelo que definamos no será un diagrama UML válido. Nombre de la clase
Cliente Nombre RealizarPago(cantidad)
} }
Propiedades Métodos
Figura 1. En el diagrama de clases, cada una se representa mediante un rectángulo con el nombre de la clase en la parte superior.
De las tres partes, sólo es obligatorio el nombre de la clase en la primera; las otra dos pueden quedar vacías. Además, no es necesario especificar todas las propiedades y operaciones de la clase, sólo las que resultan útiles para el modelo. 77
05_IntrodProg.qxd
23/7/07
20:16
Page 78
UML
Para darle más precisión al modelo, se puede especificar el tipo de dato de cada propiedad, separándolo del nombre de la propiedad con dos puntos (Nombre: String). Si el tipo de dato de una determinada propiedad es otra clase, estamos ante un caso de composición, que tiene una forma específica en UML (que veremos luego), sin embargo, en casos en que no haya lugar a dudas es posible indicarlo como si fuese un tipo de dato básico, como muestra la Figura 2.
Cliente Nombre: String Contacto: Persona La sección de propiedades y de métodos puede quedar vacia.
Podemos especificar los tipos de dato de las propiedades. En algunos casos podemos usar clases como tipo de dato de las propiedades.
Figura 2. Si bien es un caso de composición, a veces, podemos usar clases como tipo de las propiedades. Visibilidad
En UML, se puede especificar la visibilidad de cada elemento de una clase (propiedad o método). Si bien la visibilidad es algo más relacionado a la implementación en un lenguaje particular, es importante indicarla en el modelo de diseño, ya que permite dejar definidas ciertas normas de encapsulamiento que el analista o el diseñador detectan durante las primeras fases del proyecto. Si bien UML prevé notaciones para los tipos de alcance básicos, el lenguaje está abierto y permite utilizar modificadores propios del lenguaje que se utilice luego para la implementación.
› INGENIERÍA DIRECTA E INVERSA Cuando se habla de modelos y, sobre todo de UML, es muy común mencionar a la Ingeniería Directa y a la Ingeniería Inversa. Mediante la ingeniería directa, se puede generar 78
el código fuente que implemente el modelo, mientras que, a través de la ingeniería inversa, se puede obtener un modelo completo o parcial a partir de código fuente ya escrito.
05_IntrodProg.qxd
23/7/07
20:16
Page 79
› DIAGRAMA DE CLASES
La visibilidad se expresa mediante un signo a la izquierda del nombre de la propiedad o del método, y los utilizados son el signo más (+) para los elementos públicos, el signo menos (-) para los privados y el numeral (#) para los protegidos. Recordemos que los elementos protegidos sólo son visibles por la clase y por sus descendientes, mientras que los privados sólo son visibles dentro de la clase. RELACIONES ENTRE CLASES
Las relaciones entre clases también están contempladas en UML, dado que, durante el diseño, es muy útil especificar claramente cualquier relación, que deberá ser traducida luego al código. Composición
En UML, la composición se representa mediante una línea que une las dos clases, pero colocando un pequeño rombo del lado de la clase que se compone a partir de la otra. Opcionalmente, se puede indicar el nombre de la propiedad que representa la composición, pero no es muy habitual hacerlo, sobre todo en las etapas iniciales del análisis. En la Figura 3, vemos que una Escuela se compone de Aulas. Observemos los pequeños números debajo de la línea. Estos números indican la multiplicidad, es decir, cuántos elementos pueden existir de cada lado. El asterisco (*) indica muchos, sin especificar cuántos. Según esto, el diagrama de la Figura 3 deja bien claro que una escuela se compone de muchas aulas. Del mismo modo, si leemos la multiplicidad en sentido inverso, podemos entender que un aula está sólo en una escuela.
Escuela + Nombre: String
Aula
Posee 1
1..*
+ Capacidad: int
Figura 3. UML permite especificar la relación de composición, indicando además cuántos elementos de una clase componen a la otra.
Además, sobre la línea que indica la relación, se puede colocar un texto aclaratorio sobre el tipo de composición. En el ejemplo de la Figura 3, hemos especificado Posee como nombre de la relación. Si bien es opcional, el nombre de 79
05_IntrodProg.qxd
23/7/07
20:16
Page 80
UML
la relación ayuda mucho a la claridad del diagrama y a su posterior implementación en un lenguaje de programación. Asociación simple
Anteriormente vimos que, si el tipo de dato de una propiedad es una clase, podemos escribirlo directamente o bien utilizar una relación de asociación. Hay una tercera opción que es usar ambos al mismo tiempo, pero no la veremos ahora. La relación de asociación se representa de manera similar a la composición, pero sin utilizar el rombo; simplemente se coloca una línea entre las dos clases asociadas y una flecha que indica el sentido de la asociación. Al igual que en la composición, puede agregarse el nombre de la relación para aumentar la riqueza del modelo.
Escuela + Nombre: String
Ubicada en
Dirección + Calle: String + Numero: int
Figura 4. La relación de asociación es similar a la composición, pero se utiliza una flecha para indicar el sentido de la relación.
› PROGRAMACION Y DISEÑO Los conceptos introducidos por la POO como forma de trabajo, no sólo se aplican a la programación propiamente dicha, sino que también se puede hacer
80
análisis orientado a objetos, y diseño orientado a objetos. UML está pensado para la creación de diagramas usando este paradigma.
05_IntrodProg.qxd
23/7/07
20:16
Page 81
› DIAGRAMA DE CLASES
Herencia
La relación de herencia también tiene su forma particular dentro del lenguaje UML. La representación de la herencia consiste en una flecha con la punta sin rellenar (en blanco). Lo más común es colocar las clases derivadas debajo de la clase base, de manera que la flecha que representa la herencia tenga sentido de abajo hacia arriba, generando un diagrama tipo árbol. Si la clase base es abstracta, se debe colocar el nombre con letra itálica (por ejemplo, Figura). Aunque parezca una obviedad (dada la definición de herencia) tengamos en cuenta que, en el diagrama de las clases derivadas, no es necesario volver a indicar las propiedades y los métodos heredados.
Escuela + Nombre: String
Primaria
Secundaria
Terciaria
Publica
Privada
Figura 5. La herencia se representa mediante flechas sin relleno, apuntando hacia la clase base.
En el caso de las interfaces, UML prevé una forma de representarlas, como así también una notación especial para indicar la implementación de una interfaz por parte de una clase. Para indicar que se trata de una interfaz y no de una clase, en el diagrama de clases se debe colocar el indicador interfaz sobre el nombre, en la pri81
05_IntrodProg.qxd
23/7/07
20:16
Page 82
UML
mera división del rectángulo que representa en este caso la interfaz. Además, si bien desde el punto de vista del polimorfismo la implementación de interfaces resulta similar a la herencia, no es lo mismo, por lo que UML hace la distinción correspondiente. Para especificar que una clase implementa una interfaz, se debe colocar una flecha de punta hueca apuntando hacia la interfaz, pero con línea punteada. En la Figura 6 se aprecia una interfaz (IComunicador) y una clase, la que implementa (ComunicadorPorModem). Notemos que, a diferencia del diagrama de herencia, aquí sí debemos especificar en la clase todos los métodos y propiedades de la interfaz, ya que al aceptar el contrato que la interfaz nos impone, debemos implementarlo completamente (caso contrario ni el modelo ni el código serán válidos).
IComunicador IComunidor
+ Inicializar(): Inicializar() :void void + Finalizar(): Finalizar() :void void + DispositivoListo(): DispositivoListo() :bool bool + CadenaRecibida(string): CadenaRecibida(string) :void void + EscribirCadena(string): EscribirCadena(string) :void void + AsignarReceptorDeDatos(RecepciónDeDatos): AsignarReceptorDeDatos(RecepcionDeDatos) :void void
IDisponible ComunicadorPorModem – puerto: SerialPort + Inicializar(): void + Finalizar(): void + DispositivoListo(): void + CadenaRecibida(string): void + EscribirCadena(string): void
Figura 6. Al modelar la implementación de una interfaz, debemos volver a indicar todas las propiedades y métodos en la clase que la implementa. 82
05_IntrodProg.qxd
23/7/07
20:16
Page 83
› DIAGRAMA DE SECUENCIA
DIAGRAMA DE SECUENCIA > Varias veces dijimos que la programación orientada a objetos se basa en la representación en software de los objetos que intervienen en el dominio de la aplicación junto con las relaciones e interacciones entre ellos. Mediante el diagrama de clases, UML permite especificar de manera estática las clases del modelo y sus relaciones. Para expresar la forma en que los objetos (no las clases) interactúan para llevar a cabo su objetivo dentro de la aplicación y solucionar un problema, UML provee el diagrama de secuencia, que permite especificar los mensajes que intercambian los objetos, el orden en que lo hacen y cómo reacciona cada objeto ante la llegada de un mensaje proveniente de otro. ELEMENTOS DEL DIAGRAMA DE SECUENCIA
A diferencia del diagrama de clases, en el diagrama de secuencia intervienen instancias, es decir, objetos concretos que pertenecen a una determinada clase. Los objetos se representan simplemente como rectángulos con su nombre y la clase a la que pertenecen dentro de él, separadas por dos puntos (:). En caso en que no interese la identidad de una instancia en particular, se puede omitir el nombre, colocando sólo el nombre de la clase a la que pertenece, precedida por los dos puntos. Otro elemento de este diagrama son los mensajes. Desde el punto de vista de la implementación, podemos considerar los mensajes como invocación a métodos: cuando un objeto A envía un mensaje B a un objeto C, podemos traducir como que el objeto A invoca el método B del objeto C. Los mensajes se representan mediante flechas simples, con el nombre del mensaje escrito sobre ellas. Por último, dado que para representar una secuencia debemos especificar el orden en que los mensajes son enviados, UML permite indicar una línea de tiempo, que se coloca en el diagrama de manera vertical, y que consiste en una línea punteada por cada una de las instancias que intervienen en el diagrama. Para entender cómo funciona el diagrama de secuencia, imaginemos que tenemos que representar un escenario de matriculación de un alumno en un curso. Tenemos una clase Alumno, una clase Curso y una clase Facultad. La clase Facultad tiene la capacidad de determinar si un alumno puede inscribirse en un curso (porque conoce las correlatividades y las asignaturas aprobadas por el alumno). Cuando un alumno quiere inscribirse, hace la solicitud al curso (mediante un 83
05_IntrodProg.qxd
23/7/07
20:16
Page 84
UML
mensaje), y el curso consulta con la universidad si se cumplen las condiciones. Si todo está bien, el curso acepta la inscripción del alumno. La Figura 7 representa este escenario mediante un diagrama de secuencia.
unAlumno : Alumno
unCurso: Curso
: Universidad
SolicitarInscripcion(unAlumno)
VerificarCorrelatividades(unAlumno, unCurso)
PuedeCursar
Solicitud aprobada
Figura 7. El diagrama de secuencia muestra la interacción entre los objetos en un determinado escenario.
Como observamos en el diagrama, el objeto Alumno envía un mensaje SolicitarInscripcion al objeto Curso. Luego, éste envía un mensaje VerificarCorrelatividades al objeto de clase Universidad, quien contesta afirmativamente. Ante la llegada de la respuesta de la universidad, el objeto Curso le confirma la inscripción al objeto Alumno. Observemos que el tiempo corre hacia abajo, por lo que el orden en que aparece cada mensaje en el diagrama es cronológico. En la Figura 7, hay algunos detalles que aún no mencionamos. Los mensajes de línea punteada corresponden a las respuestas de un objeto a un mensaje determinado. Los rectángulos delgados sobre las líneas de tiempo corresponden a instancias particulares, esto es, cada rectángulo en la línea de tiempo de un objeto corresponde a una instancia particular. 84
05_IntrodProg.qxd
23/7/07
20:16
Page 85
› DIAGRAMAS DE CASOS DE USO
DIAGRAMAS DE CASOS DE USO > Un caso de uso es un resumen de un escenario particular, que describe la forma en que los elementos externos (usuarios u otros sistemas) interactúan con el software que se está modelando. UML permite crear diagramas para modelar casos de uso, que resultan de gran utilidad para documentar y para negociar con los usuarios y clientes sobre la funcionalidad que tendrá el software una vez implementado. En los diagramas de caso de uso, intervienen actores y casos de uso. Los actores son cualquier elemento que puede interactuar con el software (usuarios, organizaciones o sistemas). Los casos de uso son secuencias de acciones que proveen algún tipo de valor para los actores. Los casos de uso se dibujan mediante óvalos con su nombre adentro, mientras que los actores se representan mediante hombrecitos. Opcionalmente, puede dibujarse un rectángulo para indicar los límites del sistema y así dejar bien claro cuáles son las responsabilidades que se asumirán a la hora de la implementación (todo lo que está fuera de los límites, se asume que no será responsabilidad de los desarrolladores). En la Figura 8, se aprecia cómo el gerente de personal interactúa con el sistema de sueldos, tanto renovando contratos de personal como generando liquidaciones de sueldos. Además del gerente, un sistema que pertenece al estado interactúa con el sistema de sueldos solicitando el cálculo de los impuestos para controlar el estado impositivo de los empleados. Observemos también que un caso de uso no es independiente, sino que puede utilizar a otro para cumplir su tarea (representado en la figura como una línea que une Generar Liquidaciones con Calcular Impuestos).
› RECURSOS EN LA WEB En Internet hay una gran cantidad de información sobre UML, desde detalles básicos hasta ejemplos completos y de gran complejidad. Sin duda, el sitio de cabecera es el oficial de UML (en inglés): www.uml.org.
Otra página de gran interés, también en idioma inglés, que se puede visitar es Agile Modeling (www.agilemodeling.com/artifacts), que cuenta con una sección completa dedicada a diagramas y modelos UML.
85
05_IntrodProg.qxd
23/7/07
20:16
Page 86
UML
Sistema de Sueldos
Renovar Contratos
Gerente de Personal
Generar Liquidaciones
Calcular Impuestos
Sistema estatal de Control
Figura 8. El diagrama de casos de uso especifica cómo se relaciona el sistema con el mundo exterior.
■ CONCLUSIONES A lo largo del capítulo, vimos algunos de los diagramas que permite crear UML y su aplicación en el ciclo de vida del desarrollo de software, sobre todo para documentar las primeras etapas (análisis y diseño). Si bien, al ser visual, UML resulta bastante fácil de aprender, es recomendable familiarizarse primero con las técnicas y conceptos generales de la orientación a objetos y luego dedicarse a aprender UML, de manera de poder sacarle el mayor provecho posible a este poderoso lenguaje de modelado. También será imprescindible la lectura del libro UML Distilled, de Martin Fowler (traducido al español como UML gota a gota). 86
06_IntrodProg_ApendA.qxd
23/7/07
20:17
Page 87
APÉNDICE
El examen En este apéndice, haremos un repaso general de los principales conceptos aprendidos y veremos algunas preguntas del ejemplo del examen correspondiente a la estrella cero del programa Desarrollador Cinco Estrellas 2005 (http://www.dce2005.com).
ATENCIÓN AL LECTOR >
[email protected]
06_IntrodProg_ApendA.qxd
23/7/07
20:17
Page 88
EL EXAMEN
ALGORITMOS
Un algoritmo es una secuencia ordenada y sistemática de pasos que lleva a la solución de un problema o a realizar cierto cálculo complejo. Recordemos que se debe tener en cuenta la importancia del orden de las operaciones. Si contamos con una secuencia de pasos para realizar una tarea, pero en ella no importa el orden en que los llevemos a cabo, porque igual alcanzamos el objetivo, no se trata de un algoritmo. Todo programador debe tener la habilidad para deducir algoritmos a partir de un problema, ya que esta tarea conforma el primer paso para la implementación de cualquier aplicación de software. Cuando creamos un algoritmo para un problema determinado, normalmente, deberemos expresarlo para poder comunicarlo a los demás o para dejar documentada la idea. Existen distintas formas de expresar algoritmos, pero las dos más comunes y más usadas son el pseudocódigo y los diagramas de flujo. El pseudocódigo es una forma flexible y abierta de expresar algoritmos, pero sin las ambigüedades del lenguaje natural. Resulta parecido a un lenguaje de programación, pero no existen reglas precisas que deban ser cumplidas al usarlo, con lo cual cada programador puede escribir pseudocódigo como mejor le resulte. Por otro lado, los diagramas de flujo son una herramienta visual y más precisa, ya que cuenta con reglas que definen exactamente cómo debe expresarse cada tipo de estructura de un algoritmo (como por ejemplo, puntos de decisión). LENGUAJES DE PROGRAMACIÓN
Los lenguajes de programación permiten escribir instrucciones para una computadora utilizando palabras y símbolos pertenecientes a algún idioma humano. El principal objetivo de los lenguajes de programación es proveer una herramienta que resulte más sencilla que ingresar las secuencias de números que es capaz de interpretar una computadora como instrucciones. Según el grado de abstracción o de lo mucho que se aleje un lenguaje del código nativo de la máquina, se puede clasificar en lenguaje de bajo nivel (como Assembler), lenguaje de nivel intermedio (como C) o lenguaje de alto nivel (como por ejemplo los lenguajes de .Net). En general, los lenguajes de bajo nivel son más difíciles de utilizar, pero permiten escribir programas más eficientes, mientras que los de alto nivel poseen mayor expresividad, ya que con una instrucción del lenguaje es posible darle varias instrucciones a la máquina, incluso con un esfuerzo mental menor por parte del programador. 88
06_IntrodProg_ApendA.qxd
23/7/07
20:17
Page 89
› PROGRAMACIÓN ESTRUCTURADA
Como la computadora sólo puede interpretar instrucciones expresadas como secuencias de números binarios (unos y ceros), es necesario traducir los programas escritos en algún lenguaje, a instrucciones que la máquina pueda entender. Para ello, podemos utilizar un intérprete o un compilador. Los compiladores toman el texto del programa (el código fuente) y generan un archivo con instrucciones ejecutables por la computadora. El proceso de compilación se realiza una sola vez: cuando el programa está terminado y antes de poder correrlo en la computadora. Los intérpretes, en cambio, toman cada una de las instrucciones del programa, las convierten a código de máquina y las ejecutan. A diferencia de la compilación, el proceso de interpretación se lleva a cabo cada vez que el programa se ejecuta; por esto, generalmente, los programas compilados funcionan un poco más rápido que los programas interpretados. En .Net, al momento de compilar, no se genera código de máquina, sino que se produce código en un lenguaje intermedio que luego, durante la primera ejecución, es traducido a lenguaje de máquina. En algunos lenguajes, el compilador no genera el código definitivo, sino que genera un archivo llamado código objeto, que luego debe ser enlazado con las librerías utilizadas para generar un único archivo ejecutable. El encargado de realizar esto es un programa llamado linker (enlazador). PROGRAMACIÓN ESTRUCTURADA
La programación estructurada es un paradigma que propone construir programas utilizando sólo tres tipos de construcciones: la secuencia de instrucciones, la ejecución condicional y la repetición. Frente a la programación lineal que utiliza las instrucciones de salto (GOTO) para modificar el curso de ejecución, la programación estructurada representa una importante mejora en las formas de construir software, ya que permite obtener código mucho mejor escrito, más claro, más legible, fácil de mantener y de modificar. Repasemos algunos de los principales conceptos de la programación estructurada: Identificadores: un identificador es una palabra que se utiliza para darle nombre a algún componente del programa, ya sea una instrucción, un subprograma o una variable. La mayoría de los lenguajes de programación poseen reglas estrictas para la escritura de nuevos identificadores, como por ejemplo, que deban comenzar necesariamente con una letra del alfabeto o con un guión bajo. Variables: es una posición de memoria donde se encuentra un dato que debemos manejar dentro del programa. Casi siempre, las variables poseen un nom89
06_IntrodProg_ApendA.qxd
23/7/07
20:17
Page 90
EL EXAMEN
bre, y se utiliza un identificador para nombrarlas y referenciarlas en el código fuente. Las variables poseen un alcance o ámbito, que determina las porciones de código desde donde se puede acceder al valor contenido en la variable. Tipos de datos: definen el rango de valores y las operaciones que se pueden aplicar sobre una variable. No todos los lenguajes exigen asociar un tipo de dato a una variable (como por ejemplo Visual Basic). Los lenguajes que obligan a definir las variables con tipo, se denominan comúnmente fuertemente tipados. Los lenguajes de esta clase son más seguros en cuanto a errores en tiempo de ejecución, ya que permiten detectar asignaciones incorrectas a las variables durante el proceso de compilación del programa. Sentencias: son acciones que pueden ser ejecutadas como parte del programa. La principal diferencia entre sentencia e instrucción es que una sentencia puede traducirse en cero o más instrucciones. Una sentencia que se traduce en cero instrucciones se denomina no ejecutable. El ejemplo más conocido de sentencia no ejecutable son los comentarios utilizados para dejar mensajes en el código fuente, que explican alguna idea o cualquier otro aspecto que el programador considere necesario para aumentar la legibilidad del código. Siempre resulta una buena práctica el uso de comentarios. Las sentencias pueden ser simples o compuestas. Una sentencia simple es una sentencia en sí misma, mientras que una sentencia compuesta consiste en dos o más sentencias simples agrupadas mediante alguna estructura de control, como puede ser un condicional. Procedimientos y funciones: una de las técnicas más usadas para simplificar la implementación de un programa consiste en dividirlo en unidades más pequeñas llamadas módulos o subprogramas. Generalmente, los módulos son implementados como procedimientos o funciones. Los procedimientos son unidades independientes que pueden ser llamadas desde cualquier otra parte del programa y consisten de una secuencia de instrucciones que se ejecutan y terminan, pero sin devolver nada a quien lo llamó. Las funciones, en cambio, luego de ejecutarse, devuelven un valor como resultado. Por eso, se utilizan para realizar algún cálculo o alguna operación que requiera devolver un resultado. PROGRAMACIÓN ORIENTADA A OBJETOS
La programación orientada a objetos (POO u OOP, por sus siglas en inglés) es un paradigma de programación que propone identificar los objetos propios del dominio del problema por resolver, y representar sus propiedades y sus interac90
06_IntrodProg_ApendA.qxd
23/7/07
20:17
Page 91
› PROGRAMACIÓN ORIENTADA A OBJETOS
ciones mediante el código del programa. La POO permite lograr programas aún más fáciles de entender que con la programación estructurada, ya que hay una correspondencia casi directa entre la realidad y la representación de esa realidad mediante un lenguaje de programación. Además, utilizada correctamente, la programación orientada a objetos permite una mayor reutilización de código y un mejor encapsulamiento, lo que facilita el mantenimiento tanto para corregir errores como para agregar funcionalidades nuevas. Los conceptos más importantes que hay que conocer son el de clase y el de objeto. Una clase es una abstracción de un grupo de objetos que comparten ciertas características y comportamiento. Un objeto es un elemento particular de una determinada clase, y como tal, posee las mismas propiedades y comportamiento que los demás miembros de la clase, pero posee una identidad única. Esto significa que cualquier otro objeto será distinto, por más que tenga las mismas propiedades. Los objetos se caracterizan por tener un estado, que consiste en el conjunto de valores de sus propiedades en una unidad de tiempo específica. Otro concepto importante es el de encapsulamiento. Éste consiste en ocultar dentro de una clase la implementación real de una propiedad o método, de manera que desde afuera se acceda mediante una interfaz que oculte los detalles internos. Lograr un buen nivel de encapsulamiento es fundamental para reducir el impacto de los cambios que harán en un futuro. Los lenguajes de .Net permiten el encapsulamiento mediante el uso de propiedades, que básicamente son una forma de exponer atributos de la clase, pero ocultando la forma interna en que se almacena ese valor. Muchas clases poseen las mismas características que otras, aunque tienen algunas propias. En situaciones así, podemos hacer uso de la herencia. La herencia es una técnica que permite definir una clase como derivada de otra. Al hacerlo, la nueva clase recibe o hereda todo el comportamiento y las propiedades de su clase padre. Los programadores y analistas más experimentados buscan crear jerarquías de herencia, de manera de aprovechar al máximo la reutilización de código de las clases superiores en las clases inferiores. En ocasiones, puede ocurrir que la clase base (la clase de la que heredan las otras) no represente un conjunto de objetos concretos y exista sólo para contener comportamiento y código común. Este tipo de clases se denominan abstractas. Algunos lenguajes permiten que una clase herede de varias, pero esto muchas veces desemboca en un problema de implementación. Los lenguajes de la plataforma .Net sólo permiten heredar de una única clase. En .Net (y en Java también), si necesitamos abstraer una cierta funcionalidad común a va91
06_IntrodProg_ApendA.qxd
23/7/07
20:17
Page 92
EL EXAMEN
rias clases no relacionadas entre sí, podemos hacer uso de las interfaces, parecidas a las clases abstractas con la diferencia que no tienen ningún método implementado y que una clase puede implementar más de una interfaz, además de heredar de una clase. Una de las capacidades más interesantes brindadas por la programación orientada a objetos es el polimorfismo Éste consiste en conocer la clase real de un objeto recién durante la ejecución del programa. El polimorfismo se basa en la herencia y es la clave para lograr código extensible y flexible, atributos muy deseados en todo programa. DESARROLLADOR 5 ESTRELLAS
Veamos algunas de las preguntas del examen cero del DCE 2005. Los exámenes de Desarrollador 5 Estrellas incluyen 20 preguntas de tipo multiple choice, donde debemos elegir entre 4 posibles respuestas. Para aprobar, es necesario responder bien al menos el 70% de las preguntas. 1)
¿Cuál es la utilidad de los modificadores de acceso? A. Permiten definir el nivel de acceso de los miembros de una clase. B. Permiten definir niveles de autenticación y autorización de los métodos de una clase. C. Permiten relacionar clases entre sí. D. Permiten crear instancias de clases. La respuesta correcta es la A. Los modificadores de acceso permiten definir quiénes pueden acceder a una propiedad o método. Los más comunes son public, private y protected.
2)
Las estructuras de control selectivas: A. Dirigen el flujo de ejecución según la evaluación de expresiones. B. Permiten ejecutar un conjunto de sentencias repetidamente una cierta cantidad de veces o hasta que se cumpla una determinada condición. C. Todas las opciones son correctas. D. Ninguna opción es correcta. La respuesta correcta es la A. Recordemos que las estructuras de selección en la mayoría de los lenguajes son dos: if y select case (o switch en C#).
92
06_IntrodProg_ApendA.qxd
23/7/07
20:17
Page 93
› DESARROLLADOR 5 ESTRELLAS
3)
¿Qué es un algoritmo? A. Un software de aplicación. B. Cualquier código escrito en un lenguaje de programación. C. Un método para resolver un problema mediante una serie de pasos precisos, definidos y finitos. D. Las opciones B y C son correctas. Esta pregunta es un poco tramposa. La respuesta correcta es la C. La respuesta D no puede ser correcta porque la opción B afirma que un algoritmo es código escrito en un lenguaje de programación, y aprendimos que si está escrito en un lenguaje de programación ya se trata de un programa (que, generalmente, es la implementación de un algoritmo).
4)
Un entorno integrado de programación normalmente contiene: A. Un editor de código fuente. B. Un compilador. C. Un depurador. D. Todas las opciones son correctas. Ésta es bastante sencilla. Los entornos integrados de desarrollo justamente se llaman se esa manera, porque integran todas las herramientas necesarias para la escritura, compilación y depuración de programas. Por lo tanto, la respuesta correcta es la D.
5)
¿Para qué sirve el encapsulamiento? A. Permite ocultar los métodos de una clase. B. Permite ocultar la implementación de los métodos de una clase. C. Permite identificar objetos de forma unívoca. D. Permite generar relaciones de herencia. Como vimos en el Capítulo 4 y repasamos en este apéndice, el principal objetivo del encapsulamiento es el de ocultar la implementación de los métodos (y propiedades) de una clase. La respuesta correcta, entonces, es la B.
93
06_IntrodProg_ApendA.qxd
23/7/07
20:17
Page 94
EL EXAMEN
6)
¿Cuál es el bloque que utiliza .NET para proveer administración estructurada de excepciones? A. OnError/Goto B. If/Else C. Throw D. Try/Catch/Finally La respuesta correcta es la D. En Visual Basic 6, se utilizaba el OnError/Goto para manejar los errores; pero desde la primera versión de, .Net se utilizó el Try/Match/Finally, y se denomina estructurada por no utilizar GoTo.
7)
Si se desea definir un comportamiento para un conjunto de clases no necesariamente relacionadas entre sí, la mejor opción es: A. Definir una jerarquía de herencia entre las clases. B. Encapsular el comportamiento en una clase agregada. C. Definir una interfaz. D. Relacionar las clases mediante una asociación bidireccional. Como hemos visto, la herencia no es una relación sólo sintáctica, sino más bien semántica. Entre la clase base y la derivada, se define una relación es-un. Si las clases no están relacionadas, entonces, no es natural usar herencia, y podemos hacer uso de las interfaces para definir el comportamiento común. Por lo tanto, la respuesta correcta es la C.
8)
¿A qué se denomina comúnmente Cast? A. A un conjunto de datos que serán convertidos. B. A la conversión implícita de un objeto/tipo o tipo/objeto. C. A una forma explicita de convertir tipos de datos entre sí. D. Ninguna opción es correcta. La respuesta correcta es la C. El Cast o Typecast es una conversión explícita entre tipos de datos. Mediante la operación de cast le estamos diciendo al compilador que nos hacemos responsables de las consecuencias de la conversión (como pérdida de precisión o, incluso, que los tipos sean incompatibles).
94