Guía Completa de Programación Java
Guía Completa de Programación Java
Pello Altadill
Eugenia Pérez
Convenciones utilizadas
Código
Código
Comando o salida
Comando de consola
Para los comandos de consola o la salida de un programa se usa esta
fuente
Notas cerficación
NOTA
Código fuente/Ejemplos
Para conseguir el código fuente con los ejemplos, soluciones a los
ejercicioes, etc., visita cualquiera de estas páginas:
[Link]
[Link]
CAPÍTULOS
1. LENGUAJES DE PROGRAMACIÓN15
2. LENGUAJE JAVA28
3. TEST UNITARIOS173
4. CÓDIGO LIMPIO203
5. LENGUAJE UML252
6. SCRUM323
7. REFACTORIZACIÓN334
8. PATRONES DE PROGRAMACIÓN379
Contenido
1 LENGUAJES DE PROGRAMACIÓN
2 CLASIFICACIÓN Y CARACTERÍSTICAS
4 INTERPRETACIÓN Y COMPILACIÓN
4.1 EL COMPILADOR
4.2 EL INTÉRPRETE
4.3 JAVA Y LAS MÁQUINAS VIRTUALES
5 PROCESOS DE DESARROLLO
5.1 ANÁLISIS
5.2 DISEÑO
5.3 CODIFICACIÓN
5.4 PRUEBAS
5.5 DOCUMENTACIÓN
5.6 EXPLOTACIÓN
5.7 MANTENIMIENTO
1 INTRODUCCIÓN
1.3 JAVA2
1.1.1 Fundamentos de java
1.1.2 Preparando el entorno
2 HOLAMUNDO
1.2 INTRODUCCIÓN
2.1.1 Ejemplo Programa básico
2.2 PAQUETES O PACKAGES
2.3 IMPORTANDO PAQUETES
2.4 COMENTARIOS
2.5 EJERCICIOS DEL TEMA INTRODUCCIÓN
2.5.1 Ejercicio 1
2.5.2 Ejercicio 2
2.5.3 Ejercicio 3
2.5.4 Ejercicio 4
2.5.5 Ejercicio 5
2.5.6 Ejercicio 6
2.5.7 Ejercicio 7
3 VARIABLES
3.1 INTRODUCCIÓN
3.2 NOMBRADO DE VARIABLES
3.3 TIPOS PRIMITIVOS
3.4 TIPOS NUMÉRICOS
3.4.1 Ejemplo de Números enteros
3.4.2 Ejemplo de Valores reales
3.5 CARACTERES
3.5.1 Ejemplo de Caracteres sueltos
3.6 BOOLEANOS
3.6.1 Ejemplo de Valores booleanos: verdadero/falso
3.7 WRAPPERS DE PRIMITIVOS
3.8 CADENAS O STRINGS
3.8.1 Ejemplo de Cadenas o Strings
3.9 ÁMBITO DE VARIABLES
3.9.1 Ámbito en bloques
3.10 EJERCICIOS DEL TEMA VARIABLES
3.10.1 Ejercicio 1
3.10.2 Ejercicio 2
3.10.3 Ejercicio 3
3.10.4 Ejercicio 4
3.10.5 Ejercicio 5
3.10.6 Ejercicio 6
3.10.7 Ejercicio 7
4 ENTRADA/SALIDA BÁSICA
4.1 INTRODUCCIÓN
4.2 CONVERSIÓN DE STRING A OTROS TIPOS
4.3 PASO DE ARGUMENTOS
4.4 PASO DE ARGUMENTOS EN ECLIPSE
4.4.1 Ejemplo de Argumentos por consola
4.4.2 Ejemplo de Argumentos por consola y conversión
4.5 LECTURA DE VALORES POR CONSOLA
4.5.1 Ejemplo de Entrada por consola
5 OPERADORES
5.1 INTRODUCCIÓN
5.2 OPERADORES ARITMÉTICOS
5.2.1 Ejemplo de Operadores aritméticos
5.3 OPERADORES DE COMPARACIÓN
5.3.1 Ejemplo de Operadores de comparación
5.4 OPERADORES BOOLEANOS
5.4.1 Ejemplo de Operadores booleanos
5.5 OPERADORES DE BITS
5.5.1 Ejemplo de Operadores de bits
5.6 OPERADOR DE STRINGS
5.6.1 Métodos de Strings
5.7 OPERADOR TERNARIO
5.8 EJERCICIOS DEL TEMA OPERADORES
5.8.1 Ejercicio 1
5.8.2 Ejercicio 2
5.8.3 Ejercicio 3
5.8.4 Ejercicio 4
5.8.5 Ejercicio 5
5.8.6 Ejercicio 6
5.8.7 Ejercicio7
5.8.8 Ejercicio8
6 ESTRUCTURAS DE CONTROL
6.1 INTRODUCCIÓN
6.2 CONDICIONAL IF
6.2.1 Ejemplo de If
6.3 CONDICIONAL IF-ELSE
6.3.1 Ejemplo de If Else
6.4 CONDICIONAL IF-ELSE-IF
6.4.1 Ejemplo de If Else If
6.5 SWITCH/CASE
6.5.1 Ejemplo de Switch/Case
6.6 EJERCICIOS DEL TEMA. ESTRUCTURAS DE CONTROL
6.6.1 Ejercicio 1
6.6.2 Ejercicio 2
6.6.3 Ejercicio 3
6.6.4 Ejercicio 4
6.6.5 Ejercicio 5
6.6.6 Ejercicio 6
6.6.7 Ejercicio 7
6.6.8 Ejercicio 8
6.6.9 Ejercicio 9
7 BUCLES
7.1 INTRODUCCIÓN
7.2 EL BUCLE WHILE
7.2.1 Ejemplo de bucle while
7.3 EL BUCLE DO/WHILE
7.3.1 Ejemplo de Bucle do/while
7.4 EL BUCLE FOR
7.5 EL BUCLE FOR TIPO FOR-EACH
7.5.1 Ejemplo de Bucle for
7.6 BREAK Y CONTINUE
7.6.1 break
7.6.2 continue
7.6.3 Ejemplo de Break y continue
7.7 EJERCICIOS DEL TEMA BUCLES
7.7.1 Ejercicio 1
7.7.2 Ejercicio 2
7.7.3 Ejercicio 3
7.7.4 Ejercicio 4
7.7.5 Ejercicio 5
7.7.6 Ejercicio 6
7.7.7 Ejercicio 7
7.7.8 Ejercicio 8
8 ARRAYS
8.1 INTRODUCCIÓN
8.2 EJEMPLO DE ARRAYS
8.2.1 Ejemplo de Arrays multidimensionales
8.3 EJERCICIOS DEL TEMA [Link]
8.3.1 Ejercicio 1
8.3.2 Ejercicio 2
8.3.3 Ejercicio 3
8.3.4 Ejercicio 4
8.3.5 Ejercicio 5
8.3.6 Ejercicio 6
8.3.7 Ejercicio 7
8.3.8 Ejercicio 8
8.3.9 Ejercicio 9
8.3.10 Ejercicio 10
8.3.11 Ejercicio 11
9 ENUM
10 CLASES
11 MÉTODOS
12 POO
12.1 INTRODUCCIÓN
12.2 MODIFICADORES DE ACCESO
12.3 HERENCIA
12.4 CONSTRUCTORES Y HERENCIA
12.5 SOBRESCRITURA DE MÉTODOS
12.6 EJEMPLO DE HERENCIA: CLIENTEVIP
12.7 EJEMPLO DE CLASE ABSTRACTA: PERSONAJE
12.8 EJEMPLO DE HERENCIA: PERSONAJEORCO
12.9 CLASES ABSTRACTAS
12.10 POLIMORFISMO
12.11 MÉTODOS VIRTUALES
12.12 PARÁMETROS POLIMÓRFICOS
12.13 CAST O CONVERSIÓN DE OBJETOS
12.14 EJERCICIOS DEL TEMA POO
12.14.1 Ejercicio 1
12.14.2 Ejercicio 2
12.14.3 Ejercicio 3
12.14.4 Ejercicio 4
12.14.5 Ejercicio 5-Avanzado
13 INTERFACES
14 EXCEPCIONES
15 API JAVA
15.1 VECTORES
15.1.1 Ejemplo de Vectores
15.2 HASHTABLES
15.2.1 Ejemplo de Hashtables
15.3 ARRAYLIST
15.4 HASHSETS
15.4.1 Ejemplo de HashSets
15.5 STACKS
15.5.1 Ejemplo de Stacks
15.6 TREEMAPS
15.6.1 Ejemplo de TreeMaps
15.7 LINKEDLISTS
15.7.1 Ejemplo de LinkedLists
15.8 STRING TOKENIZER DIVISIÓN DE CADENAS
15.8.1 Ejemplo de Tokenizer
15.9 STRINGBUILDER
15.10 FECHAS Y HORAS
15.10.1 Aritmética de fechas
15.10.2 Periodos
15.10.3 Formateo de fechas
15.10.4 Parseo de fechas
15.11 EJERCICIOS DEL TEMA API COLECCIONES
15.11.1 Ejercicio 1
15.11.2 Ejercicio 2
15.11.3 Ejercicio 3
15.11.4 Ejercicio 4
15.11.5 Ejercicio 5
15.11.6 Ejercicio 6
15.11.7 Ejercicio 7
15.11.8 Ejercicio 8
15.11.9 Ejercicio 9
19.1 INTRODUCCIÓN
19.2 MOTIVACIÓN
19.3 LAS CONVENCIONES
19.3.1 Ficheros
19.3.2 Código fuente
19.3.3 Formator de comentarios
19.3.4 Declaraciones de variables
19.3.5 Declaración de clases y métodos
19.3.6 Sentencias
19.3.7 Espacios en blanco
19.3.8 Convenciones de nombrado
19.3.9 Buenas prácticas
20 APÉNDICES
21 BIBLIOGRAFÍA:
22 PROGRAMAS MENCIONADOS/ÚTILES
1 INTRODUCCIÓN
2 EJEMPLO SIMPLE
6 FALSEO DE OBJETOS
1 INTRODUCCIÓN
2 COMENTARIOS
3 NOMBRES
4 MÉTODOS
5 CLASES
5.1 RESPONSABILIDAD ÚNICA
5.2 LA COHESIÓN
5.3 CAMBIOS CONTROLADOS
5.3.1 Uso de interfaces
5.4 ESTRUCTURAS DE DATOS Y OBJETOS
5.4.1 Ley de Demeter
5.4.2 DTOs y Registros activos
6 ARQUITECTURA
6.1 INTRODUCCIÓN
6.2 LOS CASOS DE USO
6.3 RETRASAR DECISIONES
6.4 SEPARACIÓN DE VALOR
6.5 UNA ARQUITECTURA DE ALTO NIVEL
6.5.1 Casos de uso
6.5.2 Particionado
6.5.3 Aislamiento
6.6 BASE DE DATOS
6.7 CASO DE ESTUDIO
6.7.1 Clean Bank
6.7.2 Las entidades
6.7.3 Los casos de uso
6.7.4 Boundaries
6.7.5 Gateways de entidades
7 PRINCIPIOS SOLID
8 PRINCIPIOS LEAN
9 BIBLIOGRAFÍA
1. INTRODUCCIÓN
2. DOCUMENTACIÓN DE PROYECTOS
4. DIAGRAMAS DE ACTIVIDAD
4.1. INTRODUCCIÓN
4.2. DIAGRAMAS DE ACTIVIDAD
4.3. DECISIONES/CONDICIONALES
4.4. PASOS PARALELOS
4.5. EJERCICIOS
4.5.1. Ejercicio1
4.5.2. Ejercicio2
4.5.3. Ejercicio3
4.5.4. Ejercicio4
4.5.5. Ejercicio5
4.5.6. Bibliografía
5. DIAGRAMAS DE CLASES
5.1. INTRODUCCIÓN
5.2. DIAGRAMA DE MODELO DE DOMINIO
5.2.1. Una clase en UML
5.2.2. Atributos
5.2.3. Relaciones
5.3. DIAGRAMAS DE IMPLEMENTACIÓN
5.3.1. Una clase en UML
5.3.2. Atributos y métodos
5.4. RELACIONES
5.4.1. Uso
5.4.2. Agregación
5.4.3. Composición
5.4.4. Herencia
5.4.5. Implementación
5.5. INSTANCIAS/OBJETOS
5.5.1. ¿Debemos incluir el constructor y los métodos set/get en el diagrama?
5.6. PAQUETES
5.6.1. Carpetas para casos de uso
5.7. EJERCICIOS
5.7.1. Ejercicio1
5.7.2. Ejercicio2
5.7.3. Ejercicio3
5.7.4. Ejercicio4
5.7.5. Ejercicio5
5.7.6. Ejercicio6
5.8. BIBLIOGRAFÍA
6. DIAGRAMAS DE COMUNICACIÓN
7. DIAGRAMAS DE SECUENCIA
8. DIAGRAMAS DE COMPONENTES
9. DIAGRAMAS DE DISTRIBUCIÓN
9.1. NODOS
9.2. UNA DISTRIBUCIÓN MÁS COMPLEJA: UNA TIENDA VIRTUAL
9.3. EJERCICIOS
9.3.1. Ejercicio1
9.3.2. Ejercicio2
9.3.3. Ejercicio3
9.3.4. Ejercicio4
9.3.5. Ejercicio 5
9.4. BIBLIOGRAFÍA
1 INTRODUCCIÓN
2 DEFINICIÓN SCRUM
2.1 FUNDAMENTOS
3 EL EQUIPO SCRUM
4 EVENTOS
4.1 EL SPRINT
4.2 DAILY SCRUM
4.3 SPRINT PLANNING
4.4 SPRINT REVIEW
4.5 SPRINT RETROSPECTIVE
5 ARTEFACTOS
6 EJEMPLO PRÁCTICO
6.1 ROLES
6.2 ARTEFACTOS
6.3 EVENTOS
6.4 LAS ESTIMACIONES
4 BIBLIOGRAFÍA
1 INTRODUCCIÓN
5 BIBLIOGRAFÍA
1 INTRODUCCIÓN
2 PATRONES
3 BIBLIOGRAFÍA
Capítulo 1
Lenguajes de programación
1 LENGUAJES DE PROGRAMACIÓN
Se conoce como lenguaje de programación a aquel lenguaje
formal diseñado para expresar procesos que pueden ser llevados a cabo por
los ordenadores. Es decir, es un modo práctico que nos permite a los
humanos expresar qué acciones se deben llevar a cabo el equipo.
Difiere de otros lenguajes tales como el natural, utilizado por las
personas, o bien los protocolos de comunicación, que son los lenguajes que
utilizan los propios equipos para comunicarse entre sí.
Por lo tanto, los lenguajes de programación se utilizan a través de
programas, que controlan el comportamiento físico y lógico de un ordenador,
y sirven para expresar algoritmos con precisión. Entendemos por algoritmo el
conjunto de reglas bien definidas, precisas y finitas mediante los cuales el
ordenador es capaz de resolver el problema o tarea planteado.
El lenguaje de programación está formado por un conjunto de símbolos
y reglas sintácticas y semánticas que definen su estructura y el significado de
sus elementos y expresiones. Al proceso por el cual se escribe, se prueba, se
depura, se compila (si es necesario) y se mantiene el código fuente de
un programa informático se le llama proceso de programación.
.1 Fases del
proceso de
programación
Como se ha comentado anteriormente, la palabra programación se
define como el proceso de creación de un programa de computadora,
mediante la aplicación de procedimientos lógicos.
Es importante destacar que se realiza a través de los siguientes pasos:
El desarrollo lógico del programa para resolver un problema
en particular.
Escritura de la lógica del programa empleando un lenguaje de
programación específico (codificación del programa).
Ensamblaje o compilación del programa hasta convertirlo en
lenguaje de máquina.
Prueba y depuración del programa.
Desarrollo de la documentación.
.2 Características
Un lenguaje de programación es muy estricto:
A cada instrucción le corresponde una acción de procesador.
El lenguaje utilizado por el procesador se denomina lenguaje máquina.
Se trata de datos expresados en una secuencia de 0 y 1, tal y como llegan al
procesador.
El lenguaje máquina, por lo tanto, no es comprensible para los seres
humanos, razón por la cual se han desarrollado lenguajes intermediarios sí
comprensibles para el hombre. El código escrito en los lenguajes de
programación a alto nivel se transforma en código máquina en última
instancia para que el procesador pueda procesarlo.
.1 Justificación
Actualmente el mundo de la programación está en constante expansión,
siendo un sector que avanza a velocidad de vértigo y que demanda un
creciente número de especialistas en este ámbito de manera constante. Cada
vez existe un mayor interés tanto profesional como personal por aprender a
codificar mediante lenguajes de programación. Pasando por lenguajes más
recientes y orientados a la web tales como Ruby, Python y JavaScript, o bien
lenguajes con más solera y de propósito general como C, C++, Java, VB
.NET, C#, etc.
Quizá hasta nos preguntemos ¿qué lenguaje deberíamos aprender para
empezar? Si bien es verdad que en caso de realizar esta pregunta a 10
programadores es casi seguro que te obtendríamos respuestas diferentes, aquí
expongo algunos argumentos para que podamos establecer una base que
permita llevar a cabo dicha tipología.
Dependiendo del propósito podríamos elegir uno u otro. Por ejemplo,
para desarrollo web es muy común elegir HTML, CSS, JavaScript, PHP, etc.
Si el objetivo es programar aplicaciones móviles nos decantaríamos por
Objective-C para IOs o Java para Android.
Como inciso, cabe destacar que es posible aprender a pensar como un
puro programador mediante los múltiples cursos gratuitos a tu disposición
que nos brinda Internet. Por ejemplo, en CodeAcademy encontrarás una
batería de cursos de calidad: [Link]
La mayor parte de lenguajes de programación populares como C, Java,
C#, Perl, Ruby, o Python, sirven para hacer más o menos las mismas cosas.
Java, por ejemplo, es un sistema multiplataforma utilizado para aplicaciones
web y applets. Ruby también sirve para hacer grandes aplicaciones web, al
igual que Python, los cuales corren igualmente en Linux o Windows.
El hecho de que los lenguajes estén desarrollados “unos sobre otros”
hace que muchas veces la sintaxis entre ellos resulte idéntica o muy parecida;
así que aprender uno de ellos hará que te sea más fácil aprender el siguiente.
Por ejemplo, imprimir la expresión 'Hello World' en Java y C# es similar.
Java uno de los lenguajes de programación más populares. Es muy útil
para aprender los principios de la programación orientada a objetos que se
usa en otros lenguajes modernos como C#, Python o PHP. Una vez que hayas
aprendido Java, será más fácil aprender estos otros lenguajes.
Java tiene la ventaja de que es un lenguaje muy maduro y con una larga
trayectoria. Hay infinidad de tutoriales, y se usa en una gran cantidad de
entornos, incluido el desarrollo de aplicaciones en Android, o desarrollo web
mediante el Framework de Spring.
Finalmente, para clarificar lo anteriormente expuesto se añade una
clasificación de lenguajes de programación:
Programador de Servidores o Back-end: Python, Ruby,
PHP, Java o .Net.
Programador de clientes o Front-end: HTML, CSS,
Javascript.
Programador móvil: Objective C o Java (para Android).
HTTML / CSS para sitios web móviles.
Programador 3D o de videojuegos: C/C++, OpenGL,
Animación.
Programador de alto rendimiento: C/ C++, Java.
2 CLASIFICACIÓN Y CARACTERÍSTICAS
En realidad, la cantidad de lenguajes de programación existentes es
sencillamente abrumadora en función de sus características u objetivos. Tal
abrumador es el elenco de lenguajes de programación que se hace necesario
establecer unos criterios para clasificarlos.
A continuación, se establecerá una clasificación en función de los 3
criterios globales más reconocidos.
.1 Nivel de abstracción
Decimos que cuanto más alejado esté del código máquina el lenguaje,
mayor nivel de abstracción tendrá. Es decir, la cantidad de capas de
ocultación que hay entre el código que escribimos y el código que la máquina
ejecutará en último término.
Lenguajes de bajo nivel
Primera generación: solo existe uno llamado código
máquina. Se compone de cadenas interminables de
1s y 0s que conforman operaciones que la máquina
puede entender sin interpretación alguna.
Lenguajes de medio nivel
Segunda generación: definen instrucciones para
realizar operaciones sencillas con datos simples o
posiciones de memoria. El lenguaje clave de la
segunda generación es sin duda el ensamblador.
Lenguajes de alto nivel
Tercera generación: la gran mayoría de lenguajes de
programación que se utilizan hoy en día pertenecen a
este nivel de abstracción. En concreto los actuales
lenguajes que cumplen con un paradigma OO
(orientado a objetos) son lenguajes de propósito
general que permiten un nivel de abstracción más alto
y una forma de programar mucho más intuitiva,
entendible y legible. Se acercan a ser considerados
traducciones del lenguaje humano. Algunos ejemplos
son C, C++, C#, Java, etc.
Cuarta generación: son lenguajes creados con un
propósito específico que permite reducir la cantidad
de líneas de código que tendríamos que hacer con
otros lenguajes de tercera generación. Un lenguaje de
este tipo ya tiene incorporadas rutinas, con lo que
solo se tendría que invocar la instrucción que realiza
la operación que necesitamos. Un ejemplo de este
lenguaje es SQL.
Quinta generación: son también conocidos como
lenguajes naturales basados en el conocimiento.
Pueden establecer el problema que hay que resolver y
las premisas y condiciones que hay que reunir para
que la máquina lo resuelva. Este tipo de lenguajes los
podemos encontrar en inteligencia artificial y lógica.
Algunos de los lenguajes de esta generación son
Haskell, Modula 3, etc.
.2 Forma de ejecución
Lenguajes compilados: un programa traductor
(compilador) convierte el código fuente en código objeto y
otro programa (enlazador) unirá el código objeto del
programa con el código objeto de las librerías necesarias
para producir el programa ejecutable.
Lenguajes interpretados: ejecutan las instrucciones
directamente, sin que se genere código objeto, para ello es
necesario un programa intérprete en el SO o en la propia
máquina donde cada instrucción es interpretada y ejecutada
de manera independiente y secuencial. La principal
diferencia con el anterior es que se traducen a tiempo real
solo las instrucciones que se utilicen en cada ejecución, en
vez de interpretar todo el código, se vaya a utilizar o no.
Lenguajes virtuales: tienen un funcionamiento muy similar
al de los lenguajes compilados, pero a diferencia de éstos,
no es código objeto lo que genera el compilador, sino un
bytecode que puede ser interpretado por cualquier
arquitectura que tenga la máquina virtual que se encargará
de interpretar el código bytecode generado para ejecutarlo
en la máquina; aunque en ejecución lenta (como los
interpretados), tienen la ventaja de poder se multisistema y
así un mismo código bytecode sería válido para cualquier
máquina.
.3 Paradigma de programación
Es un enfoque muy particular para la construcción de software, un
estilo de programación que facilita la tarea de programación o añade mayor
funcionalidad al programa dependiendo del problema que haya que abordar.
Todos estos paradigmas pertenecen a lenguajes de alto nivel y es común que
un lenguaje pueda usar más de un paradigma de programación.
Paradigma imperativo: describe la programación cómo una
secuencia de instrucciones que cambian el estado del
programa, indicando cómo realizar una tarea.
Paradigma declarativo: especifica o declara un conjunto de
premisas y condiciones para indicar qué es lo que hay que
hacer y no necesariamente cómo hay que hacerlo.
Paradigma procedimental: el programa se divide en partes
más pequeñas, llamadas funciones y procedimientos, que
pueden comunicarse entre sí. Permite reutilizar código ya
programado y solventa el problema de la programación
spaguetti.
Paradigma orientado a objetos: encapsula el estado y las
operaciones en objetos, creando una estructura de clases y
objetos que emula un modelo del mundo real, donde los
objetos realizan acciones e interactúan con otros objetos.
Permite la herencia e implementación de otras clases,
pudiendo establecer tipos para los objetos y dejando el código
más parecido al mundo real con esa abstracción conceptual.
Paradigma funcional: evalúa el problema realizando
funciones de manera recursiva, evita declarar datos haciendo
hincapié en la composición de las funciones y en las
interacciones entre ellas.
Paradigma lógico: define un conjunto de reglas lógicas para
ser interpretadas mediante inferencias lógicas. Permite
responder preguntas planteadas al sistema para resolver
problemas.
3 OBTENCIÓN DE CÓDIGO EJECUTABLE
El programa, escrito en el lenguaje que sea y ejecutado en la
arquitectura que queramos, necesita ser traducido para poder ser ejecutado
(con la excepción del lenguaje máquina). Por lo que, aunque tengamos el
código de nuestro programa escrito en el lenguaje de programación escogido,
no podrá ser ejecutado a menos que lo traduzcamos a un idioma que nuestra
máquina entienda.
.1 Forma de ejecución
El código de nuestro programa es manejado mediante programas
externos comúnmente asociados al lenguaje de programación en el que está
escrito nuestro programa, y a la arquitectura en donde queremos ejecutarlo.
Para ello podemos definir los distintos códigos por los que pasará el
programa antes de ser ejecutado por el sistema:
Código fuente: es un conjunto de instrucciones escritas en
un lenguaje de programación determinado. Es decir, es el
código en el que nosotros escribimos nuestro programa.
Código objeto: es el código resultante de compilar el
código fuente. Si se trata de un lenguaje de programación
compilado, el código objeto será código máquina, mientras
que, si se trata de un lenguaje de programación virtual, será
el bytecode.
Código ejecutable: es el resultado de enlazar nuestro
código objeto con las librerías. Este código ya es nuestro
programa ejecutable, que se ejecutará directamente en
nuestro sistema o sobre una máquina virtual en el caso de
los lenguajes de programación virtuales.
Cabe destacar que, si nos encontramos programando en un lenguaje de
programación interpretado, nuestro programa no pasaría por el compilador y
el enlazador, sino que solo tendríamos un código fuente que pasaría por un
intérprete interno del SO o de la máquina que realizaría la compilación y
ejecución línea a línea en tiempo real.
.2 Compilación
Aunque el proceso de obtener nuestro código ejecutable pase tanto por
un compilador como por un enlazador, se suele llamar al proceso completo
“compilación”.
Todo este proceso se lleva a cabo mediante dos programas: el
compilador y el enlazador. Mientras que el enlazador solamente une el código
objeto con las librerías, el trabajo del compilador es mucho más complejo.
Fases de compilación
Existen diversas fases de compilación o también llamadas etapas de la
compilación que se ilustrarán en la siguiente imagen:
.1 El compilador
El compilador es básicamente un programa que transforma el código
fuente en un código que es directamente ejecutable por el sistema.
Generalmente generan un código binario específico para un sistema operativo
y procesador, y por tanto está optimizado para ejecutarse en el mismo lugar
donde se ha compilado.
Gráficamente, el proceso de compilación se podría representar así:
Compilador
El código fuente es un fichero de texto cuyo contenido es entendible
por los programadores, pero no por el sistema. Es el compilador quien
transforma ese código en algo entendible para el sistema. El código binario
generado contiene unos y ceros que representan una secuencia de
instrucciones que pueden ejecutarse directamente por el procesador, en este
caso uno de la familia x86.
Ventajas
Obviamente, un lenguaje compilado genera código cuya ejecución es
mucho más óptima, ya que se crea de forma específica para esa máquina.
Además, a diferencia de los intérpretes, una vez compilado el fichero de
código fuente no hay que volver a hacerlo. Ese programa ejecutable que se ha
generado se puede distribuir en otras máquinas siempre que sean del mismo
tipo.
Desventajas
El programa que ya se ha compilado solo puede ejecutarse en un tipo de
máquina y por tanto no puede portarse a otro tipo de sistema o procesador. En
todo caso puede llevarse el código fuente a otra máquina y utilizar ahí un
compilador específico para la misma. Aparte de eso, para ejecutar estos
programas siempre necesitaremos el paso previo de la compilación.
Cabe mencionar que para superar esa rigidez también existen cross-
compilers que pueden generar código para distintos sistemas.
Ejemplos:
Los ejemplos más claros de lenguajes compilados pueden ser:
Ensamblador, el lenguaje que más se acerca al lenguaje del
procesador
C, más alto nivel que el ensamblador, pero de bajo nivel
respecto al resto
C++, para muchos el ideal que reúne rendimiento/orientación
a objetos por lo que ofrece eficiencia en ejecución y en
mantenimiento. De ahí su uso para desarrollar sistemas
operativos, gestores de bases de datos, videojuegos,
.2 El intérprete
Otra manera de ejecutar programas es a través de un intérprete, que no
es más que un programa del sistema que se encarga de ejecutar el código. El
intérprete toma el código fuente y se encarga de ejecutarlo directamente,
transformado las instrucciones del programador en instrucciones entendibles
para el sistema subyacente. Por tanto, no se genera un fichero binario a partir
del código fuente, y la ejecución parece directa.
.1 Análisis
La fase de análisis define los requisitos del software que hay que
desarrollar. Inicialmente, esta etapa comienza con una entrevista al cliente,
que establecerá lo que quiere o lo que cree que necesita, lo cual nos dará una
buena idea global de lo que necesita, pero no necesariamente del todo
acertada.
Aunque el cliente crea que sabe lo que el software tiene que hacer, es
necesaria una buena habilidad y experiencia para reconocer requisitos
incompletos, ambiguos, contradictorios o incluso necesarios. Es importante
que en esta etapa del proceso de desarrollo se mantenga una comunicación
bilateral, aunque es frecuente encontrarse con que el cliente pretenda que
dicha comunicación sea unilateral, es necesario un contraste y un consenso
por ambas partes para llegar a definir los requisitos verdaderos del software.
Para ello se crea un informe ERS (Especificación de Requisitos del Sistema)
acompañado de los diagramas de caso de uso, donde se especifican los
actores y escenarios del proyecto.
.2 Diseño
En esta etapa se pretende determinar el funcionamiento de forma global
y general, sin entrar en detalles. Uno de los objetivos principales es diseñar el
sistema en función de los recursos del mismo, tanto físicos como lógicos.
En esta fase se crearán los diagramas de clases y de Entidad/Relación
así como de secuencia para definir la funcionalidad del sistema.
Se debe especificar también el formato de la información de entrada y
salida, las estructuras de datos y la división modular.
.3 Codificación
Una vez definido el software el cometido es programarlo. Gracias a las
etapas anteriores, el programador contará con un análisis completo del
sistema que hay que codificar y con una especificación de la estructura básica
que se necesitará, por lo que en un principio solo habría que traducir el
cuaderno de carga en el lenguaje deseado para culminar la etapa de
codificación, pero esto no siempre es así, las dificultades son recurrentes
mientras se modifica. Por supuesto que cuanto más exhaustivo haya sido el
análisis y el diseño, la tarea será más sencilla, pero nunca está exento de
necesitar un reanálisis o un rediseño al encontrar un problema al programar el
software.
.4 Pruebas
Con una doble funcionalidad, las pruebas buscan confirmar que la
codificación ha sido exitosa y el software no contiene errores, a la vez que se
comprueba que el software hace lo que debe hacer, que no necesariamente es
lo mismo.
No es un proceso estático, y es usual realizar pruebas después de otras
etapas, como la documentación. Generalmente, las pruebas realizadas
posteriormente a la documentación se realizan por personal inexperto en el
ámbito de las pruebas de software, con el objetivo de corroborar que la
documentación sea de calidad y satisfactoria para el bueno uso de la
aplicación.
En general, las pruebas las realiza, idílicamente, personal diferente al
que codificó la aplicación, con una amplia experiencia en programación,
personas capaces de saber qué condiciones de un software puede fallar de
antemano sin un análisis previo.
.5 Documentación
Por norma general tiene dos caras: la documentación disponible para el
usuario y la documentación destinada al propio equipo de desarrollo.
La documentación para el usuario debe mostrar una información
completa y de calidad que ilustre mediante los recursos más adecuados cómo
manejar la aplicación. Una buena documentación permitiría a un usuario
cualquiera comprender el propósito y el modo de uso de la aplicación sin
información previa o adicional.
Por otro lado, la documentación técnica destinada a equipos de
desarrollo, que explica el funcionamiento interno del programa, haciendo
especial hincapié en explicar la codificación del programa. Se pretende con
ello permitir a un equipo de desarrollo que cualquiera pueda entender el
programa y modificarlo si fuera necesario.
.6 Explotación
Una vez que tenemos nuestro software, hay que prepararlo para su
distribución. Para ello se implementa el software en el sistema elegido o se
prepara para que se implemente por sí solo de manera automática.
Cabe destacar que en caso de que nuestro software sea una versión
sustitutiva de un software anterior es recomendable valorar la convivencia de
sendas aplicaciones durante un proceso de adaptación.
.7 Mantenimiento
Un software necesita siempre un mantenimiento continuado. En esta
fase se arreglan los fallos o errores que suceden cuando el programa ya ha
sido implementado en un sistema y se realizan las ampliaciones necesarias o
requeridas.
Cuando el mantenimiento que hay que realizar consiste en una
ampliación, el modelo en cascada suele volverse cíclico, por lo que,
dependiendo de la naturaleza de la ampliación, puede que sea necesario
analizar los requisitos, diseñar la ampliación, codificar la ampliación,
probarla, implementarla y, por supuesto, dar soporte de mantenimiento sobre
la misma, por lo que al final este modelo es recursivo y cíclico para cada
aplicación y no es un camino rígido de principio a fin.
Capítulo 2
Lenguaje Java
1 INTRODUCCIÓN
Sun Microsystems era una empresa
estadounidense que destacaba como fabricante de
servidores de alto rendimiento tanto el hardware
como el software, con su arquitectura y sistema
operativo Unix propio.
A principio de los años 90 en Sun
Microsystems estaban tratando de desarrollar un
entorno para poder programar pequeños
dispositivos y electrodomésticos con la finalidad de
que fueran programables y pudieran interactuar
entre sí. No en vano Sun siempre le ha querido dar
mucha relevancia a las redes hasta el punto de que
uno de sus lemas era “The net is the computer”;
toda una profecía de lo que hoy se conoce como
computación en la nube.
Siguiendo en esa línea visionaria en Sun trataban de crear un lenguaje
de programación que pudiera funcionar en cualquier tipo de plataforma:
“Write once, run everywhere” era el primer lema de ese nuevo lenguaje. Un
par de ideas estaban claras: debía ser orientado a objetos, pero sin ser tan
complicado como C++. Dicho y hecho, el proyecto empezó llamándose oak
(roble) y más tarde se convirtió en un homenaje a los sufridos programadores
cuyo principal combustible es un buen café: Java (en inglés café del bueno),
un lenguaje de programación interpretado, similar a C++, pero quitándole lo
difícil como la herencia múltiple y los temidos punteros. Todo ello para
facilitar su aprendizaje, uso y extensión.
Sin embargo, lo que en principio iba a ser un lenguaje para lavadoras
tuvo la suerte de coincidir con un fenómeno tecnológico: el desarrollo de la
web. Java cogió la ola de los primeros navegadores y facilitó que unos
programas llamados applets se pudieran ejecutar en cualquier plataforma sin
necesidad de hacer versiones diferenciadas. Ello contribuyó enormemente a
la popularidad de un lenguaje que poco a poco fue derivando hacia un
lenguaje de propósito general, hasta el punto de convertirse en una referencia
como lenguaje de servidores en aplicaciones empresariales.
.1 Java2
A partir de la segunda versión de
Java2, el lenguaje empezó a ser algo más
que un juguete para especializarse en
distintas ramas: aplicaciones para la web,
aplicaciones empresariales, aplicaciones
para smartcards y finalmente Java vuelve a
sus orígenes convirtiéndose en la opción
más utilizada para los pequeños
dispositivos: móviles, tablets, etc.
Al igual que hicieran los Applets en
los navegadores, Java puede estar presente
en cualquier móvil gracias a los Midlets. E
incluso uno de los sistemas más pujantes
para móviles como es Android utiliza java
como lenguaje de desarrollo de aplicaciones.
Actualmente Sun Microsystems, una
empresa que desarrolla hardware, sistemas
operativos, lenguajes de programación, ha
sido comprada por la todopoderosa Oracle
que se dedica principalmente a su famoso
gestor de bases de datos. Oracle tenía sus
propios gestores de aplicaciones y otros productos y sin embargo ha tomado
un atajo y directamente se ha comprado todo: la máquina, el sistema
operativo, el lenguaje de programación y los gestores de aplicaciones.
En cualquier caso, Java es un lenguaje que resulta imprescindible
aprender ya sea por el desarrollo web o por la rama de móviles. Lenguajes
hay muchos, pero este es de los que pone las lentejas encima del plato (o los
granos de café en la cafetera).
...1 INSTALACIÓN
DE ECLIPSE
Previamente debemos tener el JDK instalado. Para Eclipse basta con
descargarse la versión Eclipse EE (Enterprise Edition) de la web de eclipse
[Link] Una vez descargado, basta con descomprimirlo y ya
tendremos eclipse funcionando.
Lo primero que nos preguntará Eclipse es dónde queremos situar
nuestro workspace o área de trabajo. Se trata de un directorio local donde
guardará los proyectos. Por defecto se utiliza una carpeta workspace en la
carpeta del usuario, pero posteriormente puede cambiarse.
Lo primero que haremos es aprender a compilar y a ejecutar y una vez
le demos un vistazo al lenguaje daremos un repaso a una serie de
herramientas básicas de las que disponemos en el JDK: depurador, generador
de documentación, etc.
...2 CREACIÓN
DE UN
PROYECTO
Este es el aspecto de Eclipse, con la ventana de bienvenida cerrada y
con un proyecto ya creado.
Para crear un proyecto debemos ir al menú File > New > Maven Project
Si no vemos esa opción directamente tenemos que ir a File > New >
Other… y buscar ahí Maven Project.
A continuación, pulsamos “Siguiente” y en la ventana de selección de
archetype (tipo de proyecto), seleccionamos maven-archetype-quickstart
.1 Introducción
Como en cualquier lenguaje vamos a empezar haciendo un ejemplo
simple de programación: el clásico HolaMundo o Hello World, que lo único
que hace es mostrar un texto por la pantalla.
De esta manera podemos ver el tamaño mínimo que tiene un programa
hecho en Java. Siendo Java un lenguaje orientado a objetos, todo el código
debe estar metido en una clase:
public class NombreDeClase {
}
Ahora no hace falta entender todas las palabras del mismo. Si se declara
un método así en una clase, el programa podrá ser ejecutado a través de esa
clase. Por ejemplo, en la siguiente clase:
public class Hello {
public static void main (String args[]) {
}
}
2.1 Paquetes
o
packages
Generalmente las clases Java se agrupan en paquetes. Los paquetes nos
permiten organizar las clases, algo muy conveniente en proyectos grandes.
Para establecer que una clase pertenece a un paquete, debemos empezar
la primera línea de su código con la declaración package:
package [Link];
2.2 Importando
paquetes
Una clase Java, puede importar clases desde otros paquetes para utilizar
determinadas clases que le sean útiles. Para hacerlo, el programa debe usar la
orden import, siempre entre la declaración de package y previa al inicio de la
clase:
import [Link];
Por ejemplo
import [Link];
NOTA
Import estático
Otra manera de hacer un import poco usual es importar únicamente
un método de una clase, pero ojo, este método debe ser static. Para eso se
añade la palabra static en el import, además de la clase y el método en
cuestión:
import static [Link];
Al hacer ese import, en el código no necesitaremos poner
[Link], sino que bastará con poner asList.
2.3 Comentarios
Dentro del código se pueden y deben introducir comentarios para
aclarar determinadas partes del código o bien como manera de documentar el
programa.
Existen dos tipos de comentarios en Java:
1. Comentarios de una sola línea: comienza con //
// Esto es un comentario
/*
* Aquí ponemos lo que queramos
* bla bla
*/
2.4 Ejercicios
del tema
Introducción
Para los proyectos, utiliza el package: [Link]
2.4.1 Ejercicio
1
¿Cuáles son las propiedades principales del lenguaje de programación
java? ¿A qué lenguajes se parece?
2.4.2 Ejercicio
2
Crea con Eclipse un proyecto llamado HelloWorld, que tenga una clase
llamada [Link] como esta:
package [Link];
public class HelloWorld {
public static void main(String[] args) {
[Link]("Hello World!");
}
}
Compílalo y ejecútalo.
2.4.3 Ejercicio
3
Escribe una versión propia del programa HelloWorld y llámalo
[Link]. Identifica cada parte del programa con un comentario.
Cambia el mensaje y personalízalo. Ejecútalo.
2.4.4 Ejercicio
4
Abre el programa [Link] y comenta la línea de package:
// package [Link];
2.4.5 Ejercicio
5
Abre el proyecto HelloWorld. Modifica la declaración de la clase:
public class HelloWorld {
y pon esto:
public class Hello {
2.4.6 Ejercicio
6
Crea con Eclipse un proyecto llamado Arguments, que tenga una clase
llamada [Link] como esta:
package [Link];
/**
* Arguments, shows how to deal with program arguments
* @author Eugenia Pérez, Pello Altadill
*/
public class Arguments {
public static void main(String[] args) {
[Link]("Argument is ");
[Link](args[0]);
}
}
Una vez hecho, para ejecutar la clase ve al menú Run As > Run
configurations y verás lo siguiente:
En esa ventana se configuran opciones de ejecución. Ve a la pestaña e
introduce alguna palabra como argumento en el cuadro Program arguments:
2.4.7 Ejercicio
7
Sobre cualquiera de los proyectos que tengas abiertos, en una pantalla
de código utiliza la combinación de teclas: Ctrl-Shift-F ¿Qué ocurre?
3 VARIABLES
.1 Introducción
En todo programa se suelen manejar valores de distintos tipos con los
que se hacen operaciones. Algunos valores son fijos, pero otros pueden variar
y por eso se almacenan en variables.
Al ser Java un lenguaje fuertemente tipado, nos obliga a indicar el tipo
de cada variable al definirla. Por ejemplo, si definimos una variable llamada
counter lo tendríamos que hacer así:
int counter;
NOTA
Inicialización de variables
Las convenciones Java recomiendan inicializar una única variable
por línea de código, pero, aun así, es posible definir varias variables del
mismo tipo en una única sentencia:
int a,b,c;
int i = 0, j, k = 5;
Lo que no se puede hacer es mezclar:
int a, float b;
.2 Nombrado
de
variables
No se puede elegir un nombre cualquiera para las variables Java.
Existen unas normas muy simples.
Solo pueden empezar por letras, $ o _
A partir de la primera letra pueden usarse números
No pueden usarse las palabras reservadas Java. Ver
apéndice.
.3 Tipos
primitivos
En java como en cualquier otro disponemos de variables de tipos
básicos o primitivos para representar número enteros, reales, caracteres
sueltos y cadenas de caracteres e incluso las variables booleanas cuyo valor
solo puede ser verdadero o falso. Estos son los tipos básicos en java:
byte
short
int
long
float
double
char
boolean
Veamos las variables con más detalle.
.4 Tipos
numéricos
Sirven para almacenar números, enteros o con decimales, negativos o
positivos.
Tipo Longitud Valor Ejemplo
por
defecto
byte 1 byte 0 4
short 2 bytes 0 4
NOTA
Base numérica
Si queremos representar números en bases distintas a 10, podemos
hacerlo añadiendo un prefijo especial a los literales:
1. Números en base octal; usamos un 0 como prefijo: 047
2. Números en base hexadecimal; usamos 0x o 0X como
prefijo: 0xdead
3. Números en binario; usamos 0b o 0B como prefijo:
0b01101
NOTA
Legibilidad de números
Para mejorar la legibilidad de los números se puede utilizar guiones
bajos con los que se puede indicar las fracciones de mil, un millón, etc.:
1000 = 1_000
1000000 = 1_000_000
Los subrayados se pueden poner en cualquier parte siempre que no se
pongan al principio o al final del mismo o junto al punto que indica un
decimal.
.4.1 Ejemplo
de
Números
enteros
/**
* clase ValoresEnteros
* Muestra la declaración de tipos numéricos enteros básicos:
* byte : 1 byte
* short : entero corto, 2 bytes
* int : enter, 4 bytes
* long : entero largo, 8 bytes
*
* @author Eugenia Pérez, Pello Altadill
*/
public class ValoresEnteros {
public static void main (String args[]) {
// Declaración de variables: tipo nombre;
byte dias;
short contador;
// Declaración y asignación de valor
// tipo nombre = valor inicial;
short resultado = 42;
// Declaración de varias variables del mismo tipo
short codigo, edad, media;
// Declaración y asignación a varias a la vez:
int i, j, k;
i = j = k = 0;
// El tipo long
long sueldoFutbolista = 23489343;
long valorGoogle = 666000666;
// Vamos a probar a mostrarlos por pantalla: concatenamos con +
[Link]("El sentido de la vida es: " + resultado);
[Link]("El sueldo de un futbolista medio es: " + sueldoFutbolista);
}
}
.4.2 Ejemplo
de
Valores
reales
/**
* clase ValoresReales
* Muestra la declaración de tipos numéricos reales o de coma flotante:
* float : real precisión simple, 4 bytes
* double : real precisión doble, 8 bytes
*
* @author Eugenia Pérez, Pello Altadill
*/
public class ValoresReales {
public static void main (String args[]) {
// En los tipos reales debemo usar un . en lugar de , para las decimales
float temperatura;
// Al asginar valor le ponemos la F para distinguir del tipo double
float peso = 78.9F;
double saldoCuentaCorriente = 3423343.43D;
// Los valores altos se pueden abreviar:
// esto sería 4.6 multiplicado por 10 elevado a 9.
double masaJupiter = 4.6E+9D;
// Vamos a probar a mostrarlos por pantalla: concatenamos con +
[Link]("Tu peso es : " + peso + ", y tu saldo: " + saldoCuentaCorriente);
[Link]("La masa de Jupiter: " + masaJupiter);
}
}
.5 Caracteres
Son valores que representan un simple carácter o una simple letra. Por
ejemplo:
char letter = ’a’;
char space = ’’;
.5.1 Ejemplo
de
Caracteres
sueltos
/**
* clase ValoresCaracteres
* Muestra la declaración de booleanos: son variables que solo
* pueden contener un caracter, encerrado en comillas simples
* por ejemplo 'a'
*
* @author Eugenia Pérez, Pello Altadill
*/
public class ValoresCaracteres {
public static void main (String args[]) {
// Los caracteres no son más que una letra
char caracter = 'A';
char ultima = 'z';
// Los especiales comienzan por \
char nuevaLinea = '\n';
char tabulacion = '\t';
char retornoCarro = '\r';
char comillaSimple = '\'';
char contrabarra = '\\';
char dobleComillas = '\"';
char formFeed = '\f';
// Vamos a probar a mostrarlos por pantalla: concatenamos con +
[Link]("Primera letra " + caracter + " y última: " + ultima);
[Link]("Con la \\ usamos caracteres especiales");
[Link]("Que parezca un \"accidente\" ");
[Link]("Vamos a saltar \n y ahora otra vez: " + nuevaLinea);
[Link](tabulacion + " Vamos a ver: " + nuevaLinea + " y ahora \r");
}
}
.6 Booleanos
Son variables cuyo valor solamente puede ser true o false, verdadero o
falso. Esos valores resultan esenciales en la toma de decisiones de los
programas, tanto para establecer condiciones como para crear iteraciones o
bucles.
boolean javaIsEasy = true;
.6.1 Ejemplo de
Valores
booleanos:
verdadero/falso
/**
* clase ValoresBooleanos
* Muestra la declaración de booleanos: son variables que solo
* pueden tener dos posibles valores: true (verdadero) o false (falso)
*
* @author Eugenia Pérez, Pello Altadill
*/
public class ValoresBooleanos {
public static void main (String args[]) {
// Solo pueden ser true o false
boolean terminado = false;
boolean aprobar = true;
boolean resultado = aprobar;
// Vamos a probar a mostrarlos por pantalla: concatenamos con +
[Link]("Este programa ha terminado? " + terminado);
[Link]("Aprobaré la asignatura? " + aprobar);
}
}
.7 Wrappers
de
primitivos
Cada tipo primitivo dispone de una clase Java que la representa. En el
código podemos por tanto utilizar estas clases en lugar de los tipos
primitivos:
byte → Byte
short → Short
int → Integer
long → Long
float → Float
double → Double
char → Char
boolean → Boolean
¿Qué ventaja nos aporta esto? Ahora es un objeto, y como veremos más
adelante, al tener un objeto dispondremos de propiedades y métodos extra.
NOTA
Reference types
Esta es otra forma para referirse a tipos que no son primitivos, es decir,
clases.
.8 Cadenas
o
Strings
El tipo String es un conjunto de caracteres o texto. En Java un String
NO es un primitivo sino una clase, pero aun así lo introducimos aquí por ser
un tipo de valor esencial y de gran utilidad.
Los valores de cadenas en Java se crean simplemente poniendo testo
entre comillas dobles:
String name = “Eugenia Pérez”;
NOTA
El pool de Strings
.8.1 Ejemplo
de
Cadenas
o
Strings
/**
* clase ValoresCadenas
* Muestra la declaración de Cadenas: son variables que
* contienen más de un carácter: una palabra una frase, etc...
* Para esto no existen tipos primitivos y se usa una clase
* llamada String
*
* Una clase, como ya se verá más adelante es mucho más que un tipo
* de dato. Es un tipo complejo que tiene propiedades y métodos.
*
* @author Eugenia Pérez, Pello Altadill
*/
public class ValoresCadenas {
public static void main (String args[]) {
String nombre;
String frase = "A quien madruga, patada en los cojones";
String presidente;
// Podemos iniciarla con una cadena vacía
String otraFrase = "Solo quiero que seamos \"amigos\"";
int edad = 666;
presidente = "Cthulhu";
nombre = "Optimus Prime";
// Los especiales comienzan por \
char nuevaLinea = '\n';
char tabulacion = '\t';
// Podemos unir una cadena y un carácter con el operador de concatenación.
frase = frase + nuevaLinea;
// Vamos a probar a mostrarlos por pantalla:
[Link](frase);
.9 Ámbito
de
variables
Cuando hablamos de ámbito de variables, nos referimos al alcance que
tiene una variable en un programa.
En los programas que hemos hecho hasta ahora, de un solo método y
sin bloques, el ámbito de una variable alcanza todo el método. Es decir, si
una variable se declara al principio del método main:
public class AnyClass {
public static void main (String args[]) {
int anyNumber = 0;
// …
// El programa sigue…
// …
// y sigue…
[Link]("Valor de la variable " + anyNumber);
}
}
.9.1 Ámbito
en
bloques
Más adelante conoceremos estructuras como las condiciones, los bucles
e incluso otros métodos. Estas estructuras se caracterizan por utilizar los
símbolos { y } para delimitar su ámbito.
…
if (x == 2) {
int a = 0;
// la variable a solo existe dentro de este bloque
}
NOTA
Resumen del ámbito o scope de variables
Para una variable local abarca hasta desde su declaración el
final de su bloque.
Para una variable de instancia desde su declaración hasta que
la instancia se destruye
Para una variable de clase (static) desde su declaración hasta
que termina el programa.
NOTA
Destrucción de objetos y garbage collector
Java se encarga de gestionar la memoria de su propia máquina
virtual. En cuanto la variable que referencia a una instancia desaparece o
no quedan referencias a ese objeto en el ámbito, ese objeto queda marcado
para ser destruido.
Aunque podemos invocar el método [Link](), esto solo sirve
como sugerencia a la JVM para que haga limpieza.
En las clases podemos añadir un método especial llamado finalize:
public class SimpleClass {
...
protected void finalize() {
}
}
Este método es invocado de forma automática cuando el objeto es marcado
para ser destruido.
NOTA
Clase, Objetos/Instancia y referencias
En la programación orientada a objetos es crucial distinguir estos
conceptos:
Clase = Una clase es una definición de una entidad con sus
propiedades y métodos. En Java todos los ficheros que componen un
proyecto se componen de una clase, y por tanto un proyecto Java son un
conjunto de clases. Por ejemplo, puede definir la clase Persona con los
atributos nombre, edad y los métodos andar y dormir.
public class Persona {
String nombre;
int edad;
public void andar() { … }
public void dormir() { … }
}
Objeto/Instancia = Un objeto o instancia es cuando creamos algo
que es de determinada clase. Por ejemplo, puedo crear un objeto/instancia
de la clase Persona que se llama Fermín tiene 23 años. Es un ejemplo
concreto de esa clase:
Persona p = new Persona();
[Link] = “Fermín”;
[Link] = 23;
Referencia = Es una variable a la que le hemos asignado un
objeto/instancia o que apunta a un objeto/instancia. Por ejemplo, p y ppp
son referencias al mismo objeto:
Persona p = new Persona();
[Link] = “Fermín”;
[Link] = 23;
Persona ppp = p;
.10 Ejercicios
del tema
Variables
Para los proyectos, utiliza el package: [Link]
.10.1 Ejercicio
1
¿Qué tipo de variable usarías para estos valores?
a. Tu edad
b. La temperatura de un día de invierno
c. Un precio en pesetas
d. Un sueldo en euros
e. Tu altura en centímetros
f. La nota de un examen
g. Una letra del alfabeto
h. El nombre de un alumno
.10.2 Ejercicio
2
Crea un proyecto con una clase llamada Variables en el que declares todas las
variables del ejercicio anterior, usando el tipo más adecuado y un nombre de
variable descriptivo.
.10.3 Ejercicio
3
Crea un proyecto con una clase llamada BasicVariables en el que definas e
inicialices una variable de todos los tipos básicos de java.
.10.4 Ejercicio
4
Depura los dos proyectos anteriores y comprueba paso a paso los valores que
toman las variables. Para ello, abre su código, introduce un breakpoint
haciendo click en el lateral del editor y utiliza Debug en lugar de Run para
ejecutar el programa:
line = [Link]();
.10.6 Ejercicio
6
Crea un proyecto con una clase llamada NumericArguments en el que definas
todos los tipos de datos numéricos (como en el anterior) y asígnales un valor
desde los argumentos de main (args[0], args[1],…).
Recuerda que para pasar los argumentos deberás ir al menú Run As > Run
configurations…
l. LABESTIA
m. labestia
n. la-bestia
o. brujería
4 ENTRADA/SALIDA BÁSICA
.1 Introducción
Es necesario presentar este tema para poder hacer programas algo más
útiles que sean capaces de recibir una entrada variable en el momento de
ejecución. De esa forma podemos hacer que el mismo programa sirva para
distintos parámetros, sin necesidad de reescribir el programa.
.2 Conversión
de String a
otros tipos
Sea el método que sea el utilizado, lo que se lee como argumento o por
consola está en formato String, así que en algunos casos es necesario llevar a
cabo una conversión de cadenas de caracteres a números. Para ello podemos
usar métodos de conversión desde el tipo String al tipo deseado, por ejemplo:
int number = [Link](args[0]);
.3 Paso de
argumentos
El paso de argumentos consiste en que, en el momento de ejecutar el
programa se le pasan unos valores al mismo. Esos valores son los argumentos
y son precisamente los que se asignan en la variable args de un programa
principal
public class ClassName {
public static void main (String args[]) {
}
}
Valor
Ahí se pasan varios tipos de argumentos. Java los captura todos como
String, y los almacena así. Luego será deber del programador comprobar si
efectivamente los argumentos están ahí y convertirlos a otros tipos si es
necesario. En el array args, quedarían así:
Índice 0 1 2 3
etc…
.4 Paso de
argumentos
en Eclipse
Veamos cómo hacer el paso de argumentos en un entorno como
Eclipse. Sea cual sea el entorno, todos ellos tienen alguna forma de ejecución
en la que se nos permite incluir argumentos antes de llevar a cabo la
ejecución. En Eclipse se usa la opción Run Configurations
Para ejecutar la clase ve al menú Run As> Run configurations y verás
lo siguiente:
.4.1 Ejemplo de
Argumentos
por consola
/**
* clase ValoresCadenasArgumentos
* Clase que muestra la declaración de variables de cadenas o Strings
* y cómo pasarles lo que viene como argumento. Los argumentos se trasvasan
* a través del parámetro args[] de la función main. Ese parámetro es un conjunto
* de Strings o Cadenas: args[0] args[1] args[2] …
*
*
* @author Eugenia Pérez, Pello Altadill
*/
public class ValoresCadenasArgumentos {
public static void main (String args[]) {
// En el caso de que el argumento sea una frase al ejecutar
// el programa la pasaríamos así:
// C:\jdk>java ValoresCadenasArgumentos "Dios le ayuda"
String frase = "A quien madruga, " + args[0];
[Link]("El primer argumento es: " + args[0]);
[Link]("La frase final: \n" + frase);
}
}
.4.2 Ejemplo de
Argumentos
por consola
y
conversión
/**
* clase ValoresEnteros
* Muestra la declaración de tipos numéricos enteros básicos:
* y cómo pasarles lo que viene como argumento. En el caso
* de los enteros HAY QUE CONVERTIR lo que viene por el argumento
* porque viene como un String!
*
* @author Eugenia Pérez, Pello Altadill
*/
public class ValoresEnterosArgumentos {
public static void main (String args[]) {
// Declaración de variables: tipo nombre;
int dias;
short contador;
String diasString;
// Así no hay problemas porque son del MISMO TIPO
diasString = args[0];
// Atención a la CONVERSIÓN. Utilizamos la clase Integer,
// y sú método para convertir de String a int
dias = [Link](args[0]);
// Con los short y con cualquier otro tipo básico hariamos
// lo mismo, usar su clase correspondiente y la misma función:
contador = [Link](args[0]);
// ATENCIÓN: si lo que pasamos como argumento NO ES un entero
// el programa casca irremediablemente y vomita una excepción
// Vamos a probar a mostrarlos por pantalla: concatenamos con +
[Link]("El total de días es: " + dias);
[Link]("El contador queda así: " + contador);
}
}
.5 Lectura
de
valores
por
consola
Este es la segunda forma de que el programa reciba valores o
parámetros desde el exterior, solicitándolos al usuario. Para eso el programa
debe hacer lo siguiente:
1. Sacar un mensaje por pantalla solicitando un dato.
2. Usar alguna clase específica para la lectura de datos por
pantalla
3. Tomar el dato y convertirlo si es preciso.
Existen distintas librerías para leer datos por pantalla. Veamos un par:
Console
Utilizando la clase Console, creamos una instancia y ya podemos leer lo
que el usuario introduce por teclado:
Console consola = [Link]();
String lectura;
lectura = [Link]();
Scanner
Scanner es otra clase de la que disponemos en Java, nos permite la
misma funcionalidad que Console y además es capaz de leer distintos tipos.
Veamos este simple ejemplo, donde se solicita al usuario por pantalla que
introduzca un valor, se lee, se convierte a byte y se muestra:
Scanner readFromConsole = new Scanner([Link]);
String line = "";
[Link]("Enter a byte:");
byte simpleByte = [Link]();
// Esto equivale a:
//line = [Link]();
//byte simpleByte = [Link](line);
[Link]("You introduced: " + simpleByte);
.5.1 Ejemplo
de
Entrada
por
consola
package [Link];
//Tenemos que importar las clases necesarias para su uso
import [Link];
import [Link];
/**
* Clase EntradaPorConsola Muestra como se solicitan datos al usuario por
* consola y guardarlos en todo tipo de variables. En el caso de los enteros HAY
* QUE CONVERTIR lo que viene por el argumento porque viene como un String!
*
* @author Eugenia Pérez, Pello Altadill
*/
public class EntradaPorConsola {
public static void main(String args[]) throws IOException {
// Para leero por consola hay que crear
// una instancia de Scanner
Scanner scan = new Scanner([Link]);
// Declaración de variables: tipo nombre;
int edad;
float temperatura;
String lectura;
boolean booleano;
booleano = [Link]();
// Y mostramos por pantalla lo que el usuario ha metido
[Link]("Has escrito: " + booleano);
}
}
5 OPERADORES
.1 Introducción
Para cualquier programa es fundamental realizar operaciones de
cálculo, de comparación e incluso de lógica. Para esas tareas básicas
disponemos de unos operadores básicos.
.2 Operadores
aritméticos
Los operadores aritméticos son los clásicos que podemos encontrar en
la mayoría de lenguajes:
- Resta 10 - 3 7
* Multiplicación 20 * 4 80
/ División 6/2 3
¿Y si no hubiera paréntesis?
4 / 1 * number + a % 1000 * b
NOTA
Reglas de promoción de primitivos
Cuando tenemos valores de determinados tipos con los que se hacen
operaciones, el tipo puede se debe promocionar para que todos acaben con
el mismo tipo:
1. Si son dos tipos java distintos (long y short por ejemplo),
los valores se convierten al tipo más grande.
453234213L + 56; // 56 convierte en long
2. Si uno de los valores es un entero y otro con decimales, el
entero se convierte o promociona a valor con decimales.
3L + 4 + 10 // 17L
3. Las primitivas enteras pequeñas: byte, short y char se
promocionan automáticamente a int en cuanto se hace
una operación con ellas, aunque no haya otros int o long
implicados. Tiene sentido, ya que haciendo operaciones
entre short se pueden alcanzar valores de int.
4. Una vez se promocionan todos los valores, el resultado
pasa a ser el del tipo promocionado:
42 + 15.5 // 42 pasa a ser 42.0f
Por eso hay que tener cuidado ya que esto no compilaría:
int a = 5;
long b = 5;
a = a + b; // ERROR DE COMPILACIÓN
Operadores de asignación
En ocasiones se lleva a cabo una operación aritmética con una variable
y el resultado se almacena en la misma variable:
number = number + 3;
.2.1 Ejemplo de
Operadores
aritméticos
/**
* clase OperadoresAritmeticos
* Muestra la declaración de operadores arítmeticos:
* +, -, *, / : suma, resta, multiplicación, división
* % : resto de la división: 7 % 3 = 1
* ++, -- : incremento y decremento en 1
* - : cambio de signo
*
* Por último tenemos un operador condicional o terciario: ?:
* que equivale a una estructura if-else. Mostramos un
* ejemplo simple
*
* @author Eugenia Pérez, Pello Altadill
*/
public class OperadoresAritmeticos {
public static void main (String args[]) {
int a,b,c;
float x,y,z;
a = b = c = 0;
x = y = z = 0;
// Mostramos los valores antes y después
[Link]("a:" + a + ", b:" + b + ", c:" + c);
[Link]("x:" + x + ", y:" + y + ", z:" + z);
a = b + 45;
c = a * 666;
x++;
y = --z;
[Link]("a:" + a + ", b:" + b + ", c:" + c);
[Link]("x:" + x + ", y:" + y + ", z:" + z);
// Para operaciones en las que el resultado vaya a uno de los propios
// operandos (x = x + 4) podemos usar operadores de asignación especial:
// += , -=, *=, /=, %=
a += 666;
c %= 2;
// Atención a la diferencia entre ++c y c++:
// a = ++c primero se incrementa c, luego se asigna a a
// b = c++ primero se asigna a b, luego se incrementa c
a = b = c = 0;
c = 2;
a = ++c;
b = c++;
[Link]("a:" + a + ", b:" + b + ", c:" + c);
== Igual a 20 == 4 false
!= Distinto a 20 != 4 true
.3.1 Ejemplo de
Operadores
de
comparación
package [Link];
/**
* clase OperadoresComparacion Muestra el uso de los operadores de comparación
* que sirven para comparar entre si dos valores. Es importantes saber que: -
* Los dos valores comparados deben ser del mismo tipo - El resultado es
* booleano, es decir verdadero o falso
*
* Los operadores de comparación son los siguientes: > mayor que, por ejemplo a
* mayor que b: a > b < menor que == igual que >= mayor o igual que <= menor o
* igual que != distinto de
*
* Normalmente se utilizan como expresiones para establecer una condición en
* estructuras condicionales, bucles, etc. y unidos mediante operadores
* booleanos pueden construirse expresiones más complejas
*
* @author Eugenia Pérez, Pello Altadill
*/
public class OperadoresComparacion {
public static void main(String args[]) {
// Solo pueden ser true o false
boolean resultado;
int enano, grande;
char letra = 'a';
char otraLetra = 'k';
char mayuscula = 'A';
String autobot = "Optimus";
String decepticon = "Megatron";
String agente = "007";
enano = grande = 0;
resultado = (enano == grande);
}
}
.4 Operadores
Booleanos
Estos son operadores que nos permiten hacer operaciones con valores
booleanos, true/false. Son algo básico en cualquier lenguaje de programación
y en Java los tenemos de dos maneras con shortcircuit o sin él. Veamos
primero las versiones normales:
NOTA
Operadores booleanos con shortcircuit
Si bien ahorran tiempo de procesador mucho ojo al usar estos
operadores, ya que al utilizarlos puede que ciertas expresiones queden sin
evaluar. Por tanto, los resultados no serán los esperados según las
condiciones.
Por ejemplo:
int a = 5;
int b = 0;
boolean result = (a > 6) && (b++ > 0);
Al no ser verdad que (a > 6) el resto de la expresión && (b++ > 0) NO se
evalúa, es decir, NO se ejecuta. Por tanto, b++ NO se incrementa.
.4.1 Ejemplo de
Operadores
booleanos
/**
* clase OperadoresBooleanos
* Muestra el uso de los operadores booleanos: se trata de operaciones
* que operan sobre sentencias y cuyo resultado es un booleano, true o false.
* Las operaciones son:
* && : operación AND, a && b: el resultado es verdadero si a y b son verdaderos
* || : operación OR, a || b: el resultado es verdadero si cualquiera de los dos es verdadero
* ! : operación NOT, !a : invierte el valor booleano de a. Si a es true !a devuelve false.
*
* @author Eugenia Pérez, Pello Altadill
*/
public class OperadoresBooleanos {
public static void main (String args[]) {
// probaremos con algunos enteros
int bajo, alto, mediano;
bajo = 4;
alto = 666;
mediano = 42;
// Solo pueden ser true o false
boolean a = false;
boolean b = true;
boolean c = true;
boolean resultado = false;
resultado = a || b;
[Link]("Si alguno entre A o B es true el resultado es: " + resultado);
resultado = b && c;
[Link]("B y C son true por tanto el resultado es: " + resultado);
.5 Operadores
de bits
Son operadores poco frecuentes que se llevan a cabo a nivel de bit:
.5.1 Ejemplo de
Operadores
de bits
/**
* clase OperadoresBits
* Muestra el uso de operadores de bits. Su uso no es que sea muy frecuente
* pero suele estar presente en todos los lenguajes tipo c.
*
* & : AND Conjunción de bits: 1011 & 1001 = 1001
* | : OR disjunción de bits: 1011 | 1001 = 1011
* ^ : XOR disjunción excluyente de bits: 1011 ^ 1001 = 0010
* <<: desplazamiento de bits a la izquierda
* >>: desplazamiento de bits a la derecha
*
* @author Eugenia Pérez, Pello Altadill
*/
public class OperadoresBits {
public static void main (String args[]) {
int numero1, numero2;
numero1 = 0x0010;
numero2 = 0x1101;
[Link]("Resultado: \n" + (numero1 & numero2));
[Link]("Resultado: \n" + (numero1 | numero2));
// se puede abreviar:
// numero1 = numero1 & numero2;
numero1 &= numero2;
// Para operaciones en las que el resultado vaya a uno de los propios
// operandos (numero1 = numero1 & 0x1111) podemos usar unos operadores de asignación
//especial:
// &=, |=, ^=, >>=, >>>=, <<=
}
}
.6 Operador
de
Strings
Con Strings podemos llevar a cabo una operación básica: la
concatenación, que consiste simplemente en unir un String con otro. Esto se
puede hacer bien con métodos o bien usando el operador de concatenación
que en Java es: +
String name = “Eugenia “;
String completeName = name + “Pérez”; // “Eugenia Pérez”
Pero ojo:
[Link](“El resultado es: “ + result + 6); // “El resultado es: 48”
NOTA
Inmutabilidad de los Strings
Una vez que se declara un String ya no se puede modificar: ni se
puede agrandar, ni empequeñecer ni modificar sus caracteres.
En todo caso lo que podemos hacer es machacar el String que
contiene una variable con un nuevo String. Aquí, aunque parezca que lo
estamos modificando, realmente estamos cambiando el String por completo
cada vez:
String mensaje = “”; // mensaje = “”
mensaje = mensaje + “ más texto”; // mensaje = “ más texto”
mensaje = mensaje + “, y más texto”; // mensaje = “ más texto, y
más texto”
Pero ojo, si utilizamos el método de concatenación del propio String
NO modificaremos el propio String, pero si podemos guardar su valor en
otra variable:
String mensaje1 = “Say my name”;
String mensaje2;
mensaje2 = [Link](“: Heisenberg”);
// mensaje2 = “Say my name: Heisenberg”
// mensaje1 = “Say my name”
.6.1 Métodos
de
Strings
Siendo los Strings objetos y no primitivas, disponen de una serie de
métodos útiles que conviene conocer. Supongamos que tenemos el siguiente
String
String name = “Cuatrovientos”;
Estos son algunos de los métodos y lo que obtenemos. Recuerda que los
Strings son inmutables, por tanto, estos métodos devuelven un nuevo String
y NO modifican el propio String a los que se aplican
en blanco o caracteres
vacíos (\n, \t, \r)
alrededor del String
.7 Operador
ternario
También conocido como elvis operator, su formato es el siguiente:
condición ? expresión1 : expresión2
x será 42, ya que a>b es verdad por tanto se asigna el valor de la primera
expresión.
El operador ternario es una versión extra reducida de una estructura
condicional if-else en la que lo único que se decide es si se devuelve un valor
u otro.
5.8 Ejercicios del tema Operadores
Para los proyectos, utiliza el package: [Link]
..1 Ejercicio
1
¿Qué resultados se obtienen en las siguientes operaciones? Crea un
proyecto con una clase llamada CheckOperators que lleve a cabo las
siguientes operaciones y muestre el resultado. Si alguna es incorrecta,
indícalo con un comentario y no la hagas. Luego ejecútalo en el depurador.
a. 10 - 7;
b. 40 + 2;
c. 10 * 4 + 2
d. 56 % 4
e. (100 / 4) + 25 – (16 / 2)
f. 39.56F + 3 * 100
g. (2++) + 5 / 3
..2 Ejercicio
2
¿Qué resultados se obtienen de las siguientes operaciones booleanas?
Crea un proyecto con una clase llamada CheckOperatorResult que lleve a
cabo las siguientes operaciones. Luego ejecútalo en el depurador.
1. int x = 4;
2. int y = 6;
3. (x > 0)
4. (x > 0) || (y>7)
5. (x <= 4) && (y != 4)
8. x == 4 && y == 7
..3 Ejercicio
3
Crea un proyecto con una clase llamada Average que solicite por
consola 5 números al usuario y calcule la media de esos valores. Toma
números enteros y luego comprueba si obtienes una media precisa.
Puedes empezar así:
Scanner readFromConsole = new Scanner([Link]);
String line = "";
int number1 = 0;
int number2 = 0;
int number3 = 0;
int number4 = 0;
int number5 = 0;
float result = 0;
[Link]("Please enter a number:");
line = [Link]();
number1 = [Link](line);
..4 Ejercicio
4
Crea un proyecto con una clase llamada Converter que solicite por
consola al usuario un valor en dólares y lo convierta a euros.
..5 Ejercicio
5
Crea un proyecto con una clase llamada Booleans que solicite al
usuario dos valores booleanos, lleve a cabo la operación and y or y muestre el
resultado. La ejecución podría ser así:
..6 Ejercicio
6
Crea un proyecto con una clase llamada MultiplyArguments que recoja
dos valores como argumentos (args[0] y args[1]), los multiplique y muestre
el resultado por pantalla.
..7 Ejercicio7
Crea un proyecto con una clase llamada MassBodyIndex que solicite
por consola dos valores. Los valores deben ser el peso en kilos y la altura en
centímetros y debe calcularse el Índice de Masa Corporal: peso dividido por
la altura al cuadrado. Luego multiplícalo por 10000 para sacar un número
más legible por pantalla. Utiliza variables float para una mayor precisión.
..8 Ejercicio8
Crea un proyecto con una clase llamada CompleteName que solicite por
consola el nombre y dos apellidos. El programa debe mostrar al final el
nombre completo en este formato:
“Apellido1 Apellido2, Nombre”
5 ESTRUCTURAS
DE CONTROL
1
5.1 Introducción
Los programas pueden necesitar tener un comportamiento variable
según determinadas condiciones. Para eso disponemos de las estructuras de
control condicionales if-else.
Y en caso de que la dirección de programa la decida el valor concreto
de una variable podemos usar el switch-case, muy típico en los menús.
¡Siempre con llaves! ¿O no?
Todas estas estructuras de control y las de los bucles que veremos más
adelante agrupan su código dentro de llaves { }. Si dentro de ese bloque
existe únicamente una sentencia o expresión sintácticamente Java no requiere
el uso de llaves. Sin embargo, las convenciones instan a utilizarlas en todos
los casos para evitar despistes y, en teoría, dar mayor claridad. Aunque las
convenciones oficiales de Java, como veremos en adelante, a veces chocan
con las prácticas de código limpio que son las que, en general, deberían
prevalecer.
1
6.1
6.2 Condicional
If
Esta es una estructura básica que nos permite que un programa haga
una cosa siempre que se cumpla que una condición es verdadera. La forma
básica de un if es la siguiente:
if (condición) {
Expresión;
}
6.2.1 Ejemplo de
If
/**
* clase If
* Muestra el uso de un bloque condicional if, cuyo interior solo
* se ejecuta si la condición del if es verdadera
* if (condición) {
* sentecias_bloque_if;
* }
*
* @author Eugenia Pérez, Pello Altadill
*/
public class If {
public static void main (String args[]) {
// Declaramos una variable.
int x = 666;
int y = 0;
if (x == 666) {
[Link]("x lleva la marca de la bestia");
}
// La condicón del If puede ser cualquier expresión compleja siempre que
// devuelva un valor booleano
if (x > 0 && y < 0) {
[Link]("x es positivo e y negativo");
}
// Atención: ¡¡JAVA NO considera los enteros como booleanos!!
// Tradicionalmente en c y otros lenguajes similares cualquier valor
// que sea distinto de se considera como un true booleano
// mientras que el 0 se considera como false.
/*
if (x) { // En JAVA NO HACER ESTO
[Link]("x es distinto de 0");
}
if (y) { // En JAVA NO HACER ESTO
[Link]("y es distinto de 0");
}
// por tanto !y es como preguntar si y es igual a 0
// esta forma de escribir es muy kewl pero se considera poco clara
if (!y) { // En JAVA NO HACER ESTO
[Link]("y es 0!!");
}*/
}
}
6.3 Condicional
If-else
A veces nos puede interesar que el programa haga una cosa o bien
otra según la condición impuesta. Para eso podemos añadir a la estructura if
una cláusula else. Si la condición del if no se cumple, el programa ejecutará
lo que hay en el cuerpo del else:
if (condición) {
Expresión;
} else {
Expresión;
}
6.3.1 Ejemplo
de If
Else
/**
* clase IfElse
* Muestra el uso de un bloque condicional if-else: si se cumple la
* condición del if se ejecuta su bloque, en caso contrario se ejecuta
* el bloque del else.
* if (condición) {
* sentecias_bloque_if;
* } else {
* sentecias_bloque_else;
* }
*
* @author Eugenia Pérez, Pello Altadill
*/
public class IfElse {
public static void main (String args[]) {
// Declaramos una variable.
int a = 666;
int b = 0;
if (a >= b) {
[Link]("A es mayor o igual que B");
} else {
[Link]("A es menor que B");
}
}
}
6.4 Condicional
If-else-if
Las condiciones a veces pueden ser complejas por existir más de una
variante, y en esos casos nos puede interesar que el programa haga una cosa
o bien otra o bien otra... Según distintas condiciones. Para eso podemos
añadir a la estructura if una cláusula else seguida de otro if. Así el programa
va comprobando condiciones hasta que alguna se cumpla y en su defecto,
entra en la cláusula else:
if (condición1) {
Expresión;
} else if (condición2) {
Expresión;
} else if (condición3) {
Expresión;
} else {
Expresión;
}
6.4.1 Ejemplo
de If
Else If
/**
* clase IfElseIf
* Muestra el uso de un bloque condicional if-else-if: si se cumple la
* condición del if se ejecuta su bloque, en caso contrario se comprueba la condición
* del siguiente bloque else-if... y así hasta llegar opcionalmente a un último else.
* if (condición) {
* sentecias_bloque_if;
* } else if (condición2) {
* sentecias_bloque_else_if_1;
* } else if (condición2) {
* sentecias_bloque_else_if_2;
* } else {
* sentecias_bloque_else;
* }
*
* NOTA: si todas las condicionals comprueban la igualdad de una única variable
* con un valor concreto es probable que debas usar un switch-case
* NOTA2: efectivamente, como en c aquí no hay elseif, elif, elsif, ni similares
*
* @author Eugenia Pérez, Pello Altadill
*/
public class IfElseIf {
public static void main (String args[]) {
// Declaramos una variable.
int a = 666;
int b = 0;
if (a > b) {
[Link]("A es mayor que B");
} else if (a==b) {
[Link]("A es igual a B");
} else {
[Link]("A es menor que B");
}
}
}
6.5 Switch/Case
Esta es una estructura que nos permite que el programa haga una cosa u
otra dependiendo únicamente de valor que tenga una variable. En cierto
modo es similar a encadenar if-else-if, solo que cuando la condición en todos
los casos es comprobar el valor de una variable, esta es la estructura correcta:
switch(expresión) {
case valor1: expresión;
break;
case valor2: expresión;
break;
case valor3: expresión;
break;
default: expresión;
break;
}
Importante:
Es imprescindible meter la cláusula break al final de cada expresión
ya que si no se hace se ejecutarán todas las expresiones del switch case.
Incluso en la última cláusula, las convenciones del lenguaje recomiendan
introducir el break;
Los switch case en Java son poco flexibles, aunque permiten agrupar
los case como se ve en el siguiente ejemplo.
6.5.1 Ejemplo de
Switch/Case
/**
* clase SwitchCase
* Clase que muestra el uso de un switch case
* Estas estructuras son como un if-else-if pero se aplican comprobando
* si una variable tiene determinado valor
*
* switch (variable) {
* case valor1 : sentencias; break;
* case valor2 : sentencias: break;
* ...
* default: sentencias;
*}
*
* NOTA: no olvides el break para cada caso.
* NOTA2: en versiones ANTERIORES al JDK1.7 el switchcase solo funciona para tipos simples *
(int, char), no se pueden usar Strings.
* como en algunos lenguajes interpretados.
*
* NOTA3: no se permiten los intervalos como en VB.
*
* @author Eugenia Pérez, Pello Altadill
*/
public class SwitchCase {
public static void main (String args[]) {
int numero = 0;
int dorsal = 10;
// Según el valor de edad sacaremos un mensaje u otro
// NO hay que olvidar el break para cada CASO!!!!!!
switch (numero) {
case 0:
[Link]("Eres un 0");
break;
case 15:
[Link]("Eres la niña bonita");
break;
case 42:
[Link]("Eres la respuesta a todo");
break;
case 69:
[Link]("Eres el puerto tftp, malpensao.");
break;
default:
[Link]("Eres un número sin personalidad: " + numero);
break;
}
// Podemos agrupar más opciones como una especia de OR:
switch (dorsal) {
case 3:
case 4:
case 5:
[Link]("Dorsal de un defensa" + dorsal);
break;
case 10:
[Link]("El dorsal del capitán: " + dorsal);
break;
case 9:
[Link]("El dorsal de un ariete: " + dorsal);
break;
default:
[Link]("Eres un número sin personalidad: " + dorsal);
break;
}
}
}
NOTA
Particularidades de los switches
La cláusula default no tiene que ponerse obligatoriamente al final, ni
tampoco es obligatorio poner una cláusula break en todos los casos.
Por otro lado, los tipos soportados hoy en día en un switch/case se
han ampliado a:
byte, short, int
char
String
Byte
Short
Integer
Char
Enum
Lo que sí deben cumplir todos los case del switch es que cada caso
debe presentar un valor del mismo tipo que el usado al declarar el switch.
6.6 Ejercicios
del tema.
Estructuras
de control
Para los proyectos, utiliza el package:
[Link]
6.6.1 Ejercicio
1
Crea un proyecto con una clase llamada OddOrEven que solicite al
usuario un número entero y muestre por pantalla si ese número es par o
impar. Usa el operador % para comprobar si ese número es par o impar.
6.6.2 Ejercicio
2
Crea un proyecto con una clase llamada PositiveNegativeOrZero que
solicite al usuario un valor entero y muestre por pantalla si ese número es
positivo, negativo o 0.
6.6.3 Ejercicio
3
Crea un proyecto con una clase llamada PositiveEven que solicite al
usuario un número entero y muestre por pantalla si ese número es par y
positivo. En caso contrario debe indicar si es negativo, impar o ambos.
6.6.4 Ejercicio
4
Crea un proyecto con una clase llamada CompareNumbers que solicite
al usuario dos valores enteros, los compare y muestre por pantalla si uno es
mayor que el otro o si son iguales.
6.6.5 Ejercicio
5
Crea un proyecto con una clase llamada Multiple que solicite al usuario
dos valores enteros y muestre por pantalla si el primero es múltiplo del
segundo.
6.6.6 Ejercicio
6
Crea un proyecto con una clase llamada MassBodyIndexDiagnostic que
solicite al usuario su peso en kilos y su altura en centímetros y calcule el IMC
(peso / altura2); debe mostrar el resultado y luego hacer el diagnóstico:
Si el IMC es menor que 16 se muestra el mensaje: “You need to eat
more”.
Si el IMC está entre (>=)16 y 25(<) se muestra el mensaje: “You are
fine”
Si el IMC está entre 25 y 30(<) se muestra el mensaje: “You are
eating too much”.
Si el IMC es superior a 30 se muestra el mensaje: “Go to the
hospital”
6.6.7 Ejercicio
7
Crea un proyecto con una clase llamada LanguageMessage que solicite
al usuario un nombre de lenguaje y saque un mensaje distinto según el
nombre de ese lenguaje:
Si el lenguaje es “C++” el mensaje a sacar será “The best
language ever”
Si el lenguaje es “Java” el mensaje a sacar será “The second best
language ever”
Si el lenguaje es “JavaScript” el mensaje a sacar será “The
language of the present”
Si es cualquier otro debe decir “Nothing to say about that”
En caso de que te diera algún problema al hacer un switch case con una
variable de tipo String, basta con que modifiques el JRE compliance del
proyecto a la versión 1.7 o superiores, cosa que el propio IDE te sugerirá.
6.6.8 Ejercicio
8
Crea un proyecto con una clase llamada Position que solicite al usuario
un dorsal de jugador y haga lo siguiente: comprobar que ese número está
entre 0 y 99. Y a continuación el programa debe mostrar un texto con la
posición que corresponde a cada dorsal:
Si el usuario ha tecleado 1 el texto será “Keeper”
Si el usuario ha tecleado 3,4,5 el texto será “Defender”
Si el usuario ha tecleado 6, 8, 11 el texto será “Midfield”
Si el usuario ha tecleado 9 el texto será “Striker”.
Para cualquier otra opción el texto será “Any”.
6.6.9 Ejercicio
9
Crea un proyecto con una clase llamada CurrencyConverter que solicite
al usuario una cantidad monetaria y un carácter d, p, y (dollar, pound, yen).
Según el carácter introducido por el usuario el programa debe convertir la
cantidad monetaria (que serán euros) a la moneda correspondiente.
No es preciso que el tipo de cambio sea real.
7 BUCLES
7.1 Introducción
No debemos olvidar que el ordenador es tonto y sin embargo tiene gran
capacidad de trabajo. En muchos casos necesitaremos que el ordenador lleve
a cabo una tarea repetitiva mientras se cumpla una condición o para recorrer
una estructura de tamaño fijo. Para cada caso tenemos un tipo de bucle, una
herramienta que nos permitirá que una expresión se ejecute tantas veces
como lo indique una condición
7.2 El
bucle
While
Este es el tipo de bucle o iteración más simple. Permite que un código
se ejecute mientras la condición del mismo se cumpla. Este es su aspecto:
while (condición) {
expresión;
}
7.2.1 Ejemplo
de bucle
while
/**
* clase While
* Muestra el uso de bucles while. Este tipo de bucles
* repiten unas sentencias mientras una condición sea verdadera.
* El final no será previsible.
* Formato:
* while (true) {
* sentencias;
* }
*
* @author Eugenia Pérez, Pello Altadill
*/
public class While {
public static void main (String args[]) {
// Vamos a usar un contador
int contador = 10;
// Ejecutamos el bucle mientras contador sea mayor que 0
while (contador > 0) {
[Link]("Dentro del bucle " + contador);
// y vamos decrementando
contador--;
}
// Vamos a hacer otra prueba
contador = 10;
[Link]("El siguiente bucle:");
// Atención: podemos actualizar la variable en la propia condición
while (contador-- > 0) {
[Link]("Dentro del bucle " + contador);
}
// el bucle infinito: simplemente poniendo en la condición true
//while (true) {
// sentencias;
// }
}
}
7.3 El bucle
do/while
Este bucle también se ejecuta mientras la condición se cumpla o sea
true, pero a diferencia del while normal, la expresión del bucle do/while se
ejecuta al menos una vez. Este es su aspecto:
do {
expresión;
} while (condición);
7.3.1 Ejemplo
de Bucle
do/while
/**
* clase DoWhile
* Muestra el uso de bucles do while. Este tipo de bucles
* es como el while, repiten unas sentencias mientras una condición sea verdadera
* pero en su caso la primera iteración sucede siempre ya que la condición se comprueba
* al final.
* Formato:
* do {
* sentencias;
* } while();
*
* @author Eugenia Pérez, Pello Altadill
*/
public class DoWhile {
public static void main (String args[]) {
// Vamos a comprobar si un número es primo
// para eso hay que verificar que solo es divisible
// por si misma o por 1.
int numero, anterior;
// Les asignamos a las dos
numero = anterior = 7;
// para guardar el resultado
boolean esPrimo = true;
// Ejecutamos el do-while
do {
anterior--;
if (numero % anterior == 0) {
esPrimo = false;
}
} while(anterior > 2 && esPrimo);
// Mostramos el resultado
if (esPrimo) {
[Link]("Este numero: " + numero + " es primo");
} else {
[Link]("Este numero: " + numero + " NO es primo");
}
}
}
7.4 El
bucle
for
El bucle for permite ejecutar un código un número determinado de
veces y tiene la ventaja de que gestiona él mismo las variables para controlar
la salida del bucle.
¿Cuándo usar este bucle? Hay veces que necesitamos que un bucle se
ejecute un número determinado de veces, ni una más ni una menos. O bien
necesitamos un bucle que recorra determinada estructura de datos, como un
array, desde el primero al último elemento.
Este es el aspecto de un bucle for:
for (inicio;condición;actualización) {
expresión;
NOTA
Cuidado con los tipos en un for
En el for se utiliza al menos una variable para controlar el bucle. Hay
que tener en cuenta lo siguiente:
Si previamente ya existe una variable con el mismo
nombre, NO se puede redeclarar.
Si se usa más de una variable en el for, todas deben ser
del mismo tipo.
7.5 El
bucle
for
tipo
for-
each
Existe otra forma de bucle presente desde la versión 1.5 de Java en la
que usando la misma expresión for podemos iterar sobre estructuras de datos
como arrays o las colecciones Java que veremos más adelante. Esta es su
estructura:
for (tipo variable: Colección) {
expresión;
}
Esta estructura for resulta útil cuando queremos recorrer una colección
de datos y no queremos manejar ni un índice ni controlar el límite. El propio
for se encarga de ir asignando en cada iteración el elemento siguiente de la
colección a la variable. En el ejemplo que hay a continuación veremos un
ejemplo comentado. En el tema de arrays y colecciones se mostrarán más
ejemplos.
También podremos ver en el siguiente ejemplo que en el bucle for
también podemos utilizar más de una variable de control del bucle.
7.5.1 Ejemplo
de
Bucle
for
/**
* clase For
* Muestra el uso de un bucle for. Los bucles for sirven para
* ejecutar unas sentencias un número determinado de veces.
* Los bucles while se usan cuando la condición de salida es más incierta,
* o dependemos del valor de alguna variable.
*
* Formato:
* for (inicio;condición;actualización) {
* sentencias;
* }
* @author Eugenia Pérez, Pello Altadill
*/
public class For {
public static void main (String args[]) {
// Vamos a dar 10 vueltas.
// En los bucles for solemos usar un número como índice
// del bucle.
int i,j,k;
i = j = k = 0;
// Vamos a dar 10 vueltas: dentro del for
// - Primero iniciamos: i=0;
// - Luego ponemos la condición de salida: i>0
// - Luego ponemos la actualiación
for (i=0; i<10; i++) {
[Link]("Dentro del bucle 1 : " + i);
}
// podemos inicializar la variable dentro del for
// pero ojo, el ámbito de z solo será el bucle for.
for (int z = 10; z>0; z--) {
[Link]("Dentro del bucle 2 : " + z);
}
// Podemos usar más de una variable usando la ,
for (j=0, k=20; j<10 && k>0; j++, k=k-2) {
[Link]("Dentro del bucle 3 : " + j + " , " + k);
}
// En JDK 1.7
// [Link]<String> meses = new [Link]<String>();
//[Link]("enero");
//[Link]("febrero");
//[Link]("marzo");
//…
// for (String mes : meses) {
// [Link](mes);
// }
// El bucle infinito:
// for (;;)
}
}
7.6 Break y
continue
La ejecución de los bucles while y for puede verse alterada desde dentro
mediante las sentencias break y continue. Ambas puedes aplicarse tanto en
bucles for como while
7.6.1 break
Esta sentencia nos permite terminar la ejecución del bucle actual de
forma inmediata. Resulta útil si por determinada condición necesitamos poder
abortar la ejecución del bucle.
¡Ojo! En el caso de tener más de un bucle anidado, una sentencia break
solo saldrá del bucle actual. Si necesitas hacer un break saliendo al exterior
del bucle o a cualquier nivel del bucle anidado, entonces debes utilizar
etiquetas en el código y hacer break a esa etiqueta. En el siguiente ejemplo
vemos como indicamos con la etiqueta salida: la parte exterior del bucle. Si
desde el bucle interno hacemos break salida; la ejecución saldrá fuera de los dos
bucles, pues es donde se encuentra la etiqueta
salida:
for (inicio;condición;actualización) {
for (inicio;condición;actualización) {
expresión;
break salida;
}
}
7.6.2 continue
Continue detiene la ejecución de la iteración actual del bucle y continúa
la ejecución de la siguiente iteración. También puede hacer uso de etiquetas
como el break para poder hacer un continue en distintos niveles de un bucle
anidado:
for (inicio;condición;actualización) {
for (inicio;condición;actualización) {
expresion1;
continue;
expresion2;
}
}
7.6.3 Ejemplo
de
Break y
continue
/**
* clase BreakContinue
* Clase que muestra el uso de Break y Continue que nos sirven para
* modificar el normal comportamiento de los bucles.
* - Con break se rompe el bucle y se sale de él.
* - Con continue interrumpimos la ejecución actual del bucle y se salta a la siguiente vuelta
* sin salir del bucle.
*
*
* @author Eugenia Pérez, Pello Altadill
*/
public class BreakContinue {
public static void main (String args[]) {
// Declaramos una serie de variables
int numero, anterior;
int x,y;
x = 10;
while (x > 0) {
if (x == 5) {
break; // salimos del bucle
}
x--;
}
// Vamos a buscar los números primos
// que hay del 2 al 20
for (numero = 2; numero < 20; numero++) {
anterior = numero;
do {
anterior--;
// En cuanto es divisible, salimos
if (numero % anterior == 0) {
break;
}
} while(anterior > 2);
// Si se ha llegado hasta el final, es primo
if (anterior == 2) {
[Link](numero + " es un PRIMO");
}
}
// Si tenemos dos bucles anidados,
// ¿cómo podemos salir de un bucle concreto con break?
// hay que usar una etiqueta, que es un identificador seguido de
// dos puntos:
salida:
for(x=1;x<20;x++) {
[Link]("Bucle principal: " + x);
if (20 % x == 7) { // salimos del bucle principal
break;
}
// para salir desde el bucle interno hasta fuera
// tendremos que usar la etiqueta salida
for(y=10;y>0;y--) {
[Link]("Bucle interno: " + y);
if (20 % y == 4) {
// Salimos de este y del bucle principal también
// pero hay que especificar la etiqueta de salida
break salida;
}
}
}
}
}
7.7 Ejercicios
del tema
Bucles
Para los proyectos, utiliza el package: [Link]
7.7.1 Ejercicio
1
Crea un proyecto con una clase llamada SimpleLoop que solicite al
usuario un valor entero, comprueba si es mayor que 0 y si es así muestre por
pantalla un saludo tantas veces como el valor del número. Si el valor
introducido no es mayor que 0 debes mostrar un mensaje de advertencia al
usuario y terminar el programa con un return; o con [Link](0);
7.7.2 Ejercicio
2
Crea un proyecto con una clase llamada DrawStars que solicite al
usuario un valor entero, comprueba si es mayor que 0 y además par y si es así
muestre por pantalla una línea con el carácter “*” (asterisco) tantas veces
como el valor del número. Usa [Link](“*”); Por ejemplo, si
introduce 8 mostrará:
********
Si el valor introducido no cumple los requisitos debes mostrar un
mensaje de advertencia al usuario y terminar el programa con un return; o
con [Link](0);
7.7.3 Ejercicio
3
Crea un proyecto con una clase llamada DrawSequence parecido al
anterior pero la línea que debes mostrar debe tener este aspecto:
*-*-*-*-*-*-*-*-*-*-*
Y siempre debe terminar en “*”
Por ejemplo, si introducen el 5:
Si introducen el 8:
7.7.4 Ejercicio
4
Crea un proyecto con una clase llamada DoWhile que solicite al usuario
una palabra y hasta que esa palabra no sea “out!” el programa no debe
terminar y debe seguir solicitando una palabra.
7.7.5 Ejercicio
5
Crea un proyecto con una clase llamada DrawSquare que solicite al
usuario un número entero y usando ese valor debe “dibujar” en la consola un
cuadrado formado por “*”.
Por ejemplo, si introduce 4 se mostrará:
****
****
****
****
7.7.6 Ejercicio
6
Crea un proyecto con una clase llamada Factorial que solicite al
usuario un número entero y calcule su factorial. Por ejemplo, el factorial de 5
sería 5 x 4 x 3 x 2 x 1 = 120
7.7.7 Ejercicio
7
Crea un proyecto con una clase llamada PrimeNumber que solicite al
usuario un número entero y comprueba si ese número es primo o no, es decir
si solamente es divisible por sí mismo o por 1.
7.7.8 Ejercicio
8
Crea un proyecto con una clase llamada MultiplicationTable que
muestre todas las tablas de multiplicar desde el número 0 al 10.
8 ARRAYS
8.1 Introducción
Un array es una variable que en lugar de contener un único valor
contiene un número concreto de valores indexados numéricamente desde 0.
Por ejemplo, un array de 15 números, un array de 5 nombres, etc… La
indexación significa que para acceder a cada elemento necesitamos indicar su
número. Visualmente tiene este aspecto:
Índice 0 1 2 3 4
Valor
O bien:
int numbers [];
Índice 0 1 2 3 4
Valor 0 0 0 0 0
Índice 0 1 2 3 4
Valor 23 56 42 -1 6
NOTA
Declaraciones múltiples
Si declaramos más de un array en una línea hay que tener cuidado.
Aquí es donde se puede diferenciar las consecuencias de poner los
corchetes delante o detrás del nombre de variable.
Aquí estamos creando dos variables de arrays de números enteros:
int [] numbers, moreNumbers; // int numbers[], moreNumbers[];
8.2 Ejemplo
de
Arrays
/**
* clase Arrays
* Clase que muestra la declaración y uso de arrays. Los arrays
* o arreglos son variables que contienen un conjunto de datos del mismo tipo
* indexados numéricamente desde el 0 en adelante.
*
* NOTA: pueden crearse arrays de elementos del tipo básico: int, float…
* y también pueden crearse de clases.
* NOTA2: los arrays en java se definen con un tamaño concreto y no puede
* alterarse. Si necesitamos elasticidad entonces debemos usar clases
* como por ejemplo Vector.
*
* @author Eugenia Pérez, Pello Altadill
*/
public class Arrays {
public static void main (String args[]) {
// Vamos a definir un array de enteros, todavía sin especificar el tamaño.
// Lo podemos hacer de dos formas:
int valores[];
int [] dorsales;
// Podemos establecer el tamaño mediante new:
// En este caso definimos un array de DIEZ elementos,
// pero ATENCIÓN, los índices irán del 0 al 9.
int [] puntos = new int[10];
// Si quisieramos crear un array de caracteres que contenga el abecedario
// lo hariamos así. El abecedario español tiene 28 letras, en el array serán
// del 0 al 27.
char abecedario[] = new char[28];
boolean verdades[] = new boolean[5];
// Podemos inicializar los arreglos con valores concretos,
// lo cual sería una forma de implícita de especificar su tamaño:
int numeros[] = {7,15,42,69,666};
char letras [] = {'a','b','c','d','e','f','g','h'};
// Podemos crear arrays de Strings
String heroes[] = {"Gandalf", "Haplo", "Jon Nieve", "Vader", "Trancos"};
// para acceder a un elemento del array debemos indicar su índice.
// el índice es un número entero que va de 0 al tamaño-1 del array
[Link]("The number of the beast: " + numeros[4]);
// Podemos alterar valores de un elemento del array
verdades[0] = false;
numeros[2] = 23;
// Y por supuesto operar con ellos:
// al elemento 0 del array heroes le concatenamos algo:
heroes[0] = heroes[0] + " el gris";
numeros[3] = numeros[2] + 8;
// Vale, ¿qué hacemos con el array? Podemos recorrerlo con un for
// Todo array tiene una propiedad que es length, la cual no da su tamaño
// Por ejemplo, los elementos del array puntos los podemos inicializar
// con un valor concreto:
for (int i = 0;i< [Link]; i++) {
puntos[i] = 0;
}
[Link]("Estos son los mayores heroes: ");
8.3.1 Ejercicio
1
Crea un proyecto con una clase llamada ShowArray que defina un array
de 10 números enteros y luego muestre cada elemento en un bucle.
8.3.2 Ejercicio
2
Crea un proyecto con una clase llamada NameArray que defina un array
de 10 Strings vacíos y luego en un bucle solicite al usuario que introduzca
cada elemento. Después de muestra todos los elementos con otro bucle.
8.3.3 Ejercicio
3
Crea un proyecto con una clase llamada NumberArray que defina un
array de 10 números enteros inicializados a 0 y luego en un bucle solicite al
usuario que introduzca cada elemento. Luego crea otro bucle que determine si
en el array hay algún elemento repetido. Con que encuentre uno repetido es
suficiente.
8.3.4 Ejercicio
4
Crea un proyecto con una clase llamada IncrementArray que defina un
array de 10 números enteros inicializados a 0 y luego en un bucle solicite al
usuario que introduzca cada elemento. Luego crea otro bucle que incremente
en uno cada uno de los elementos y los muestre.
8.3.5 Ejercicio
5
Crea un proyecto con una clase llamada AverageArray que defina un
array de 10 números con decimales (float) inicializados a 0 y luego en un
bucle solicite al usuario que introduzca cada elemento. Luego crea otro bucle
que calcule la media de todos los números.
8.3.6 Ejercicio
6
Crea un proyecto con una clase llamada CountArray que defina un
array de 10 números enteros inicializados a 0 y luego en un bucle solicite al
usuario que introduzca cada elemento. Luego crea otro bucle que contabilice
el total de números positivos, negativos y los que sean 0.
8.3.7 Ejercicio
7
Crea un proyecto con una clase llamada CheckPrimes que defina un
array de 10 números enteros inicializados a 0 y luego en un bucle solicite al
usuario que introduzca cada elemento. Luego crea otro bucle que descubra
que números son primos.
8.3.8 Ejercicio
8
Crea un proyecto con una clase llamada RandomArray que defina un
array de 10 elementos. Crea un bucle que inicialice los valores del array
usando números aleatorios:
// Variable para generar números aleatorios, incluye import [Link].*;
Random rnd = new Random();
[Link](30); // número aleatorio entre 0 y 30
...
8.3.11 Ejercicio 11
Crea un proyecto con una clase llamada Menu que tenga las siguientes
opciones:
9.1 Declaración
de ENUM
Los Enum pueden declararse de varias maneras:
Dentro de un método, aunque su ámbito no pasará de ahí
Fuera de una clase
En un fichero, declarado -casi- como una clase
Lo normal es que se defina un ENUM para ser utilizado en todo un
proyecto, así que de hacerlo se puede hacer en un fichero específico para el
mismo.
El ENUM se declara simplemente así. Con los valores en letras
mayúsculas.
enum { EARTH, AIR, FIRE, WATER };
9.2 ENUM
con
métodos
En principio Java permite declarar un ENUM en un fichero
independiente y además le permite tener sus propios métodos. Obviamente,
los ENUM no pueden sustitur a las clases y deberían limitarse a definir
variables que solo pueden tener un número concreto de valores posibles.
En el siguiente ejemplo, se declara un ENUM en su propio fichero e
incluso tiene métodos.
Lo primero que se hace es poner la lista de posibles valores, sin mayor
preámbulo.
/**
* ENUM defined in itw own file, representing the working days
* @author Eugenia Pérez, Pello Altadill
*/
public enum WorkDay {
// This is how we set the values
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY;
10.1 Métodos
o
funciones.
Las clases contienen atributos y métodos. Los métodos son lo que en la
programación tradicional se suele llamar funciones, que no son más que
trozos de código que realizan una tarea concreta. Para utilizar ese código lo
que se hace es llamar a la función. Gracias a eso conseguimos:
Separar el código en pequeñas partes y organizar mejor los
programas, algo fundamental en la programación
estructurada
Facilitar la reutilización de esas partes.
Facilitar la depuración de los programas.
10.2 El método
constructor
Las clases tienen un método especial que se utiliza cuando se crea una
instancia del mismo:
Customer customer = new Customer();
this
Dentro de ese constructor, al asignar el parámetro al atributo usamos la
palabra this:
[Link] = name;
NOTA
Llamando de un constructor a otro
A veces creamos más de un constructor en una clase don distintos
parámetros, pero para no repetir código, podemos hacer que desde un
constructor se llame a otro. En esos casos resulta importante llamar de un
constructor a otro usando this ya que de lo contrario lo que estaríamos
haciendo es crear otra instancia.
Así es como hay que hacer. Supongamos que nuestra clase Customer
tiene dos parámetros con tres constructores:
class Customer {
private String name;
private int age;
public Customer () {
name = “Fermín”;
age = 23;
}
10.3 Ejemplo
de Clase
simple
public class Clases {
String nombre;
/**
* Método constructor, se ejecuta al crear una instancia de la clase
*/
Clases () {
nombre = "Juan Solo";
[Link]("Has creado una instancia de la clase");
}
/**
* saludo
* Un método de la clase que simplemente saca un mensaje
*/
void saludo () {
[Link]("Hola Mundo, soy " + nombre);
}
public static void main (String args[]) {
// Creamos una INSTANCIA de la clase:
// es como declarar una variable, pero el tipo es el
// nombre de la clase
Clases unaClase = new Clases();
// Con la instancia llamamos a uno de sus métodos
[Link]();
}
}
NOTA
Una clase, un fichero
Generalmente cada clase debe ir dentro de un fichero cuyo nombre
debe coincidir con el nombre de clase, teniendo en cuenta incluso las
mayúsculas/minúsculas. Java permite meter más de una clase en un
fichero, aunque si lo haces, al menos una de las clases debe ser
declarada como public.
10.4 Ejemplo
de Clase
cliente
import [Link];
/**
* clase Cliente
* Muestra la declaración de una clase que representa un cliente.
* Una clase se compone de atributos (propiedades) y métodos (funciones)
* La clase representa una entidad y cuando definimos una variable de
* de esa clase
*
* @author Eugenia Pérez, Pello Altadill
*/
public class Cliente {
public String nombre;
public String apellidos;
public Date nacimiento;
public int codigo;
/**
* Cliente
* este es el método constructor, al que se invoca
* al crear una instancia de la clase
*/
Cliente () {
[Link]("Has creado una instancia de Cliente");
}
/**
* Cliente
* Otro constructor con parámetros.
* Nos sirve para crear una instancia
*/
Cliente (String nombre, String apellidos, Date nacimiento, int codigo) {
[Link]("Has creado una instancia de Cliente");
[Link] = nombre;
[Link] = apellidos;
[Link] = nacimiento;
[Link] = codigo;
}
/**
* nombreCompleto
* Método que une el nombre y el apellido del Cliente
* @return resultado
*/
public String nombreCompleto () {
String resultado = nombre + " " + apellidos;
return resultado;
}
/**
* fichaCliente
* Método que muestra todos los datos del cliente
*
*/
public void fichaCliente () {
[Link]("--Ficha del Cliente--");
[Link]("Código: " + codigo);
[Link]("Nombre completo: " + nombreCompleto());
[Link]("Fecha nacimiento: " + nacimiento);
}
/**
* saluda
* Un método que nos muestra un saludo
*/
public void saluda () {
[Link]("Hola mundo desde la clase");
}
/**
* main
* Función principal
* esta función es la que se inicia directamente al ejecutar el programa
* Y desde ella vamos a crear una instancia de Cliente
*/
public static void main (String args[]) {
// Creamos un par de instancias
Cliente unCliente = new Cliente();
Cliente otroCliente = new Cliente("Darth","Vader", new Date(), 666);
[Link] = 89;
[Link]();
[Link]();
}
}
10.5 Ejercicios
del tema
Clases
Para los proyectos, utiliza el package: [Link]
10.5.1 Ejercicio
1
Crea un proyecto con una clase llamada Hello que simplemente tenga
una propiedad String greet y un método sayHello() que muestre esa
propiedad por la consola. Incluye el método main y crea una instancia de la
clase para probarla. Crea el diagrama de clases UML.
10.5.2 Ejercicio
2
Crea un proyecto con una clase llamada Coin. La clase debe tener un
constructor vacío y un único método llamado flip cuyo resultado debe ser
aleatoriamente un número entero, que en función de su valor, mostrará:
“CROSS” o “PILE”, es decir, cara o cruz. Incluye el método main y crea 5
instancias de la clase para probarla. Crea el diagrama de clases UML
10.5.3 Ejercicio
3
Crea un proyecto con una clase llamada Conversor que tenga varios métodos
para convertir monedas. Incluye el método main y crea una instancia de la
clase para probarla. Crea el diagrama de clases UML.
Para definir cada uno de los cambios podrías utilizar una constante. Esto lo
veremos más adelante, pero para definir constantes en Java:
private static final double CHANGE_PESETAS_EUROS = 166.386d;
10.5.4 Ejercicio
4
Crea un proyecto con una clase llamada Array que sirva para crear un array
de enteros. La clase debe contener un atributo para contener el array de 10
elementos y los siguientes métodos:
constructor. Se encarga de invocar (llamar) al
public Array():
siguiente método: init.
inicia el array con números aleatorios. Se le
private void init ():
llama desde el constructor
public void increment(): incrementa cada elemento del array en
1.
public void decrement(): decrementa cada elemento en 1
public int countEven(): devuelve el número de elementos pares
en el array
Añade una clase Main con un método main que cree una instancia de
Array, muestre el total de números pares, incremente, muestre el total de
números pares, decremente y muestre el total de números pares. Debería
volver al valor inicial:
10.5.5 Ejercicio
5
Crea un proyecto con una clase llamada Dice para simular el comportamiento
de un dado de N caras. Estos serán los métodos de la clase:
10.5.6 Ejercicio
6
Crea un proyecto con una clase llamada Square que nos servirá para dibujar
cuadrados de caracteres por la consola. Estos son los métodos de la clase:
10.5.8 Ejercicio
8
Crea un proyecto con una clase llamada Nicknames, que servirá para
generar apodos y mostrarlos por pantalla.
Para generar apodos puedes crear dos arrays de Strings,
uno con la primera parte del apodo y otro con la segunda
por ejemplo:
private String[] nicknameStart = {“Ojos”,”Puño”,”Espada”,”Viento”}
private String[] nicknameEnd = {“Negro”,”de Fuego”, “del
Infierno”,”Helada”}
10.5.9 Ejercicio
9
Crea un proyecto con una clase llamada Passwords, que servirá para generar
passwords aleatorias y mostrarlas por pantalla.
Donde:
public: no requerido, pero conviene indicarlo. Es el
modificador de acceso. Si se omite el modificador se
considera default, si se incluye debe ser public, protected o
private.
final: no requerido, es un modificador opcional. Pueden ser
final, static, abstract, a veces combinados y sin que importe
el orden. Más adelante veremos cómo se usan.
int: requerido, es el retorno del método. Siempre hay que
indicar algo, ya sea primitivo, una clase o bien void si el
método no devuelve nada. Si no es void, el método debe
tener una instrucción return donde se debe retornar algo del
tipo especificado.
add: requerido, es el nombre del método. Las convenciones
indican que debe ser un verbo que describa qué hace el
método, empezando por minúscula y en camel case.
int a, int b: requeridos, son los parámetros del método.
Cuando se invoca el método deben pasarse valores de esos
tipos. Las variables se podrán utilizar dentro del método y
ese será su ámbito. Un método puede:
Tener varios parámetros
No tener parámetros, pero se mantienen los paréntesis: ()
Tener parámetros variables: vararg…
throws Exception: no requerido, indica que el método
puede lanzar una excepción. Esto obliga a quien invoque al
método a capturarlo o a lanzarlo también. Puede indicarse
más de un tipo de excepción separada por coma.
NOTA
Paso de parámetros
Cuando en Java se pasan parámetros a un método se hace por valor,
es decir, se hace una copia del valor si es un primitivo o se hace una copia
de la referencia si se trata de una clase.
Por ejemplo, si los parámetros son de tipos primitivos, dentro del
método en ningún caso se alterará lo que contienen las variables a y b.
int a = 5;
int b = 8;
[Link](a, b);
// a=5, b=8
Si los parámetros son objetos, se hace una copia de la referencia.
Ten en cuenta que en ese caso si el objeto se modifica dentro del método al
salir del mismo permanecerá el cambio, ya que lo que se ha hecho es una
copia de la referencia, pero el objeto sigue siendo único.
Estudiante estudiante = new Estudiante(“Fermín”);
// [Link] = “Fermín”
[Link](estudiante, “Juan”);
// [Link] = “Juan”
11.1 Miembros
estáticos
Una clase puede tener atributos y métodos estáticos. Se conocen
también como miembros de clase: variables de clase y métodos de clase. Los
atributos estáticos tienen la particularidad de que no existe una copia por cada
instancia, sino que solo hay una copia. Este tipo de miembros pueden
utilizarse sin necesidad de crear una instancia de la clase.
public class SampleStatic {
public static int instances = 0;
public SampleStatic () {
instances++;
}
NOTA
Referencia nula y elementos estáticos
Podemos crear instancias si así queremos de una clase estática. Pero,
aunque luego anulemos la referencia ¡esta puede seguir usando miembros
estáticos de la clase!
SampleStatic sample = new SampleStatic()
sample = null;
// sorpresa, esto funciona, porque es la parte estática
[Link]()
NOTA
Static vs Instancia
Los métodos estáticos solo pueden llamar a otros métodos estáticos o
acceder a otros atributos estáticos. En ningún caso pueden acceder a
métodos o atributos de instancia (no static). Los miembros de instancia, en
cambio, pueden acceder tanto a los estáticos (a través del nombre de clase
o una referencia) como los de instancia.
11.2 Ejemplo
de
métodos
estáticos
/**
* clase Métodos
* Muestra la declaración de distintos tipos de métodos: constructores,
* métodos con distintos tipos de retorno, métodos que lanzan excepciones,
* métodos estáticos, métodos privados, protegidos y públicos
*
* La clase es un poco absurda pero simplemente trata de mostrar distintos
* tipos de métodos
*
* @author Eugenia Pérez, Pello Altadill
*/
public class Métodos {
Métodos () {
[Link]("Has creado instancia");
}
Métodos (String opcion) {
super();
[Link]("Opción pasada: " + opcion);
}
/**
* estático
* Aquellos que tienen la palabra static se consideran Método de clase,
* puede ser invocado sin que se cree una instancia
* de la clase. ver main.
* De estos métodos solo se crea una copia en memoria, al igual que pasa
* con los atributos que se declaran static.
*/
public static void estático (int veces) {
for (int i = 0; i < veces;i++) {
[Link]("Estático> " + i);
}
}
/**
* saludar
* Método público que devuelve un String
* Si no ponemos nada, se considera público
* @return String
*/
String saludar () {
saludoPrivado();
return "Hola yo te saludo.";
}
/**
* saludoPrivado
* Método que saca un mensaje por consola.
* Solo se puede invocar desde dentro de la clase
*/
private void saludoPrivado () {
[Link]("Java rulez");
}
/**
* tomarDato
* Método público lee un valor por consola y devuelve un entero
* Puede lanzar dos excepciones
* @return String
* @throws IOException, NumberFormatException
*/
public int tomarDato () throws IOException, NumberFormatException {
Console c = [Link]();
String linea = [Link]("Dame un número: ");
return [Link](linea);
}
/**
* main
* Función principal
* esta función es la que se inicia directamente al ejecutar el programa
* @throw IOException
*/
public static void main (String args[]) throws IOException
{
// Probamos el método estático. No necesitamos crear instancia!!!
Mé[Link]ático(5);
Métodos pruebaMétodos = new Métodos("hola");
pruebaMé[Link]();
int entero = pruebaMé[Link]();
}
}
NOTA
La declaración completa de un método se conoce como method
signature. Lo que distingue a un método de otro dentro de una clase es:
El nombre
El tipo de retorno
Los parámetros
Las excepciones
11.3 Variables
estáticas y
constantes
Las clases, además de tener atributos estáticos, también pueden tener
atributos constantes, es decir, que no cambian una vez se han inicializado.
Por tanto, una vez inicializadas, NO se puede cambiar su valor. En java las
constantes se indican utilizando el modificador final. Por ejemplo, una
constante para almacenar la relación euro/peseta:
private static final float EURO_PESETA = 166.386f;
NOTA
Atributos de referencia final static
Hay atributos final static que en lugar de un primitivo (int, float)
pueden ser clases como un ArrayList. Por tanto, son referencias y siempre
que no alteremos la referencia podemos añadir elementos y gestionar esa
referencia del arrayList sin problema, siempre que no cambiemos a otra
referencia.
NOTA
Inicializadores de instancia o estáticos
En las clases Java podemos usar bloques de código independientes al
margen de ningún método donde llevar a cabo inicializaciones de atributos
estáticos o de instancia. De esta forma, podemos declarar atributos de
instancia, atributos constantes y finales sin inicializarlos en el momento y
hacerlo más adelante en uno de esos bloques:
public class Currency {
private static final float EURO_PESETA;
private static int counter= 0;
private String name;
static {
EUROS_PESETA = 166.386f;
counter = 0;
}
{
name = ”Default”;
[Link](“Ok, name initialized”);
}
}
Obviamente, solo se pueden iniciar una vez aquellas que sean final.
NOTA
Orden de inicialización
Dentro de una clase podemos tener variables o atributos de clase, de
instancia, constructores, inicializadores static, etc. pero ¿en qué orden se
ejecutan? En este:
1. Si hay una superclase, primero se inicializa ésta.
2. Atributos estáticos e inicializadores estáticos según el orden
3. Atributos de instancia (no static) e inicializadores de
instancia
4. Constructores
11.4 Varargs
Hay métodos que pueden tener una lista de parámetros dinámica, es
decir, a veces pueden tener un parámetro, otras tres, etc... A nivel de código
se trata como un array. Todos los elementos deben ser, por tanto, del mismo
tipo.
public int addNumbers (int... numbers) {
int result = 0;
for (int i = 0;i< [Link];i++) {
result = result + numbers[i];
}
return result;
}
NOTA
Evita ambigüedades provocadas por varargs
Ojo, no se pueden poner los varargs de cualquier manera ya que
puede provocar que se forme código ambiguo:
public String concat1 (String... words) {}
public String concat2 (String word, String... words) {}
public String concat3 (String... words1, String words) {} // ERROR
DE COMPILACIÓN
public String concat4 (String... words1, String... words2) {} // ERROR DE
COMPILACIÓN
11.5 Sobrecarga
de métodos
Una de las características importantes de la programación orientada a
objetos es la sobrecarga de métodos. Esto se produce cuando tenemos
métodos que tienen el mismo nombre con distintos parámetros.
public int add (int a, int b) { }
public int add (Integer a, Integer b) { }
public float add (float a, float b) { }
public long add (long a, long b) { }
public void add (short a, short b) { }
public void add (double a, double b) throws Exception { }
NOTA
¿Qué método elige Java cuando hay sobrecarga?
A veces se pueden producir situaciones en las que más de un método
sobrecargado puede servir ante una llamada. Por ejemplo, si hacemos
add(40,2) Java procura elegir según estos criterios:
11.6.1 Ejercicio
1
Crea un proyecto con una clase llamada Calculator que contenga las
siguientes funciones estáticas para hacer operaciones con números enteros:
suma los números
1. public static int add (int numberA, int numberB):
numberA y numberB y retorna el resultado.
restar los números
2. public static int substract (int numberA, int numberB):
numberA y numberB y retorna el resultado.
multiplicar los
3. public static int multiply (int numberA, int numberB):
números numberA y numberB y retorna el resultado.
dividir los números
4. public static int divide (int numberA, int numberB):
numberA y numberB y retorna el resultado.
solicita al usuario un número para
5. public static int readNumber ():
que lo introduzca por la consola, lo convierte a entero y lo
retorna.
En una clase principal, crea la función main con un par de variables
enteras, llama a la función readNumber, prueba las cuatro operaciones
mostrando el resultado final por pantalla.
11.6.2 Ejercicio
2
Crea un proyecto con una clase llamada NumberArray que cree 5
funciones estáticas:
Este método debe
1. public static void initWithNumber (int[] numbers, int number)
inicializar todos los elementos del array numbers con el número
que pasamos como parámentro.
Este método debe recorrer el
2. public static void show (int[] numbers)
array numbers y mostrar cada uno de los elementos por
pantalla.
Este método debe
3. public static void initRandomly (int[] numbers)
inicializar todos los elementos del array numbers con un
número aleatorio del 0 al 10.
Este método selecciona
4. public static int getRandomElement (int[] numbers).
un elemento del array numbers al azar y lo devuelve.
Este método devuelve la
5. public static int getEven (int[] numbers).
cantidad de números pares que tiene el array numbers.
Crea una clase principal en la que en la función principal main se cree
un array de números enteros de 15 elementos:
int[] numbers = new int[15];
11.6.3 Ejercicio
3
Crea un proyecto con una clase llamada Dispenser en el que se haga lo
siguiente: debe mostrar al usuario una lista de 5 productos disponibles. El
usuario debe elegir uno de ellos y luego la cantidad. El programa calcula el
importe correspondiente y lo muestra por pantalla. A continuación, el
programa vuelve a mostrar los 5 productos otra vez y sigue haciendo lo
mismo repetidamente.
Debes definir dos arrays estáticos como atributos de clase para los
productos y precios:
private static String[] products = { "Water", "Coke", "Milk", "JellyBeans", "Chocolate" };
private static float[] prices = { 1f, 2f, 1.5f, 0.75f, 1.25f };
[Link]([Link](selected, qty));
11.6.4 Ejercicio
4
Crea un proyecto con una clase llamada Generator que contenga las
siguientes funciones:
un método que dada
1. public static String generatePassword(int length):
una longitud genere un String con caracteres aleatorios.
Puedes usar un array de Strings con caracteres e ir sacando
caracteres aleatorios de un array de vocales y consonantes
para generar un nombre.
11.6.5 Ejercicio
5-
Avanzado
Genera un proyecto llamado Attributes con una clase llamada
Attributes. La clase Atributos debe tener un método llamado
generateAttributes:
public static void generateAttributes (int compensationLevel)
Este método debe definir tres variables de tipo int: fuerza, velocidad e
inteligencia. El programa lo que debe hacer es repartir 20 puntos entre las tres
variables. O, dicho de otra forma, entre las tres variables deben sumar 20. El
parámetro compensationLevel debe servir para indicar si se reparten puntos
muy diferenciados o igualados, cuanto más alto el valor más descompensado,
es decir, si la diferencia entre atributos es mayor; cómo hacerlo es cosa del
programador.
Al final el programa debe mostrar un resumen de los puntos asignados.
12 POO
12.1 Introducción
La POO o Programación Orientada a Objetos surge como evolución de
la programación estructurada. Todo se fundamente en la forma de dividir los
programas.
Cuando hay que resolver un problema en programación se aplica el
divite et vinci, es decir, divide y vencerás: el problema se divide en pequeños
problemas para facilitar su solución. En la programación estructurada el
problema se dividía en funciones: el programa tiene que hacer esto, esto otro,
etc.…
Cuando se programa en POO el enfoque es diferente. El problema se
divide en conceptos, es decir, en clases. Y esas clases interactúan entre sí
para resolver los problemas. La POO se sustenta sobre tres pilares básicos:
1. Encapsulación: la POO trata de crear clases o componentes
que tienden a funcionar como cajas negras. De esta forma los
componentes no tienen por qué conocer todos los detalles del
resto y pueden ser reemplazados, mejorados o reutilizados en
otra parte.
2. Herencia: en la POO se permite que una clase herede todos
los atributos y métodos de otra. Gracias a eso la nueva
subclase puede extender o especializar la clase padre y facilita
el mantenimiento y mejora de las clases.
3. Polimorfismo: el polimorfismo consiste en poder usar el
mismo nombre de función para distintos tipos de parámetros o
clases. Gracias a eso desde fuera las clases tienen un aspecto
uniforme y ocultan los detalles de implementación para cada
caso.
12.2 Modificadores
de acceso
Llega el momento de explicar qué efectos tienen los modificadores de
acceso. Los modificadores nos permiten ajustar la encapsulación y la
visibilidad entre clases y se aplican a clases, sus atributos y métodos. Son
estos:
public: todo atributo o método de la clase declarado como
public es accesible desde cualquier otra clase.
protected: todo atributo o método declarado como protected
es accesible desde clases del mismo paquete o desde clases
que hereden de la propia clase.
private: todo atributo o método declarado como private NO
es accesible desde otras clases.
default: cuando no se indica nada, se considera que el
modificador es default. Todo atributo o método en el que no
se indica el modificador es accesible desde otras clases
dentro del mismo paquete.
OJO, NO se pueden poner explícitamente default, ya que no se
reconoce como modificador de acceso
default class Clase {
}
Puede Si el Si el Si el Si el
acceder miembro es miembro miembro es miembro es
privado tene acceso protected public
default
(mismo
paquete)
Un miembro Sí Sí Sí Sí
de la misma
clase
Un miembro No Sí Sí Sí
en otra clase
en el mismo
paquete
Un miembro No No Sí Sí
en una
superclase en
un paquete
diferente
Un miembro No No No Sí
en una clase
sin relación y
en un paquete
diferente
12.3 Herencia
Mediante este mecanismo una clase hereda de otra todos sus miembros
(atributos y métodos) públicos y protegidos. Es una medida que nos permite
ahorrar y extender código y, por tanto, facilitar enormemente el desarrollo y
mantenimiento de los proyectos.
Respecto a otros lenguajes, Java tiene una limitación: a una clase solo
le permite heredar de otra clase, es decir, no hay herencia múltiple. Esto se
puede ver como una limitación, pero también como una manera de restar
complejidad al lenguaje.
Para heredar de otra clase se debe utilizar la palabra reservada extends.
Por ejemplo, la clase Student y la clase Teacher, heredarán los atributos y
métodos de Person:
public class Person {
String name;
int age;
/**
* Person with parameters.
* @param name
* @param dni
* @param age
*/
public Person(String name, int age) {
[Link] = name;
[Link] = age;
}
/**
* Shows the features of the class.
*/
public void showFeatures(){
[Link]("Name: "+[Link]);
[Link]("Age: "+[Link]);
}
}
Y su extensión, Student:
public class Student extends Person {
12.4 Constructores
y herencia
Todas las clases deben tener al menos un constructor. En caso de no
hacerlo de forma específica, Java añade un constructor vacío. Un constructor
en Java puede hacer dos cosas en primer lugar:
1. Llamar a otro constructor de la propia clase con this(), con
argumentos si es necesario.
2. Llamar a un constructor de una clase padre de la que hereda
con super(), también con argumentos si es necesario.
En caso de no meter la llamada a super() no pasa nada, ya que Java se
encarga de incluir esa llamada de forma automática, igual que el constructor
por defecto. Por tanto, teniendo en cuenta lo comentado anteriormente, si
definimos una clase como esta:
public class Car {
}
12.5 Sobrescritura
de métodos
La sobrescritura u Overriding se produce cuando una clase hija
implementa el mismo método que una clase padre. Es decir, creando un
método con el mismo nombre y tipo de retorno.
Si por lo que sea en la clase hija nos quisiéramos referir al método de la
clase padre y no al sobrescrito, podemos usar la palabra super.
Reglas de sobrescritura
Existes una reglas básicas y obvias para poder sobrescribir métodos:
1. El método en la clase hija debe tener la misma signature.
2. El método en la clase hija debe ser al menos tan accesible
(public, protected) o más que la de la clase padre.
3. El método de la clase hija no puede lanzar una excepción
que sea nueva o más amplia que cualquiera de las
excepciones lanzadas en el método de la clase padre.
4. Si el método tiene un retorno, en el método de la clase
hija debe usarse el mismo tipo o una subclase de ese tipo
(conocido como covariant return type).
Ocultación de métodos estáticos
Una clase hija puede ocultar un método estático del padre creando un
método igual. Para crear este tipo de métodos (hidden method) la clase hija
debe declarar un método con la misma signatura que la del padre y ser
estático. El método oculto debe cumplir las reglas mencionadas antes para la
sobrescritura además de una nueva regla
5. El método debe estar marcado como static en la clase hija si
en el padre también lo está (Method hiding). Por contra el
método no debe estar marcado como static en la clase hija si
en la clase padre no lo está (Overriding).
Métodos final
Para evitar que un método sea sobrescrito podemos utilizar la palabra
final. Esta palabra indica a Java que no podemos ni sobrescribir ni si quiera
crear un método oculto. Esta clase de métodos tienen sentido cuando
queremos garantizar el comportamiento de un método sin que se sobrescriba
de ninguna manera.
Atributos
Los atributos NO se pueden sobrescribir en Java. Lo que si podemos
hacer es redeclarar un atributo en la clase hija que sea igual que el padre. De
esa manera, cuando se trabaje con una instancia de la clase hija usaremos el
atributo de la hija y no del padre.
NOTA
Redeclaración de métodos privados
Una clase hija puede redeclarar un método privado en su clase padre
sin ningún problema. A fin de cuentas, no tiene acceso a ese método del
padre y ni si quiera se trata de sobrescritura de métodos. Para la clase hija
este sería un método propio sin vínculo alguno con el padre.
12.6 Ejemplo
de
Herencia:
ClienteVip
// Libería necesaria para la clase Date
import [Link];
/**
* clase ClienteVip
* Muestra la declaración de ClienteVip, una extensión de la clase Cliente.
* Es un ejemplo simple de herencia, donde creamos una clase especializada
* que hereda todos los atributos y métodos de la clase padre.
* Para lograrlo debemos añadir la cláusula extends en la declaración
* de la clase.
*
* @author Eugenia Pérez, Pello Altadill
*/
public class ClienteVip extends Cliente {
// ATRIBUTOS o PROPIEDADES DE LA CLASE extendida:
public float descuento;
ClienteVip () {
// Lamamos al constructor de la clase padre
super();
[Link]("Has creado una instancia de ClienteVip");
}
/**
* ClienteVip
* Otro constructor con parámetros.
*/
ClienteVip (String nombre, String apellidos, Date nacimiento, int codigo, float descuento)
{
// Lamamos al constructor de la clase padre
super(nombre,apellidos,nacimiento,codigo);
// Establecemos el atributo descuento
[Link] = descuento;
[Link]("Has creado una instancia de ClienteVip");
}
/**
* aplicarDescuento
* Método que aplica el descuento del cliente a un determinado total
* @return float resultado
*/
public float aplicarDescuento (float precioTotal) {
float resultado = precioTotal * (1-descuento);
return resultado;
}
/**
* quitarIVA
* Método que le quita el IVA al cliente de un total
* @return float
*/
public float quitarIVA (float precioTotal) {
float resultado = precioTotal * (0.82F);
return resultado;
}
/**
* fichaCliente
* Método que sobrescribe al de la clase padre Cliente,
* añadiendo un dato más.
*/
public void fichaCliente () {
// al principio hace los mismo que la clase padre.
[Link]();
[Link]("CLIENTE VIP");
[Link]("Descuento: " + descuento);
}
/**
* main
* Función principal
* esta función es la que se inicia directamente al ejecutar el programa
* Y desde ella vamos a crear una instancia de ClienteVip
*/
public static void main (String args[]) {
// Creamos un par de instancias
ClienteVip unClienteVip = new ClienteVip();
ClienteVip otroClienteVip = new ClienteVip("Frodo","Bolson", new Date(), 19, 0.10F);
[Link] = 89;
[Link]();
[Link]("Total 109.56 euros, con descuento del " +
[Link] + "%: " +
[Link](109.56F));
[Link]();
}
}
12.7 Ejemplo
de Clase
abstracta:
Personaje
import [Link];
/**
* clase Personaje
* Muestra la declaración de una clase ABSTRACTA que representa un Personaje.
* De una clase abstracta no se pueden crear instancias. Sirve únicamente como
* una clase genérica de la que otras deben especializarse.
* Puede tener atributos y métodos implementados, que podrán ser heredados.
*
* @author Eugenia Pérez, Pello Altadill
*/
public abstract class Personaje {
// ATRIBUTOS o PROPIEDADES DE LA CLASE.
// ¿public, protected o private?
// Haciendo un chiste malo, ¿cómo considerarías
// tus propios atributos?
// - Si son públicos, es que eres un exhibicionista
// - Si son protegidos es que a alguien de confianza los enseñas
// - Si son privados, es que NUNCA los enseñarás.
// Atributos públicos, accesibles desde cualquier clase
public String nombre;
public String profesion;
// Atributos protegidos, solo accesibles desde clases HIJAS
// que extiendan esta
protected int fuerza;
protected int inteligencia;
protected int agilidad;
// Atributos privados, inaccesibles.
private int edad;
private Random aleatorio;
private int monedas;
// MÉTODOS DE LA CLASE: Constructores, y otras funciones
/**
* Personaje
* este es el método constructor, al que se invoca
*/
Personaje () {
[Link]("Has creado una instancia de Personaje sin nombre.");
// Variable para generar números aleatorios
aleatorio = new Random();
// Llamamos a la función privada para establecer
// los atributos de fuerza, inteligencia, agilidad y edad
establecerAtributos();
// Y ahora establecemos la edad
establecerEdad();
}
/**
* Personaje
* Otro constructor con parámetros.
* Nos sirve para crear una instancia especificando el nombre y profesión
*/
Personaje (String nombre, String profesion) {
[Link]("Has creado una instancia de Personaje");
[Link] = nombre;
[Link] = profesion;
// Variable para generar números aleatorios
aleatorio = new Random();
// Llamamos a la función privada para establecer
// los atributos de fuerza, inteligencia, agilidad y edad
establecerAtributos();
}
/**
* Personaje
* Otro constructor con parámetros.
* Nos sirve para crear una instancia especificando el nombre y profesión
* y otros atributos
*/
Personaje (String nombre, String profesion, int fuerza, int inteligencia, int agilidad) {
[Link]("Has creado una instancia de Personaje");
[Link] = nombre;
[Link] = profesion;
[Link] = fuerza;
[Link] = inteligencia;
[Link] = agilidad;
// Variable para generar números aleatorios
aleatorio = new Random();
// Y ahora establecemos la edad
establecerEdad();
}
/**
* getEdad
* Método que muestra la edad del personaje
* Sirve para poder ver la edad SIN dar acceso al atributo privado
*/
public int getEdad () {
return edad;
}
/**
* establecerAtributos
* Método privado para establecer los atributos iniciales.
*/
private void establecerAtributos () {
// Y ahora establecemos los atributos con números aleatorios
fuerza = [Link](10);
inteligencia = [Link](10);
agilidad = [Link](10);
edad = [Link](200);
}
/**
* establecerEdad
* Método privado para establecer la edad.
*/
private void establecerEdad () {
edad = [Link](200);
}
/**
* fichaPersonaje
* Método que muestra todos los datos del Personaje
*/
public void fichaPersonaje () {
[Link]("--Ficha del Personaje--");
[Link]("\tNombre: " + nombre);
[Link]("\tProfesión: " + profesion);
[Link]("\tEdad: " + edad);
[Link]("--Atributos--");
[Link]("\tFUE: " + fuerza);
[Link]("\tINT: " + inteligencia);
[Link]("\tAGI: " + agilidad);
}
}
12.8 Ejemplo de
Herencia:
PersonajeOrco
/**
* clase PersonajeOrco
* Muestra la declaración de la clase PersonajeOrco, que extiende la clase
* abstracta Personaje. Por tanto, reutiliza todo su contenido y le permite acceder
* a atributos y métodos PÚBLICOS o PROTEGIDOS.
*
* @author Eugenia Pérez, Pello Altadill
*/
public class PersonajeOrco extends Personaje {
// ATRIBUTOS o PROPIEDADES DE LA CLASE.
// Esta clase puede tener algun propiedad propia
public float hedor;
public String tribu;
// MÉTODOS DE LA CLASE: Constructores, y otras funciones
/**
* PersonajeOrco
* este es el método constructor, al que se invoca
*/
PersonajeOrco () {
super();
}
/**
* PersonajeOrco
* Otro constructor con parámetros.
* Nos sirve para crear una instancia especificando el nombre y profesión
*/
PersonajeOrco (String tribu, float hedor) {
super();
[Link] = tribu;
[Link] = hedor;
[Link]("Has creado una instancia de PersonajeOrco, sin nombres!");
}
/**
* PersonajeOrco
* Otro constructor con parámetros.
* Nos sirve para crear una instancia especificando el nombre y profesión
* y otros atributos
*/
PersonajeOrco (String nombre, String profesion, String tribu, float hedor) {
super(nombre, profesion);
[Link] = tribu;
[Link] = hedor;
[Link]("Has creado una instancia de PersonajeOrco");
}
/**
* fichaPersonaje
* Método que muestra todos los datos del PersonajeOrco
*/
public void fichaPersonaje () {
[Link]();
[Link]("\tRaza: ORCO");
[Link]("\tTribu: " + nombre);
[Link]("\tHedor: " + hedor + " olfs");
}
/**
* main
* Función principal
* esta función es la que se inicia directamente al ejecutar el programa
* Y desde ella vamos a crear una instancia de PersonajeOrco, que es una clase
* hija de la clase abstracta Personaje
*/
public static void main (String args[]) {
// Creamos un par de instancias
PersonajeOrco orcoMalo = new PersonajeOrco();
12.9 Clases
abstractas
Las clases abstractas son clases que no se pueden instanciar
directamente, pero sí se pueden extender. Generalmente cuando se define una
jerarquía de clases la superclase se declara como abstract ya que solo debe
servir como clase base para otras clases más concretas (NOTA: Por ese
motivo nunca deberíamos ver una clase abstract que también sea final)
Por ejemplo, en una aplicación de gestión de un centro educativo,
podríamos tener la siguiente clase:
public abstract class Person {
protected String name;
public abstract String getData();
}
Como se puede ver en la clase Person, una clase abstracta puede definir
métodos abstractos, que se caracterizan por NO estar implementados:
public abstract String getData();
NOTA
Abstractas extendidas por abstractas
Una clase abstracta puede ser extendida por otra clase abstracta.
Siendo así, NO está obligada a implementar los métodos abstractos de la
clase padre. Eso sí, en el momento en que una clase concreta extienda a la
abstracta tendrá que implementar todos los métodos:
public abstract class Shape {
public abstract String getData();
public abstract float area();
}
12.10 Polimorfismo
Estas es una de las características básicas de la programación orientada
a objetos y consiste en que un objeto puede tomar diferentes formas. De esa
forma podemos gestionar un objeto a través de una referencia de una
superclase, una subclase o un interfaz.
Supongamos que tenemos las siguientes clases e interfaz:
public interface Device {
public boolean isOn();
}
public class Mobile {
public void call (int number) {
}
}
12.11 Métodos
virtuales
Gracias al polimorfismo, podemos crear métodos cuya implementación
no estará determinada hasta el tiempo de ejecución. De hecho, todos aquellos
métodos que no sean ni final, ni static ni privados pueden ser virtuales ya que
a través de la herencia pueden ser sobrescritos en cualquier momento.
Retomando el ejemplo anterior:
public class Mobile {
public void call (int number) {
[Link](“Calling from Mobile to ” + number);
}
}
public class SmartPhone extends Mobile {
public void call (int number) {
[Link](“Calling from Smartphone to ” + number);
}
}
//...
Mobile mobile = new SmartPhone();
[Link](948124126); // “Calling from Smartphone to 948124126”
12.12 Parámetros
polimórficos
El polimorfismo nos permite también definir parámetros que pueden
tomar distintas formas. Por ejemplo, podemos crear una clase que sirva para
operar tanto con Mobile como con SmartPhone. Añadiremos otra clase más que
extiende Mobile:
public class Phablet extends Mobile {
}
12.13 Cast o
conversión
de objetos
Como se ve en el ejemplo de polimorfismo, podemos cambiar una
referencia a un mismo objeto, pero a veces si la referencia pasa a ser una
clase muy específica a otra más general podemos perder acceso a ciertos
métodos:
SmartPhone smartPhone = new SmartPhone();
Mobile mobile = smartPhone;
SmartPhone otherSmartPhone = mobile; // ERROR DE COMPILACIÓN
12.14.1 Ejercicio
1
Crea un proyecto llamado Devices que incluya una serie de clases. Antes de
empezar a programar dibuja el diagrama de clases, que serán las
siguientes.
1. Clase Device: tiene los atributos protegidos String name, String
brand y float price. Un constructor usando los atributos, los set y
get y un método público toString mostrando los atributos.
2. Clase Mobile: es una subclase de Device, hay que añadir el
atributo privado String number. Crea el constructor y el método
toString aprovechando los de la superclase. Añade el método
público call (String number), que saque por pantalla una cadena
diciendo “calling number”
3. Clase Computer: es una subclase de Device, hay que añadir el
atributo privado String processor. Crea el constructor y el método
toString aprovechando los de la superclase
No hay nada más, no hay clase con método main.
12.14.2 Ejercicio
2
Crea un proyecto llamado School que incluya una serie de clases. Antes de
programar dibuja el diagrama de clases con umlet. Las clases tendrán las
siguientes características.
1. Clase Person: tiene los atributos protegidos String name, int age.
Un constructor vacío, otro constructor usando los dos atributos,
los métodos set/get y un método toString()
2. Clase Teacher: es una subclase de Person. Debe tener el atributo
privado String degree, y otro que sea String [] subjects (un array
de asignaturas). Debe tener un constructor con todos los campos.
Además, debe tener lo métodos set/get para los atributos y
también un método toString generado automáticamente.
3. Clase Student: es una subclase de Person. Debe tener los
atributos privado String course y String degree. Debe tener un
constructor con todos los campos, métodos set y get y un método
toString.
4. Clase Main: debe crear una instancia de Teacher y otra de
Student y sacarlas por pantalla llamando a sus respectivos
métodos toString().
12.14.3 Ejercicio
3
Crea un proyecto llamado War que incluya una serie de clases. Antes
de programar dibuja el diagrama de clases con UML. Las clases tendrán las
siguientes características.
1. Clase Unit: tiene los atributos protegidos String division y String
name. Un constructor vacío, otro constructor usando los dos
atributos, los métodos set/get y un método toString(). También
debe tener un método público llamado fire() y otro llamado
move() que devuelven un entero aleatoriamente.
2. Clase Tank: tiene los atributos privados int armor, int ammo,
String model. Un constructor con todos los parámetros, métodos
set/get y un método toString(). También debe tener un método
público llamado turn (muestra un mensaje por pantalla con la
acción). Podemos considerar que tanto Tank como la siguiente
clase, Soldier, extienden la clase Unit.
3. Clase Soldier: tiene los atributos privados String rank, int age.
Un constructor con todos los parámetros, métodos set/get y un
método toString(). También debe tener un método llamado prone
(muestra un mensaje por pantalla con la acción).
4. Clase Main: debe tener el método main, crear una instancia de
Tank y de Soldier y sacarlas por pantalla llamando a sus
respectivos métodos toString().
12.14.4 Ejercicio
4
Crea un proyecto java llamado HelloKitty en el que la protagonista
gestiona una Cesta de comida. La comida será de varios tipos. Antes de
empezar a programar, dibuja el diagrama de clases UML. Estas son las
clases que se debes hacer:
1. Clase Food: tiene los atributos protegidos String name y float
weight. Un constructor usando los atributos, los set y get y un
método público toString mostrando los atributos. Por último
añade un método abstracto int getNutriotionalValue().
2. Clase Fruit: es una subclase de Food, y tiene el atributo privado
String vitamin. Crea el constructor y el método toString
aprovechando los de la superclase. Tanto Fruit como Candy
implementarán el método abstracto, de forma que retornen un
valor entero aleatorio, siendo siempre el de la fruta mayor.
3. Clase Candy: es una subclase de Food y tiene el atributo
privado int calories. Crea el constructor y el método toString
aprovechando los de la superclase.
4. Clase Basket, tiene un atributo Vector de elementos tipo Food.
Se inicializa en el constructor. Tiene estos métodos públicos:
12.14.5 Ejercicio
5-
Avanzado
Crea un proyecto java llamado JavaKart contenga las siguientes clases, y
antes de empezar a programar, dibuja el diagrama de clases UML
1. Clase Vehicle, que contiene los atributos protegidos String name,
int speed, int acceleration e int grip. Tiene un constructor con el
parámetro name, los set y get para los atributos y un método
toString generado automáticamente. Tiene estos métodos:
13.1 Atributos
de
interfaces
Las interfaces pueden declarar atributos, pero con limitaciones:
1. Se consideran public, y se marcan como static y final, por
tanto, no podemos declarar atributos como protected o
private.
2. El valor debe asignarse en el momento de declararse.
Por ejemplo:
public interface Sample {
public static final int SPECIAL_VALUE = 42;
}
13.2 Métodos
default
En principio los interfaces se utilizan para declarar métodos abstractos
que deben ser implementados, pero desde la versión 8 de Java también se
pueden crear métodos con su implementación. Se les considera default
method y son métodos implementados que llevan la palabra reservada
default:
public interface Sample {
public static final int SPECIAL_VALUE = 42;
public default void doSomething () {
// do stuff
}
}
13.3 Métodos
static
Un interface puede tener métodos estáticos desde la versión 8 de Java.
Al ser un método estático no se hereda ni debe ser implementado. Pero debe
cumplir ciertos requisitos
1. Como el resto de métodos de un interfaz, un método static
se considera public, y no puede marcarse como protected o
private.
2. Para poder referirse al método estático de un interfaz, debe
usarse la referencia al nombre del interfaz.
Por ejemplo:
public interface Initializer {
static int init () {
return 1;
}
}
Y para usarlo:
// ...
int value = [Link]();
//...
13.4 Ejemplo de
Declaración
de interfaz:
Log
/**
* clase Log
* Clase que muestra la declaración de un interfaz.
* Los interfaces son una especie de plantilla o contrato
* que define una serie de métodos, pero NO los implementa.
* Otras clases deben ser quienes implementen el interfaz y
* definir el código de esos métodos.
*
* Un interfaz no puede ejecutarse, debe implementarse por otra clase.
* Los interfaces permiten imitar en cierto modo el mecanismo de la HERENCIa MÚLTIPLE
* cosa que en java NO existe. En cambio, una clase de java puede implementar
* más de un interface.
*
* En este ejemplo definimos un interfaz llamado Log que va a servirnos para definir
* una función que permita guardar registros o eventos a modo de mensaje de texto.
* A partir de este interfaz se pueden crear distintas implementaciones: una que muestre
* los logs en pantalla, otra que los almacene en memoria, otra implementación que guarde
* los logs en un fichero.
*
* NOTA: No se necesitan los modificadores public, protected o private, SOLO se acepta
* public.
* Si no se pone nada se asume por tanto que es public.
* NOTA2: NINGÚN método puede estar definido.
*
* @author Eugenia Pérez, Pello Altadill
*/
public interface Log {
/**
* Los interfaces pueden definir atributos
* pero DEBE asignársele un valor!
*/
// Este será un prefijo que mostrarán todos los mensajes
public String prefijo = "Log> ";
// Este será un contador de los mensajes mostrados
public static int totalMensajes = 0;
// Este es el método que deberán definir las clases
// que implmenten este interfaz
public void log (String msg);
// Este añade el mensaje al log.
public void append (String msg);
}
13.4.1 Implementación
de interfaz:
LogConsola
import [Link].*;
import [Link];
/**
* clase LogConsola
* Esta clase muestra cómo se interpreta la interfaz Log.
* La clase por tanto debe preocuparse de implementar las dos funciones
* log y append.
*
* Aparte de eso, LogConsola es una clase normal y puede tener más funciones,
* atributos, constructores, etc...
*
* @author Eugenia Pérez, Pello Altadill
*/
public class LogConsola implements Log {
// Esta variable índica la fecha del último log
private Date fecha;
LogConsola () {
}
/**
* log
* Método definido por el interfaz que debemos implementar.
* En esta implementación, log muestra un mensaje por pantalla
*/
public void log (String msg) {
fecha = new Date();
[Link](prefijo + fecha + "> " + msg);
}
/**
* append
* Método definido por el interfaz que debemos implementar
* Se encarga de añadir un texto a un log
*/
public void append (String msg) {
[Link](msg);
}
}
13.4.2 Implementación
de interfaz:
LogFichero
import [Link].*;
import [Link].*;
/**
* clase LogFichero
* Esta clase muestra cómo se interpreta la interfaz Log.
* La clase por tanto debe preocuparse de implementar las dos funciones
* log y append. En este caso lo hace guardando los mensajes en un fichero
*
* Aparte de eso, LogFichero es una clase normal y puede tener más funciones,
* atributos, constructores, etc...
*
* @author Eugenia Pérez, Pello Altadill
*/
public class LogFichero implements Log {
// Esta variable índica la fecha del último log
private Date fecha;
// Esta variable indica el nombre del fichero de log
private String nombreFichero;
// Estos atributos los necesitamos para escribir en un fichero
private File fichero;
private FileWriter escritorFichero;
private BufferedWriter bufferEscritura;
/**
* LogFichero
* método constructor
*/
LogFichero () {
nombreFichero = "[Link]";
abrirFichero();
}
/**
* LogFichero
* método constructor con parámetro para indicar el nombre del fichero
* @param String fichero
*/
LogFichero (String nombreFichero) {
[Link] = nombreFichero;
abrirFichero();
}
/**
* finalize
* método que se invoca automáticamente al eliminarse el objeto
* Lo aprovechamos para asegurarnos de que se cierra el fichero
*/
public void finalize () {
cerrarFichero();
}
/**
* cerrarFichero
* método para cerrar el fichero
*/
public void cerrarFichero () {
try {
[Link]();
[Link]();
} catch (IOException ioe) {
[Link]("Error al cerrar: " + [Link]());
} catch (Exception e) {
[Link]("Ocurrió otro error no controlado: " + [Link]());
}
// Con esto capturamos cualquier otra cosa, es lo más genérico
/*catch (Throwable t) {
throw(t);
}*/
}
/**
* abrirFichero
* Método privado que abre el fichero
*/
private void abrirFichero () {
try {
fichero = new File(nombreFichero);
escritorFichero = new FileWriter(fichero);
bufferEscritura = new BufferedWriter(escritorFichero);
} catch (IOException ioe) {
[Link]("Error al escribir: " + [Link]());
}
}
/**
* log
* Método definido por el interfaz que debemos implementar.
* En esta implementación, log muestra un mensaje por pantalla
* Debemos capturar una excepción por si casca la escritura en el fichero
*/
public void log (String msg) {
fecha = new Date();
// Debemos iniciar un bloque try catch por si acaso
try {
[Link](prefijo + fecha + "> " + msg + "\n");
} catch (IOException ioe) {
[Link]("Error al escribir: " + [Link]());
}
}
/**
* append
* Método definido por el interfaz que debemos implementar
* Se encarga de añadir un texto a un log
*/
public void append (String msg) {
// Debemos iniciar un bloque try catch por si acaso
try {
[Link](msg);
} catch (IOException ioe) {
[Link]("Error al escribir: " + [Link]());
}
}
}
13.4.3 Implementación
de interfaz:
LogMemoria
import [Link].*;
import [Link].*;
/**
* clase LogMemoria
* Esta clase muestra cómo se interpreta la interfaz Log.
* La clase por tanto debe preocuparse de implementar las dos funciones
* log y append. En este caso lo hace guardando los mensajes en un objeto en memoria
*
* Aparte de eso, LogMemoria es una clase normal y puede tener más funciones,
* atributos, constructores, etc...
*
* @author Eugenia Pérez, Pello Altadill
*/
public class LogMemoria implements Log {
// Esta variable índica la fecha del último log
private Date fecha;
// Este objeto será quien contenga los mensajes
// Vector es una clase que nos sirve como array mejorado
private Vector mensajes;
// Contador de mensajes. Todas las implementaciones de esta clase
// van a COMPARTIR esta variable porque es STATIC
private static int contador = 0;
/**
* LogMemoria
* método constructor
*/
LogMemoria () {
mensajes = new Vector();
}
/**
* log
* Método definido por el interfaz que debemos implementar.
* En esta implementación, log guarda el mensaje en pantalla
*/
public void log (String msg) {
fecha = new Date();
[Link](prefijo + fecha + "> " + msg + "\n");
contador++;
}
/**
* append
* Método definido por el interfaz que debemos implementar
* Se encarga de añadir un texto a un log
*/
public void append (String msg) {
[Link](msg);
}
/*
* verTodo
* método que vuelca el contenido de los logs
*/
public void verTodo () {
for (int i = 0; i< [Link](); i++) {
// Mostramos por pantalla.
// El objeto mensajes es un Vector de objetos y debemos especificar
// qué es lo que devuelve por eso hacemos (String)
[Link]((String)[Link](i));
}
}
}
13.4.4 Probando
el
interfaz:
ProbarLog
/**
* clase ProbarLog
* Muestra varios ejemplos de uso de las clases que implementan
* la interfaz Log.
*
* @author Eugenia Pérez, Pello Altadill
*/
public class ProbarLog {
public static void main (String args[]) {
LogConsola logConsola = new LogConsola();
LogFichero logFichero = new LogFichero("[Link]");
LogMemoria logMemoria = new LogMemoria();
int i = 0;
NOTA
Métodos abstractos y herencia múltiple
¿Que pasa cuando se implementan varios interfaces y estos contienen
un método exactamente igual o parecidos? Ese es precisamente uno de los
problemas típicos por lo que Java evitó la herencia múltiple.
public interface Keyboard {
public int status();
public int input();
}
13.6 Ejercicios
13.6.1 Ejercicio
1.
Construye una interfaz Statistics con los siguientes métodos. Será
implementada por una clase RealNumbers, que contendrá un array de double.
Dentro del constructor de la clase RealNumbers, crearás el array con
una dimensión inicial de 5, y llamarás al método privado initilize(), que se
encargará de inicializar las posiciones del array con números double
aleatorios entre 0 y 99 (simplemente multiplica el número real aleatorio por
100).
También implementa el resto de métodos de la interfaz:
13.6.2 Ejercicio
2.
Vamos a representar el siguiente UML:
Crearemos una interfaz Vehicle. Fíjate que una interfaz solo debe
contener métodos (públicos) sin implementar o bien CONSTANTES (o
atributos con un valor inicial). MAX_SPEED vale 120.
Tanto Moto como Coche deben implementar la interfaz con sus
características. El método frenar recibe la cantidad a frenar, y retorna un
mensaje indicando la velocidad actual de la moto o el coche, una vez frenada.
Lo mismo sucederá con acelerar, teniendo en cuenta que nunca podemos
exceder la velocidad máxima. El método que devuelve las posiciones, en
realidad retorna la constante de la clase (POSITIONS) que será inicializada en
función del número de plazas del vehículo: Motorbike, 2, y Car, 4.
Finalmente, crea un objeto de tipo Motorbike y otro de tipo Car, dentro
del main, y muestra el resultado de la llamada a sus métodos frenar y
acelerar.
13.6.3 Ejercicio
3.
Vamos a representar el siguiente UML:
13.6.4 Ejercicio
4.
Crea un proyecto con una interfaz llamada Agenda que contenga el
siguiente método:
public void showContacts()
14.1 Ejemplo de
Captura de
Excepciones
import [Link].*;
/**
* clase Calculadora
* La clase tiene funciones para solicitar datos al usuario
* y ejecutar operaciones. Si el usuario mete un dato que no debe
* el programa falla. El programa no puede coger de los huevos al usuario
* pero sí puede prever sus errores, capturarlos y reconducir la ejecución
* para llevarla a buen puerto. Las excepciones sirven para mantener el control
* del programa ante situaciones que se escapan a nuestro control: datos de entrada,
* fallos de la red, bases de datos inconsistentes, etc...
*
* También es posible definir nuestras propias excepciones.
*
* @author Eugenia Pérez, Pello Altadill
*/
public class Calculadora {
public float operando1;
public float operando2;
public char operador;
public float resultado;
Calculadora () {
resultado = 0;
iniciar();
}
/**
* iniciar
* Bucle infinito para las operaciones
*/
private void iniciar () {
do {
[Link]("Nueva operación: ");
leerOperandos();
leerOperador();
realizarOperacion();
mostrarResultado();
} while (true);
}
/**
* leerOperandos
* Solicita al usuario los dos operandos para la operación
*/
public void leerOperandos () {
// Para leero por consola hay que crear
// dos instancias de clases especiales de E/S
InputStreamReader lectorFlujoEntrada;
BufferedReader lectorBuffer;
String lectura = "";
try {
lectorFlujoEntrada = new InputStreamReader([Link]);
lectorBuffer = new BufferedReader(lectorFlujoEntrada);
[Link]("POr favor, introduzca el primer operando: ");
lectura = [Link]();
// Atención a la CONVERSIÓN. Utilizamos la clase Float,
// y sú método para convertir de String al tipo simple float
operando1 = [Link](lectura);
[Link]("POr favor, introduzca el segundo operando: ");
lectura = [Link]();
// Atención a la CONVERSIÓN. Utilizamos la clase Float,
// y sú método para convertir de String al tipo simple float
operando2 = [Link](lectura);
} catch (IOException ioe) {
[Link]("Error al manejar entrada/salida: " + [Link]());
} catch (NumberFormatException nfe) {
[Link]("Los datos no parecen numéricos: " + [Link]());
// Reconducimos e iniciamos.
iniciar();
}
}
/**
* leerOperador
* Solicita al usuario el operador para la operación
*/
public void leerOperador () {
// Para leero por consola hay que crear
// dos instancias de clases especiales de E/S
InputStreamReader lectorFlujoEntrada;
BufferedReader lectorBuffer;
String lectura = "";
try {
lectorFlujoEntrada = new InputStreamReader([Link]);
lectorBuffer = new BufferedReader(lectorFlujoEntrada);
[Link]("POr favor, introduzca la operación a realizar: ");
lectura = [Link]();
// La conversión es simple: del String que hemos leido
// sacamos el primer caracter
operador = [Link](0);
// No es el operador que esperabamos?
if (operador != '+' && operador !='-' && operador != '*' && operador!= '/')
{
throw new ExcepcionOperadorInvalido();
}
} catch (IOException ioe) {
[Link]("Error en los datos de entrada: " + [Link]());
operador = '+';
} catch (ExcepcionOperadorInvalido eoi) {
[Link]("Problemas en el operador: " + [Link]());
} catch (Exception e) {
[Link]("Ocurrió una excepción no prevista: " + [Link]());
} finally {
// En la cláusula finally entraría en cualquier caso, haya excepción o no
}
}
/**
* realizarOperacion
* Lleva a cabo la operación
*/
public void realizarOperacion () {
switch (operador) {
case '+':
resultado = operando1 + operando2;
break;
case '-':
resultado = operando1 - operando2;
break;
case '*':
resultado = operando1 * operando2;
break;
case '/':
resultado = operando1 /
operando2; break;
}
}
/**
* mostrarResultado
* Muestra el resultado de la operación
*/
private void mostrarResultado () {
[Link]("\n\tEl resultado es: " + resultado +"\n\n");
}
/**
* main
* Función principal
* esta función es la que se inicia directamente al ejecutar el programa
* ATENCIÓN: debemos incluir una sentencia en la declaración de la función
* para "avisar" de que puede ocurrir un error. Lo exige el compilador
*/
public static void main (String args[]) throws IOException {
// Basta con crear la instancia
// desde el constructor se lanza el resto
Calculadora calculadora = new Calculadora();
}
}
14.2 Ejemplo de
Excepciones
propias
import [Link].*;
/**
* clase ExcepcionOperadorInvalido
* La clase extiende la clase Exception y nos sirve para crear excepciones
* personalizadas propias.
*
* También es posible definir nuestras propias excepciones.
*
* @author Eugenia Pérez, Pello Altadill
*/
public class ExcepcionOperadorInvalido extends Exception {
/**
* ExcepcionOperadorInvalido
* Método constructor
*/
ExcepcionOperadorInvalido () {
super();
}
/**
* ExcepcionOperadorInvalido
* Método constructor
*/
ExcepcionOperadorInvalido (String msg) {
super(msg);
}
/**
* getMessage
* mensaje personalizado de la excepción
*/
public String getMessage() {
return "Operador no válido. Debe ser +, -, * o /";
}
14.3 Ejercicios
14.3.1 Ejercicio
1
Crea un proyecto con una clase Main, desde la cual vamos a hacer el
tratamiento adecuado de las distintas excepciones. Te copio, a continuación,
distintos fragmentos de código, que incluirás dentro de tu main y que son
susceptibles de lanzar excepciones. Se pide que las captures, y muestres un
mensaje acorde a dicha excepción.
a)
int a = 4;
int b = 0;
[Link](a / b);
b)
int[] array = new int[5];
array[5] = 1;
14.3.2 Ejercicio
2
Define una referencia a un objeto e inicialízala a null. Trata de invocar
un método a través de esta referencia. Ahora rodea el código con una
cláusula try-catch para probar la excepción.
14.3.3 Ejercicio
3
Crea tu propia clase de excepción utilizando la palabra clave extends.
Escribe un constructor para dicha clase que tome un argumento String y lo
establezca. Escribe un método que muestre la cadena de caracteres
almacenada. Desde la clase Principal, en el método main crea una
cláusula try-catch para probar la nueva excepción.
15 API
JAVA
Java dispone de un montón de clases útiles para facilitarnos el trabajo.
Precisamente en el paquete [Link] tenemos algunas clases que veremos a
continuación, en especial las que nos sirven para manejar estructuras de
datos.
Empezamos con la estructura Vector, que si bien queda obsoleta por
el ArrayList no está de más revisar ya que sigue presente en mucho código
existente.
15.1 Vectores
Los vectores son una estructura similar a los arrays, pero mucho más
flexible y versátil. Cuando declaramos un Vector y cualquier estructura de
datos conviene indicar qué clases va a contener. De esta manera nos
ahorramos hacer conversiones:
Vector<String> names = new Vector<String>();
Método Significado
.size() Cantidad de elementos que
contiene el Vector
.addElement(elemento) Añade un nuevo elemento
al final del Vector
.removeElement(objeto) Elimina un elemento del
Vector que contenga ese objeto
.removeElementAt(indíce) Elimina el elemento del
Vector que se encuentre en esa
posición
.removeAllElements() Elimina todos los
elementos del Vector
NOTA
El autoboxing
15.1.1 Ejemplo
de
Vectores
import [Link].*;
import [Link].*;
/**
* clase Vectores
* Muestra el uso de la estructura de datos Vector
* El Vector es un super array que permite tamaño dinámico
* y añade muchos métodos para gestionar su contenido.
* Los elementos pueden ser cualquier objeto y se pueden repetir.
*
* @author Eugenia Pérez, Pello Altadill
*/
public class Vectores {
/**
* main
* Función principal
* Desde la función principal probamos un Vector
*/
public static void main (String args[])
{
Jugador crack = new Jugador("Maradona",10);
Jugador catacrack = new Jugador("Drenthe",13);
Jugador cr7 = new Jugador("Ronaldo",7);
// Creamos una instancia de Vector para guardar una plantilla
Vector plantilla = new Vector();
// Metemos unos elementos:
[Link](crack);
[Link](catacrack);
[Link](cr7);
// Recorrer la estructura con un for;
for (int i = 0; i < [Link]();i++) {
((Jugador)[Link](i)).sacarDatos();
}
[Link]("Otra pasada con Iterator:");
// Vamos a recorrer con un Iterator
Iterator iterador = [Link]();
// Nota : enumeration está algo desfasado...
// podemos usar un while
while ([Link]()) {
// Tenemos que forzar el tipo que devuelve lista a Jugador
Jugador jugadorTmp = (Jugador)[Link]();
// Ahora ya podemos jugar con esa variable
[Link]();
}
// Vamos a quitar elementos:
// por referencia de objeto -el primero que encuentre-
[Link](cr7);
// por índice
[Link](0);
// Añadimos un elemento creado al vuelo:
[Link](new Jugador("Torres",9));
[Link]("Otra pasada con Enumeration:");
// Recorrer la estructura con el interfaz Enumeration
// Los elementos del Vector pasan a ser un Enumeration
// con unas funciones añadidas: hasMoreElements y nextElement
// Nota : enumeration está algo desfasado...
// podemos usar un while
for (Enumeration lista = [Link]();[Link]();) {
// Tenemos que forzar el tipo que devuelve lista a Jugador
Jugador jugadorTmp = (Jugador)[Link]();
// Ahora ya podemos jugar con esa variable
[Link]();
}
// Podemos dejar el Vector vacío:
[Link]();
/**
* Clase Jugador
* la usamos como objeto para manejar con un vector
*/
Public class Jugador {
public String nombre;
public int dorsal;
Jugador (String nombre, int dorsal) {
[Link] = nombre;
[Link] = dorsal;
}
/**
* sacarDatos
* Muestra los datos de una jugador
*/
public void sacarDatos () {
[Link]("\nNombre: " + nombre + "\nDorsal: " + dorsal);
}
}
15.2 Hashtables
Los Hashtables son una estructura de datos que almacenan objetos
indexándolas con otro objeto, que puede ser desde un String a cualquier otra
cosa. Dicho de otra manera: para acceder a un objeto almacenado en un
Hashtable NO usamos un índice numérico como en los arrays (0,1,2,3,...),
sino un objeto que sea único como, por ejemplo, un String que representa un
DNI, una matrícula, etc… Por lo tanto, para guardar un objeto en un
Hashtable necesitamos guardar dos cosas: el objeto en sí, y una clave que lo
distinga.
Gráficamente podría verse así:
Método Significado
.size() Devuelve la cantidad de
elementos del Hashtable
.put(clave, valor) Añade un elemento en el
Hashtable
.get(clave) Recupera un elemento del
Hashtable
.containsKey(clave) Indica si determinada clave
existe
.remove(clave) Elimina el elemento del
Hashtable que tenga esa clave
.clear() Elimina todos los elementos del
Hashtable
15.2.1 Ejemplo
de
Hashtables
import [Link].*;
import [Link].*;
/**
* clase Hashtables
* Muestra el uso de la estructura de datos Hashtable
* Los hashtable son estructuras cuyos elementos por un lado tienen una clave
* que sirve como identificador único y por otro el resto de los datos de ese elemento.
* Para agregar un elemento usamos el método put, indicando la clave de ese elemento
* y el objeto que contiene. Para sacar un elemento usamos get, donde debemos indicar
* el campo clave.
* Por tanto las claves de los elementos de una Hashtable no pueden repetirse.
*
* @author Eugenia Pérez, Pello Altadill
*/
public class Hashtables {
public static void main (String args[])
{
Hashtable<String, Cliente> misClientes = new Hashtable<String, Cliente> ();
Cliente clienteVip = new Cliente("66666666X","Bill Gates",false);
Cliente otroCliente = new Cliente("00000042X","Asimov",false);
// Agregamos los dos clientes a la hashtable:
// primero indicamos un valor clave y luego el objeto en si
[Link]("66666666X",clienteVip);
[Link]([Link],otroCliente);
[Link]("10100100A",new Cliente("10100100A","Arale",true));
// Podemos comprobar si existe determinado elemento
// a través de su clave
if ([Link]("00000042X")) {
[Link]("00000042X").sacarDatos();
}
// Vamos a recorrer todo a través de las claves
Enumeration<String> lista = [Link]();
while([Link]()) {
String clave = [Link]();
[Link](clave).sacarDatos();
}
// Para quitar elementos usamos también la clave:
[Link]("66666666X");
// Volcamos todo a ver qué sale
[Link]("La Hashtable está así: " + [Link]());
}
}
/**
* Clase Cliente
* la usamos como objeto para manejar con un Hashtable
*/
class Cliente {
public String dni;
public String nombre;
public boolean esMujer;
Cliente (String dni, String nombre, boolean esMujer) {
[Link] = dni;
[Link] = nombre;
[Link] = esMujer;
}
/**
* sacarDatos
* Muestra los datos de una jugador
*/
public void sacarDatos () {
String sexo = (esMujer)?"Mujer":"Hombre";
[Link]("DNI: " + dni + "nombre: " + nombre+ " Sexo:" + sexo);
}
}
15.3 ArrayList
Los ArrayList son una estructura muy útil similar a Vector pero con
alguna utilidad más a la hora de interactuar con arrays de tipos primitivos.
Crear una instancia no tiene ningún misterio:
ArrayList<String> names = new ArrayList<String>();
Método Significado
.size() Cantidad de elementos que
contiene el ArrayList
.add(elemento) Añade un nuevo elemento al final
del ArrayList
.add(índice, elemento) Introduce un elemento en el índice
indicado
.get(índice) Elimina un elemento del ArrayList
que contenga ese objeto
.remove(indíce) Elimina el elemento del ArrayList
que se encuentre en esa posición
.contains(elemento) Indica si ese elemento está en el
ArrayList
.size() Cantidad de elementos que
contiene el ArrayList
.clear() Elimina todos los elementos del
Array
.toArray(type []) Convierte el ArrayList a un array
común del tipo indicado
15.4 HashSets
Los Hashsets no son más que listas en las que NO pueden repetirse los
valores. Pese al nombre no se parecen a los Hashtables y se definen
simplemente así:
HashSet<String> uniqueNames = new HashSet<String>();
Método Significado
.size() Devuelve la cantidad de
elementos del HashSet
.add(elemento) Añade un elemento en el
HashSet
.iterator() Devuelve un iterador del
HashSet
.contains(elemento) Indica si determinado elemento
existe
.remove(clave) Elimina el elemento del
Hashtable que tenga esa clave
.clear() Elimina todos los elementos del
Hashtable
15.4.1 Ejemplo
de
HashSets
/**
* App
* Clase que ejemplifica el uso de colecciones HashSet, que
* almacenan objetos de tipo Alumno (entendiendo que dos alumnos
* serán iguales cuando sus números de matrícula lo sean)
*
*/
public class App {
public static void main(String[] args) {
HashSet<Alumno> alumnos = new HashSet<Alumno>();
Alumno alumno1 = new Alumno("Mikel","12345");
Alumno alumno2 = new Alumno("Mikel","12345");
//Alumno alumno2 = alumno1;
[Link](alumno1);
[Link](alumno2);
for(Alumno alumno: alumnos){
[Link]([Link]());
}
}
}
/**
* Alumno
* Clase que representa un Alumno. Se debe prestar atención
* a los métodos sobreescritos: equals y hashCode, que permiten
* especificar la condición de igualdad entre objetos. Dos alumnos
* serán iguales cuandos sus números de matrícula también lo sean.
* @author Eugenia Pérez, Pello Altadill
*
*/
public class Alumno {
private String nombre;
private String numMatricula;
public Alumno(String nombre, String numMatricula) {
[Link] = nombre;
[Link] = numMatricula;
}
@Override
public String toString() {
return "Alumno [nombre=" + nombre + ", numMatricula=" + numMatricula + "]";
}
@Override
public boolean equals(Object obj){
if(obj instanceof Alumno){
Alumno alumno = (Alumno)obj;
if([Link]([Link])){
return true;
}
}
return false;
}
@Override
public int hashCode(){
return [Link]();
}
}
15.5 Stacks
Stack significa pila. Mediante esta colección obtenemos una estructura
que funciona como una pila. Los elementos se añaden o más apropiadamente
se apilan. El primero en meter es el último en salir. Por eso dispone de los
pecualiares y clásicos métodos para gestionar los contenidos: push y pop. Así
es como se instancia
Stack<String> stack = new Stack<String>();
Método Significado
.size() Devuelve la cantidad de elementos
del Stack
.push(elemento) Añade un elemento en el Stack
.pop() Saca y devuelve el último valor
metido en el Stack
.peek() Como pop() pero sin sacar el
elemento
.search(elemento) Busca el elemento en el Stack
.clear() Elimina todos los elementos del
Stack
/**
* clase Stacks
* Muestra el uso de la estructura de datos Stack
* Un Stack no es más que una Pila, es decir, un montón,
* es decir, una estructura LIFO:
* el último que entra es el primero que sale.
*
* De paso vemos el uso de Date y SimpleDateFormat
*
* @author Eugenia Pérez, Pello Altadill
*/
public class Stacks {
public static void main (String args[]) throws ParseException {
Tarea tareaTonta = new Tarea(new Date(),"Tarea tonta");
Tarea tareaImportante = new Tarea(new Date(),"Tarea fundamental para ya");
// Vamos a crear una fecha cualquiera para la tarea
SimpleDateFormat formatoFechas = new SimpleDateFormat();
Tarea otraTarea = new Tarea(new Date(), "Otra tarea cualquiera");
// ahora creamos la Pila y vamos metiendo elementos,
// uno encima de otro
Stack tareas = new Stack();
[Link](tareaTonta);
[Link](otraTarea);
[Link](tareaImportante);
// Metemos otra tarea creada al vuelo
[Link](new Tarea(new Date(),"Al vuelo"));
// Se premiten los elementos repetidos...
[Link](otraTarea);
// Vamos a recorrer la pila con el interfaz List
ListIterator lista = [Link]();
while ([Link]()) {
Tarea t = (Tarea)[Link]();
[Link]([Link]());
}
// Sacamos un par de elementos, podemos recogerlo.
[Link]();
Tarea saliente = (Tarea)[Link]();
[Link]();
[Link]([Link]());
// Podemos echar un ojo al primero de la pila
// sin tener que sacarlo: método peek
Tarea arriba = (Tarea)[Link]();
[Link]([Link]());
}
}
/**
* Clase Tareas
* la usamos como objeto para manejar con una pila
*/
class Tarea {
public Date fecha;
public String nombre;
private boolean finalizada;
Tarea (Date fecha, String nombre) {
[Link] = fecha;
[Link] = nombre;
[Link] = false;
}
/**
* finalizar
* Dar por finalizada la tarea
*/
public void finalizar () {
finalizada = true;
}
/**
* toString
* Devuelve una cadena con todos los datos del objeto
*/
public String toString () {
return "Fecha: " + fecha +" Nombre: " + nombre + " Finalizada: " + finalizada;
}
}
15.6 TreeMaps
La estructura tiene forma de árbol en el que los elementos se almacenan
de forma ordenada. Esto garantiza que la velocidad de operaciones de
búsqueda, adición y eliminación de un elemento se minimiza respecto a otras
estructuras. Se trata de una colección que mezcla árboles y mapas. La
estructura queda ordenada según el criterio que le demos.
TreeMap<String, Student> students = new TreeMap<String, Student>();
Método Significado
.size() Devuelve la cantidad de elementos
del TreeMap
.get(clave) Recupera un elemento del
TreeMap
.put(clave, elemento) Añade un nuevo elemento en el
TreeMap
.keySet() Retorna una estructura Set con
todas las claves
.remove(clave) Elimina un elemento del Stack
.clear() Elimina todos los elementos del
Stack
15.6.1 Ejemplo
de
TreeMaps
import [Link].*;
import [Link].*;
/**
* clase TreeMaps
* Muestra el uso de la estructura de datos TreeMap
* Un treemap no es sino una estructura de Arbol, en este
* caso ordenada por un criterio por defecto o por el que nosotros
* le digamos.
*
* @author Eugenia Pérez, Pello Altadill
*/
public class TreeMaps {
/**
* main
* Función principal
* Desde la función principal probamos un TreeMap
*/
public static void main (String args[])
{
Naipe n1, n2, n3, n4, n5, n6, n7, n8;
// Vamos a crear 8 cartas del mismo palo
n1 = new Naipe("Picas","10");
n2 = new Naipe("Picas","8");
n3 = new Naipe("Picas","K");
n4 = new Naipe("Picas","A");
n5 = new Naipe("Picas","Q");
n6 = new Naipe("Picas","5");
n7 = new Naipe("Picas","2");
n8 = new Naipe("Picas","J");
TreeMap baraja = new TreeMap();
// Y ahora las añadimos a nuestro arbol
// El criterio de orden será su valor
[Link]([Link],n1);
[Link]([Link],n2);
[Link]([Link],n3);
[Link]([Link],n4);
[Link]([Link],n5);
[Link]([Link],n6);
[Link]([Link],n7);
[Link]([Link],n8);
// Vamos a recorrer a ver cómo están. Para los TreeMaps hay
// un paso previo
Collection coleccionBaraja = [Link]();
Iterator lista = [Link]();
// TODO: sacar el objeto directamente
while ([Link]()) {
[Link]([Link]().toString());
}
}
}
/**
* Naipe
* Clase que utilizaremos para probar la TreeMaps
* Representa una carta de la baraja francesa (la del poker tron!)
*/
class Naipe {
String palo;
String simbolo;
int valor;
Naipe (String palo, String simbolo) {
[Link] = palo;
[Link] = simbolo;
[Link] = asignarValor();
}
/**
* asignarValor
* Segun la carta (A, K, 5,...) le asigna un valor numérico
* @return int valor de la carta
* Este método es MUY chapuzas:
* Buena candidata para refactorizar, o cambiar por un hashtable
*/
private int asignarValor () {
int resultado = 0;
if ([Link]("A")) {
resultado = 14;
} else if ([Link]("K")) {
resultado = 13;
} else if ([Link]("Q")) {
resultado = 12;
} else if ([Link]("J")) {
resultado = 11;
} else {
// Estamos dando por supuesto que cualquier otra cosa
// será un valor entre 10 y 2.
resultado = [Link](simbolo);
}
return resultado;
}
/**
* mostrarNaipe
* Vuelca todo
*/
public void mostrarNaipe () {
[Link](simbolo + " de " + palo + ". Valor: " + valor);
}
}
15.7 LinkedLists
Esta es una lista en la que los elementos estan doblemente conectados
entre sí. Por tanto, cada elemento sabe llegar a su elemento anterior y
posterior. La instancia no tiene ninguna peculiaridad:
LinkedList<String> names = new LinkedList<String>();
Método Significado
.size() Cantidad de elementos que contiene el
LinkedList
.add(elemento) Añade un nuevo elemento al final del
LinkedList
.add(índice, elemento) Introduce un elemento en el índice indicado
.get(índice) Elimina un elemento del LinkedList que
contenga ese objeto
.remove(indíce) Elimina el elemento del LinkedList que se
encuentre en esa posición
.contains(elemento) Indica si ese elemento está en el LinkedList
.clear() Elimina todos los elementos del LinkedList
.getFirst() Retorna el primer elemento de la LinkedList
.getLast() Retorna el último elemento de la LinkedtList
.push(elemento) Añade un elemento en la pila que representa la
LinkedList
.pop() Saca y retorna un elemento en la pila que
representa la LinkedList
15.7.1 Ejemplo de
LinkedLists
import [Link].*;
import [Link].*;
/**
* clase LinkedLists
* Muestra el uso de la estructura de datos LinkedList
* que consisten en listas de datos en las que
* SE PUEDEN REPETIR datos: además es una estructura
* de lista con doble enlace y puede recorrerse en ambos sentidos.
* También puede comportarse como pila, como cola, etc. ver API.
*
* @author Eugenia Pérez, Pello Altadill
*/
public class LinkedLists {
public static void main (String args[])
{
LinkedList misProductos = new LinkedList();
// Creamos unos productos
Producto producto1, producto2, producto3, producto4;
producto1 = new Producto("Mayonesa Hellmans","Kraft",2.56);
producto2 = new Producto("Donuts Bombón","Panrico",2.98);
producto3 = new Producto("Chopped","El pozo",0.28);
producto4 = new Producto("Preservativos XL","Fantasmex",15.5);
[Link](producto1);
[Link](producto2);
[Link](producto2);
[Link](producto2);
[Link](producto4);
[Link](producto3);
[Link](producto2);
// incluso podemos añadir a lo bruto la propia lista a sí misma
//[Link](misProductos);
((Producto)[Link]()).cambiarPrecio(66.6);
15.8 String
Tokenizer
división
de
cadenas
En ocasiones es frecuente que necesitemos dividir una cadena en partes
para guardar cada parte en un array.
15.8.1 Ejemplo
de
Tokenizer
import [Link].*;
import [Link].*;
/**
* clase DividirString
* Muestra el uso de la clase StringTokenizer, la cual permite
* dividir una cadena según un caracter y convertirla en un array:
* "esto,es,una,cadena,de,ejemplo"
* Dividiendo esa cadena por la coma sería:
* tokens[0] == "esto"
* tokens[1] == "es"
* tokens[2] == "una"
* ...
* Esto en otros lenguajes se hace con la función split
* @author Eugenia Pérez, Pello Altadill
*/
public class DividirString {
public static void main (String args[])
{
String cadena = "Corre Sombragris muéstranos lo que es la premura";
StringTokenizer tokens = new StringTokenizer(cadena);
// ahora recorremos los tokens
while ([Link]()) {
[Link]("palabra: " + [Link]());
}
// Otra cadena, cuyas palabras están divididas por ":"
String otraCadena = "Juan:Martinez:Irujo:24-10-1982:Ibero";
tokens = new StringTokenizer(otraCadena,":");
[Link]("\n Ahora salen: " + [Link]());
// ahora recorremos los tokens con un for.
// OJO! nextToken() altera el valor de countTokens, por tanto
// no se puede usar como condición del for
for (int i = 0; [Link]();i++) {
[Link]("palabra " + i + ">" + [Link]());
}
}
}
15.9 StringBuilder
Una clase muy útil para poder concatenar un String de manera eficiente.
Al utilizar StringBuilder estaremos manejando una variable que NO es
inmutable. Cuando concatenamos contenido en un StringBuilder, este sí se
modifica a sí mismo:
StringBuilder longMessage = new StringBuilder();
//...
15.10 Fechas
y horas
En el ejemplo de código de Stack se veía el uso de los tipos fecha tal y
como se hacía en versiones Java anteriores a la 8. Desde la versión 8 se han
creado nuevas clases que facilitan la gestión de fechas y horas, las cuales se
encuentran en:
import [Link].*;
15.10.1 Aritmética
de fechas
También podemos sumar tiempo a LocalDate, LocalTime y
LocalDateTime. Esto es muy útil para no tener que calcular a mano sumas
aritméticas con fechas. Por ejemplo:
LocalDate currentDate = [Link](2015, 6, 20);
[Link](9); // 2015-6-29
Podemos sumar tanto días como semanas, meses, etc... Y por supuesto,
también podemos restar. Los nombres de métodos resultan bastante
evidentes. Los que implican días no se aplican en tipos LocalTime y
obviamente los que implican horas, minutos,... no se aplican a LocalDate. En
acmbio todos estos métodos se pueden aplicar a LocalDateTime
plusYears() / minusYears()
plusMonths() / minusMonths()
plusWeeks() / minusWeeks()
plusDays() / minusDays()
plusHours() / minusHours()
plusMinutes() / minusMinutes()
plusSeconds() / minusSeconds()
plusNanos() / minusNanos()
15.10.2 Periodos
Para gestión de periodos de tiempo, podemos usar la clase Period. Esta
clase nos permite definir un periodo en meses, días, semanas, que luego
podemos aplicar a clases de tipo fecha. Podemos definir un periodo así, y
luego sumarlo a una fecha:
Period unaSemana = [Link](1);
[Link](unaSemana);
15.10.3 Formateo
de fechas
De una instancia de tipo fecha podemos extraer el dato que nos interese
de forma individual:
LocalDate today = [Link]()
[Link]()
[Link]()
[Link]()
Patrones
Los patrones para generar fechas son los siguientes:
yyyy: representa el año.
yy para dos dígitos: 15
yyyy para cuatro: 2015
MMMM: representa el mes:
MMMM el nombre completo: June
MMM el mes en tres letras: Jun
MM el mes en dos cifras con 0 delante si es
preciso: 06
M el mes con número y sin cero por delante: 6
dd: representa el día:
dd el día con 0 delante: 03
d el día sin 0 delante: 3
hh: representa la hora:
hh la hora con 0 delante: 07
h la hora sin 0 delante: 7
mm: representa el minuto
15.10.4 Parseo de
fechas
A veces es útil poder pasar de un String a un tipo fecha de forma
automática. Esto se puede conseguir aplicando patrones y el método parse en
las clases de tipo fecha:
DateTimeFormatter customFormat = [Link](“dd MM yyyy”);
LocalDate localDate = [Link](“29 06 2015”, customFormat);
15.11 Ejercicios
del tema
API
Colecciones
Para los proyectos, utiliza el package: [Link]
15.11.1 Ejercicio
1
Crea un proyecto java que contenga tres clases, y antes de empezar a
programar, dibuja el diagrama de clases UML
1. Clase Player, que contiene los atributos String name, String
position e int number. También tiene un constructor con todos
esos parámetros y un método toString generado automáticamente.
2. Clase Team, que contiene los atributos String name, int
foundationYear, double budget y un Vector de la clase Player.
Debe tener un constructor con los atributos name, foundationYear
y Budget, sus get/set, un método toString y otros dos métodos
públicos:
2.0.1 public void
addPlayer(Player
player) para
añadir
jugadores al
vector.
2.0.2 public String
showPlayers(),
para
devolver una
cadena con
todos los
datos de los
jugadores
15.11.2 Ejercicio
2
Crea un proyecto java que contenga tres clases, y antes de empezar a
programar, dibuja el diagrama de clases UML
1. Clase Card, que contiene los atributos String name, String suit e
int value. También tiene un constructor con todos esos
parámetros, métodos get/set y un método toString generado
automáticamente.
2. Clase Deck, que contiene un atributo que es un Vector de la clase
Card. La clase baraja tiene un constructor vacío que llama a un
método privado llamado loadDeck(), el cual mete las 52 cartas
(baraja francesa) en el Vector que tiene esta clase. Para ello,
podrías crear dos arrays, uno de palos de la baraja, y otro de
valores, e ir combinándolos. Trata de crear un método shuffle()
que efectivamente baraje las cartas en ese vector, y otro,
showCards(), que las muestre todas.
3. Clase Main, contiene el método main. En él se debe crear una
instancia de la clase Deck, mostrar las cartas inicialmente,
barajarlas y volver a mostrarlas.
Ejercicios de Hashtables
NOTA:
Para los proyectos, utiliza el package: [Link]
15.11.3 Ejercicio
3
Crea un proyecto java que contenga tres clases, y antes de empezar a
programar como una loca dibuja el diagrama de clases UML
1. Clase Student, que contiene los atributos String idCard, String
name y String course, un constructor con esos parámetros y un
método toString(), los puedes generar automáticamente.
2. Clase Group, que contiene un atributo String name y un
Hashtable con los tipos String y Student para clave y valor
respectivamente. El String será para el idCard. Hashtable<String,
Student>. Crea además set/get para el atributo name y el
Hashtable.
3. Clase Main, contiene el método main. En él se debe crear una
instancia de la clase Group, cargarlo con tres alumnos y luego
mostrarlos en un bucle.
15.11.4 Ejercicio
4
Debes crear un programa para gestionar un Parking de coches. Para eso
debes tener las siguientes clases:
1. Car
Atributos
String plate
String model
String color
Métodos: Constructor con todos los campos, set/get y
toString()
2. Parking
Atributos
String name
int capacity
Hashtable<String, Car> places
Métodos
Constructor con los campos name y capacity.
añade un coche al Parking,
public void addCar(Car c):
debe comprobar que todavía hay sitio.
public Car searchCar(String plate): busca
una matrícula y
devuelve el objeto Car correspondiente
public void removeCar(String plate): busca una matrícula
y la elimina del Hashtable
public String showAll(): muestra todos los coches que
hay en el parking.
3. Main
Solo tiene un método main, donde se instancia el
parking, se crean varios coches y se meten en el
parking. Luego se prueba a buscar un coche, se
muestran todos los coches del parking, se elimina un
coche del parking y se vuelven a mostrar todos.
Crea el diagrama de clases UML.
15.11.5 Ejercicio
5
Debes crear un programa para gestionar una lista de contactos de
clientes. El programa es muy similar al anterior:
1. Customer
Atributos
String dni
String name
String email
Métodos: Constructor con todos los campos, set/get y
toString()
2. Contacts
Atributos
String owner
Hashtable<String, Customer> customers
Métodos
Constructor con el campo owner.
public void addCustomer(Customer c): añade un
Customer a la lista de contactos
public Customer search Customer (String dni): busca
un dni
y devuelve el objeto Customer correspondiente
public void removeCustomer(String dni): busca un dni y la
elimina del Hashtable
muestra todos los Customer
public String showAll():
que hay en contactos.
3. Main
Solo tiene un método main, donde se instancia
Contacts, se crean varios Customer y se meten en la
lista de contactos. Luego se prueba a buscar un
Customer por su dni, se muestran todos los Customer
de la lista, se elimina un Customer y se vuelven a
mostrar todos.
Crea el diagrama de clases UML.
15.11.6 Ejercicio
6
Debes crear un programa para gestionar una flota estelar en la que cada
nave es comandada por un piloto espacial. Estas son las clases necesarias.
1. Commander
Atributos
String name
int experience
Métodos: Constructor con todos los campos, set/get y
toString()
2. Ship
Atributos
int firepower
int shield
boolean hyperdrive
Commander commander
Métodos
Constructor con todos los campos, set/get y
toString()
3. Fleet
Atributos
String fleetName
Hashtable<Commander,Ship> ships
Métodos
Un constructor con el parámetro fleetName
public void addShip(Ship ship): añade
una nave. Debes
usar su Commander como clave.
public Ship searchShip (Commander commander): busca un
commander y devuelve el objeto Ship
correspondiente
public void removeShip(Commander commander): busca un
comander y lo elimina del Hashtable
public String showAll(): muestra todos las naves que
hay en la flota.
4. Main
Solo tiene un método main, debes hacer lo siguiente:
i. Crear una instancia de Fleet.
ii. Crear tres instancias de Commander
iii. Crear tres instancias de Ship asignándoles un
Commander a cada
iv. Introducir las naves en la Fleet.
v. Mostrar todas las naves.
vi. Eliminar una nave
vii. Mostrar todas las naves.
15.11.8 Ejercicio
8
Vamos a ampliar el ejercicio anterior con un sistema de préstamos.
Incluye/amplia las siguientes clases:
1. User
Atributos
String name
String libraryCard
Métodos
Método constructor con parámetros.
Método toString.
El get de LibraryCard.
1. Checkout
Atributos
Hashtable<String, Book> checkouts: representa
los préstamos de libros a cada usuario. La clave
será el libraryCard de un usuario, y almacenará
el libro que tiene prestado.
Métodos
El método constructor donde se crea la
colección del Hashtable.
añade a la
public void add(User user, Book book):
colección un préstamo, es decir un libro para un
usuario en concreto.
retorna el
public Book getCheckoutByUser(User user):
libro que tuviera prestado dicho usuario.
2. Main
Sobre la clase main del ejercicio anterior, crea tres
objetos de tipo User y una instancia de tipo préstamos
(Checkout).
Debes asignar un libro a un usuario en la colección de
préstamos, pero esta operación solo la harás en caso de
que el libro exista en la librería. Fíjate en los métodos
que tienes implementados para estas operaciones
(searchBook).
Obtén el libro para el usuario del préstamo anterior
(getCheckoutByUser) y muestra su toString.
15.11.9 Ejercicio
9
Debes crear un ejercicio que gestione una carrera de coches. Incluye las
siguientes clases:
1. Car
Atributos
String plate: la matrícula del coche
String model: la marca y modelo de coche
float price: el precio del coche
Métodos
Un método constructor con todos los atributos
como parámetro y el método toString()
public boolean equals(Object obj): comprueba
si dos
coches son iguales en función de su matrícula.
public int hashCode(): devuelve el hashCode del Car.
public int run(): devuelve un número aleatorio
de entre 0 a 5.
2. Country
Atributos
String code: el código de país (2 caracteres).
String name: nombre largo del país.
String capital: capital del país.
Métodos
Un método constructor con todos los atributos
como parámetro y el método toString()
3. Team
Atributos
String name: nombre del equipo.
Country country: país del equipo.
Vector<Integer> driverNumbers: número de
coche de los pilotos.
HashSet<Car> cars: conjunto de coches del
equipo.
Métodos
Un método constructor con todos los atributos
como parámetro y que llamará al métodos
generateDriverNumbers.
private void generateDriverNumbers(): genera
varios dorsales o números aleatorios (3 por
ejemplo) y los mete en el vector driverNumbers.
public void addCar(Car car): añade un nuevo
coche al HashSet.
public String showCars(): muestra todos los
coches del equipo.
public Country getCountry(): devuelve el país
del equipo.
public HashSet<Car> getCars (): devuelve el
HashSet de coches.
El método toString().
4. Race
Atributos
String description: descripción de la carrera
Hashtable<Team, Integer> teams: Hashtable
con todos los equipos y la distancia total que
recorren todos sus coches.
Métodos
Un método constructor con el atributo
description como parámetro.
public void run(): un método que por cada
equipo, obtiene cada uno de sus coches y lo hace
correr (run()), devolviendo una distancia. Debes
sumar la distancia de todos los coches del
equipo y asignársela en el Hashtable.
añade un equipo en
public void addTeams(Team team):
el Hashtable y le asigna 0 en la distancia
recorrida inicialmente.
public String getDescription(): devuelve la descripción.
retorna toda la información
public String showTeams():
de los equipos, la distancia recorrida, y los
datos de sus coches.
5. Main
Solo tiene un método main, que hace lo siguiente:
i. Crea seis coches.
ii. Crea dos países.
iii. Crea dos equipos.
iv. Añade los tres primeros coches al equipo 1, y los
otros tres al equipo 2.
v. Crea una carrera y añádele los dos equipos.
vi. Llama al método run() de la carrera.
vii. Muestra la información de equipos/coches
(showTeams).
16 ACCESO
A
DATOS
CON
SQLITE
Y JAVA
SQLite es un motor de BD muy ligero, multiplataforma y que no
necesita configuración. SQLite puede descargarse desde [Link] para
administrar la base de datos podemos utilizar Sqliteman o también
phpLiteAdmin que es similar a phpmyadmin para Mysql.
Para gestionar la BD utilizaremos una interfaz gráfica llamada
sqliteman:
[Link]
16.1 Proyecto
de
ejemplo
Con la anterior herramienta, crearemos una BD llamada
[Link]. A continuación, creamos una tabla EMPLEADO con los
siguientes campos:
Vamos a añadir manualmente datos a través de una INSERT:
INSERT INTO empleado VALUES (1,"Carlos","Informática");
INSERT INTO empleado VALUES (2,"Raquel","RRHH");
INSERT INTO empleado VALUES (1,"Sara","Direccion");
PreparedStatement stat =
[Link](1, ++contador);
[Link](2, "Iker Iturri");
[Link]();
16.2 Ejercicios
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
10.1.
10.2.
16.2.1 Ejercicio
1
A continuación, te detalle el proyecto a realizar, que tendremos que
mantener en el repositorio [Link].Ej1 en nuestra cuenta de Bitbucket.
Vamos a gestionar la conexión a una BD de datos llamada [Link]
desde un programa de consola Java. Empezaremos describiendo la tabla de la
base de datos:
Crea un proyecto llamado ERP dentro del paquete
[Link].
Recuerda almacenar la BD en la raíz.
Importar el JAR conector de SQLite.
El proyecto debe contener las siguientes clases:
Los métodos subrayados son estáticos. Esto quiere decir que los
invocarás directamente a través del nombre de su clase, por ejemplo,
[Link]().
En el main de la clase App crearás una estructura de menú que permita
realizar las siguientes operaciones:
1. Insertar cliente
2. Mostrar todos los clientes
3. Actualizar cliente
4. Borrar cliente
5. Buscar por nombre
6. Salir
Antes de nada, deberás establecer la conexión, llamando al método
conectar. Se describen las opciones a continuación:
Insertar cliente: se debe solicitar al usuario el nombre y la
dirección del cliente, crear un objeto de este tipo y llamar al
método insertarCliente.
Mostrar todos los clientes: simplemente muestra por
consola los datos de todos los clientes almacenados.
Actualizar cliente: solicita al usuario el id, el nombre y la
dirección del usuario que quiere modificar. Dentro del
método actualizarCliente, modificaremos el nuevo nombre
y la dirección para el id proporcionado.
Borrar cliente: solicita al usuario el id del cliente a borrar y
procede a eliminarlo.
Buscar por nombre: solicita al usuario el nombre del
cliente a buscar y muestra los datos relativos al cliente
encontrado.
Salir: en esta opción deberás cerrar la conexión.
1
9.1
9.2
9.2.1
1.1
1.2
1.2.1
16.2.2 Ejercicio
2
Modifica el ejercicio anterior para que la clave primaria id sea
autoincremental. Tendrás que modificar la columna de la tabla y hacer los
cambios pertinentes en el código.
17 EXPRESIONES
LAMBDA
Desde la version Java 8 se ha introducido la posiblidad de crear
funciones lambda en Java. Este tipo de funciones son más propias de la
programación funcional; en este tipo de programación en lugar de indicar
como se resuelve un problema, se describe o declara qué es lo que hace.
También se les conoce en otros ámbitos como funciones anónimas o
closures.
18 THREADS:
VARIAS
TAREAS A
LA VEZ
Los Threads o hilos permiten la ejecución de varios códigos de manera
simultánea. En lenguaje Java disponemos de dos maneras de crear este tipo
de hilos:
1. Extendiendo la clase Thread
2. Implementando el interface Runnable
Veamos un ejemplo sencillo:
10
11
12
13
14
15
18.1 Ejemplo
de hilos
import [Link].*;
import [Link].*;
/**
* clase PersonajeHilo
* Muestra el uso de hilos o threads de ejecución, lo cual permite
* que varios códigos se ejecuten en paralelo.
*
* En este caso se crea una clase que implementa la interfaz Runnable.
* Eso la convierte en un hilo. Lo único que hace es sacar un mensaje
* por la consola.
*
* @author Eugenia Pérez, Pello Altadill
*/
public class PersonajeHilo implements Runnable{
// nombre del hilo
private String nombre;
// array de frases que dirá el hilo
// no se puede modificar (eso es el final) y aunque
// haya muchos objetos de la clase Hilos
// solo habrá un único array frases compartido
// por todos (eso es el static)
private static final String[] frases = {"Hola", "Adios", "Qué tal", "Bien...", "Buh"};
/**
* PersonajeHilo
* Constructor
* @param String nombre
*/
PersonajeHilo (String nombre) {
[Link] = nombre;
}
// Al implementar Runnable, esta clase debe definir una serie
// de métodos, entre otros run
/**
* run
* En un bucle infinito, saca un mensaje aleatorio por pantalla
* y no hace nada en un tiempo aleatorio de 0 a 5 sg.
* @throws InterruptedException
*/
public void run ()
{
Random rnd = new Random();
try {
while (true) {
sacarMensaje();
[Link]().sleep([Link](5000));
}
} catch (InterruptedException iex) {
[Link]("Error en la ejecución del hilo");
}
}
/**
* sacarMensaje
* Saca un mensaje aleatorio por pantalla
*/
private void sacarMensaje () {
Random rnd = new Random();
[Link](nombre + "> " + frases[[Link](4)]);
}
public static void main (String args[]) {
PersonajeHilo personaje1, personaje2, personaje3, personaje4;
personaje1 = new PersonajeHilo("Cersei");
Thread hilo1 = new Thread(personaje1);
personaje2 = new PersonajeHilo("Tyrion");
Thread hilo2 = new Thread(personaje2);
personaje3 = new PersonajeHilo("Jaime");
Thread hilo3 = new Thread(personaje3);
personaje4 = new PersonajeHilo("Tywin");
Thread hilo4 = new Thread(personaje4);
// Ponemos en marcha los hilos
[Link]();
[Link]();
[Link]();
[Link]();
}
}
19 JAVA CODE
CONVENTIONS
19.1 Introducción
Al igual que la mayoría de lenguajes de programación Java también
cuenta con unas convenciones que nos dictan las normas básicas que se
recomiendan seguir a la hora de escribir código. Pese a que el código puede
funcionar igual, aunque se escriba de distintas maneras, sin entrar en lo
adecuado de la solución conviene seguir una serie de reglas básicas a la hora
de programar.
En cualquier otro lenguaje encontrarás una guía o documento oficial
que describe esas convenciones, o cuando menos, una guía generada por la
comunidad de desarrolladores.
19.2 Motivación
El software tiende a pasar por muchas manos, incluso dentro de la
misma empresa. Y generalmente no es algo que se quede estático, precisa de
un desarrollo continuo y un mantenimiento. Las convenciones son una guía
de estilo que más allá de lo estético se justifican por lo siguiente:
El 80% del coste de la vida del código lo acapara el
mantenimiento del mismo.
Es poco frecuente que cualquier código sea mantenido a lo
largo de su vida por un mismo autor.
Las convenciones mejoran la legibilidad del software,
permitiendo a los ingenieros entender el nuevo código de
forma rápida y completa.
19.3 Las
convenciones
El documento de convenciones para Java está disponible en la web de
Oracle y existen traducciones al castellano. Algunas de ellas pueden entrar en
conflicto con las prácticas de código limpio, de carácter general para todos
los lenguajes y que se verán más adelante. Estas convenciones se refieren
únicamente a prácticas que el fabricante recomienda seguir con el lenguaje
Java. A falta de una guía de estilos concreta o en caso de no seguir ninguna
convención, las convenciones oficiales de cualquier lenguaje son una
referencia mínima que se debería seguir.
Los IDE como Eclipse disponen de teclas como Ctrl-Shift-F que
automáticamente formatean el código siguiendo estas convenciones y las de
otros lenguajes. Consulta sus guías o la disponibilidad de plugins incluso para
los editores ligeros.
. A continuación, damos un resumen de las convenciones oficiales de
Java.
19.3.1 Ficheros
Una clase = Un fichero
Pese a que Java es flexible en el número de clases que pueden meterse en
un fichero, es algo que se debe evitar ya que dificulta enormemente la
localización de código.
Orden
Los ficheros de código deben seguir la siguiente secuencia:
1. Comentarios iniciales
2. Nombre de paquete
3. Sección de imports
4. Comentarios de clase
5. Declaración de clase
6. Variables estáticas en orden public, protected, private
7. Variables de instancia en orden public, protected, private
8. Constructores, desde el más general al más concreto
9. Métodos por funcionalidad
/*
* Classname
*
* Version information
*
* Date
*
* Copyright notice
*/
package [Link];
import [Link].*;
/**
* Classname
*/
class ClassName {
public static int COUNT = 0;
private static int TYPE = 4;
public String name = “”;
protected int theBeast = 666;
public ClassName () {
}
...
}
19.3.2 Código
fuente
Las líneas no deben sobrepasar los 80 caracteres. Para evitarlo se
puede:
Meter un salto de línea tras una coma
Meter un salto de línea tras un operador
Son preferibles los saltos de alto nivel antes que los de bajo
nivel
Alinear la nueva línea con el inicio de la línea anterior
Si las reglas anteriores llevan a la confusión o a mucho
código apretado contra el margen derecho, se pueden meter
8 espacios:
var = someMethod1(longExpression1,
someMethod2(longExpression2,
longExpression3));
if ((condition1 && condition2)
|| (condition3 && condition4)
||!(condition5 && condition6)) {
doSomethingAboutIt();
}
19.3.3 Formator
de
comentarios
1. javadoc /** … */
2. Código /* .. */ or //
3. /* .. */ comentarios de una línea
4. // comentarios de una línea o para deshabilitar código
5. Los comentarios de bloque dentro de un método deben estar
indentados al mismo nivel que el código que describen
6. Seguir el formato Javadoc, si es que se va a generar
documentación
if (a == 2) {
return TRUE; /* special case */
} else {
return isPrime(a); /* works only for odd a */
}
19.3.4 Declaraciones
de variables
1. Una declaración por línea
2. Deben inicializarse las variables locales
3. Declarar inmediatamente, tras la llave {. No esperar a
declarar una variable hasta su primer uso
4. Evitar declaraciones locales que ocultan declaraciones a un
nivel más alto (tienen el mismo nombre)
int level; // indentation level
int size; // size of table
Object currentEntry; // currently selected ...
19.3.5 Declaración
de clases y
métodos
1. No debe haber espacios entre el nombre del método y el
paréntesis siguiente.
2. La llave de apertura { comienza al final de la mismas línea
donde se declara la clase o el métod
3. La llave de cierre } se pone al principio de una nueva
línea y debe estar al mismo nivel que su declaración de
apertura, excepto cuando sea una declaración nula; en ese
caso, el cierre } debe aparecer inmediatamente tras el {
class Sample extends Object {
int ivar1;
int ivar2;
Sample () {}
Sample(int i, int j) {
ivar1 = i;
ivar2 = j;
}
int emptyMethod() {}
...
}
19.3.6 Sentencias
Dentro del código de los métodos, debemos procurar seguir las
siguientes reglas:
1. ¡Una sentencia por línea!
2. Sentencias compuestas :
a. Las sentencias entre llaves deberían indentarse un
nivel más que una sentencia compuesta
b. La llave de apertura debería estar al final de la línea
de la sentencia compuesta; la llave de cierre debería
iniciarse en una nueva línea al mismo nivel que el
inicio de la sentencia compuesta
if (condition) {
statements;
}
if (condition) {
statements;
} else {
statements;
}
7. do while
do {
statements;
} while (condition);
19.3.7 Espacios
en
blanco
1. Una única línea en blanco en las siguientes circunstancias:
a. Entre métodos
b. Entre las variables locales de un método y la primera
sentencia
c. Antes de un bloque o un comentario de una única
línea
2. Dos líneas en blanco en las siguientes circunstancias:
a. Entre dos secciones de un fichero de código
b. Entre la definición de una clase y de un interface
3. Una palabra clave seguida de un paréntesis debe estar
separada por un espacio en blanco
while (true) {
...
}
19.3.8 Convenciones
de nombrado
1. Paquetes : letras del conjunto ASCII en minúsculas y
utilizando dominios de alto nivel invertidos. A partir de ahí,
pueden usarse convenciones que se usen dentro de la
organización, pero manteniendo los caracteres simples y las
minúsculas
[Link]
[Link].v2
[Link]
19.3.9 Buenas
prácticas
1. Variables de instancia o de clase: deben encapsularse
siempre. No las hagas públicas salvo que tengas una buena
razón para ello.
2. Como referirte a variables o métodos de una clase:
classMethod(); //OK
[Link](); //OK
[Link](); //¡EVITAR!
3. Constantes/Magic Numbers: los números constantes no
deben aparecer directamente en un programa y se deben
sustituir por una variable constante. Esto es una conocida
refactorización que se verá más adelante. La excepción a
esta regla son los números: -1, 0 y 1.
4. Asignación a variables:
[Link] = [Link] = 'c'; // AVOID!
if (c++ = d++) { // AVOID! (Java ya no lo permite)
d = (a = b + c) + r; // AVOID!
De la misma forma:
if (condition) {
return x;
}
return y;
20.1 Palabras
reservadas
Java
Estas son palabras fundamentales en el lenguaje que NO pueden usarse
como identificadores de variables, parámetros, etc.
abstract assert boolean break byte
20.2 Herramientas
del JDK
El JDK o kit de desarrollo Java se usa de forma independiente o junto con
un IDE. La mayoría pasan desapercibidas al usar un IDE ya que este las
usa de manera implícita. En cualquier caso, trae una serie de herramientas
básicas que conviene entender y entre las que vamos a destacar las
siguientes:
20.2.1 Compilador:
javac
Mediante javac podemos compilar el código fuente para convertirlo en
bytecode. El bytecode es un código que está listo para ejecutarse en cualquier
máquina virtual de java (jvm), en cualquier sistema que se encuentre:
C:\> javac [Link]
Para preparar el fichero para su depuración mejor hacer:
C:\> javac –g [Link]
Si necesitamos saber que estamos usando algo desfasado
C:\> javac –deprecated [Link]
Si necesitamos hacer referencia a alguna librería:
C:\> javac –classpath ruta [Link]
Para asegurarnos de la versión de java que tenemos
C:\> javac -version
Y para sacar la ayuda:
C:\> javac -help
Si no disponemos del compilador javac pero sí de java, puede que lo
que tengamos instalado es el JRE y no el JDK. Para poder programar es
preciso el JDK ya que con el JRE solamente no tenemos más que la JVM
donde solo podremos ejecutar programas, pero no compilarlos.
20.2.2 Máquina
virtual,
ejecutador:
java
Mediante el comando java podemos ejecutar los programas que hemos
compilado. Al ejecutar debemos, en su opción más básica, pasarle el nombre
del fichero class que queremos ejecutar. NO DEBEMOS INCLUIR la
extensión .class
C:\> java MiClase
También podemos ejecutar aplicaciones que están empaquetadas dentro
de un fichero comprimido jar.
C:\> java –jar [Link]
Para asegurarnos de la versión de java que tenemos
C:\> java -version
Y para sacar la ayuda:
C:\> java -help
20.2.3 Generador de
documentación:
javadoc
Javadoc es una potente herramienta que nos permite generar de forma
automática la documentación o API de nuestro código. Es un programa que
investiga el código fuente y saca los atributos y métodos de cada clase y los
presenta en un informe en HTML.
NOTA IMPORTANTE: en general, no conviene usar este tipo de
comentarios a menos que tu código sea un API público, o sea requerido por el
proyecto en el que estás implicado.
Para poder sacarle jugo a javadoc es imprescindible comentar el
código de la forma adecuada usando ciertas palabras clave en los
comentarios que se espera javadoc:
@version: para indicar una versión del programa
@author: el autor del código
@param: en un método, para indicar un parámetro
@return: en un método, para indicar el tipo de retorno
@throw: en un método, para indicar los tipos de
excepciones que pueden salir
Además de eso dentro de los comentarios podemos meter etiquetas
HTML básicas para poder tener texto en negrita, cursiva, etc... Además de
otras notaciones para poner referencias a otras clases.
<b>Esto saldría en negrita</b>
<i>Esto sacaría el texto en cursiva</i>
{@link OtraClase}: genera un enlace a la documentación de
otra clase
Algunos ejemplos de uso de javadoc serían:
javadoc *.java: genera la documentación en el propio
directorio.
javadoc --keywords --linksource *.java: además de lo
anterior incluye metainformación en los informes y genera
el código java en HTML, útil para publicarlo en la web.
javadoc --locale es_ES --private *.java: usa el formato
es_ES para formato de fechas, números, etc… y prívate
hace que también se documenten atributos y métodos
privados. Por defecto se generan los protected, cosa lógica
cuando se documenta un API para terceros.
20.2.4 Empaquetador
de
aplicaciones:
jar
Mediante el comando jar podemos unir todos los ficheros de una
aplicación e incluirlos en un único fichero. De esta forma facilitamos su
logística a la hora de instalarlo, moverlo por la web, firmarlo digitalmente,
etc…
Jar empaqueta y además comprime los contenidos. De forma
automáticamente puede incluir un fichero especial llamado META-
INF/[Link] que incluye la información relativa a los ficheros de la
aplicación. Vamos a ver los comandos más habituales:
1. jar cf [Link] fichero(s): generaun nuevo fichero jar ([Link])
con todos los ficheros y directorios que se le digan.
2. jar tf [Link]: muestra el contenido de un fichero jar.
3. jar xf [Link]:extrae TODOS los contenidos de un fichero jar
4. jar xf [Link] fichero(s): extrae los ficheros especificados de un
fichero jar.
5. java –jar [Link]: ejecuta una aplicación empaquete en un jar. Es
imprescindible que en la aplicación exista una clase con el
método main.
6. jar uf [Link] fichero(s): actualiza un fichero jar para incluir nuevo
fichero(s).
10
11
12
12
13
14
15
16
17
18
19
19.1
19.2
Operador Símbolo
/ Ejemplos
Otros unarios +, -, !
Mult/División/Módulo *, /, %
Suma/Resta +, -
Igual/Distinto ==, !=
Lógicos &, ^, |
21 BIBLIOGRAFÍA:
Tutoriales oficiales Java. Oracle/Sun: [Link]
[Link]
OCA Sutdy Guide, Sybex
Thinking in Java
Java in a Nutshell, Oreilly
22 PROGRAMAS
MENCIONADOS/
ÚTILES
JDK 1.8 [Link]
Apache ANT: integración automatizada
jUnit: pruebas unitarias para java
DiA: diagramas UML
MS Visio: toda clase de diagramas (red, uml, etc…)
notepad++: editor de código sencillo.
Sublime: editor de código sencillo, pero con muchas
utilidades.
Atom: editor de código sencillo, pero con muchas
utilidades.
Eclipse: completo IDE para java y otros lenguajes
IntellijIDEA: editor comercial
Plugins: subversive para Subversion
Plugin eGIT para GIT
Netbeans: IDE “oficial” para java con soporte para otros
lenguajes
IntelliJidea: un IDE profesional muy potente que dispone de una
edición gratuíta. Es el IDE en el que se basa Android Studio como plataforma
de desarrollo oficial para aplicaciones Android.
Capítulo 3
Test Unitarios
1 INTRODUCCIÓN
En el proceso de desarrollo de software es deseable que se lleven a cabo
pruebas de su funcionamiento de forma regular. Existen distintos tipos de
comprobaciones que se pueden hacer y en este caso nos ocuparemos de las
pruebas unitarias.
Las pruebas unitarias o Unit Testing es un método de comprobación de
programas en el que pequeñas unidades de software, generalmente métodos,
son testeados para verificar que ofrecen el resultado esperado. En la práctica
las pruebas consisten en programar los test que deben comprobar partes de un
software. En la programación orientada a objetos, lo habitual es que una clase
tenga a su vez otra clase que compruebe cada uno de sus métodos, por lo
general aquellos que son públicos.
En las métodologías ágiles las pruebas unitarias son una pieza
fundamental del proceso de desarrollo, ante todo para asegurar que los
cambios que se introducen en el software no alteran el comportamiento
esperado del mismo. En este tipo de métodologías se programa desde el
primer momento y se refactoriza el código de manera continua, así que
resulta imprescindible tener controlado el software mediante un conjunto de
pruebas, que además nos permite automatizar esa comprobación.
1.2 jUnit
Prácticamente todos los lenguajes de programación tienen algún
mecanismo, librería o similares para llevar a cabo test unitarios. En Java el
framework de referencia es jUnit, aunque no es el único y en ocasiones puede
funcionar conjuntamente con otros frameworks de testeo (para Mocks) o
puede ser extendido como hace Spring.
jUnit viene de serie en entornos como Eclipse, lo cual facilita mucho su
aplicación. Y lo que es mejor, está presente en ciclos de vida de herramientas
de gestión de proyectos como Maven (también integrado con Eclipse), con lo
que no hay dificultades para su uso.
Ahora mismo estamos en la versión 4 de jUnit, la cual tiene diferencias
respecto a las anteriores. En el código de los ejemplos se utilizará jUnit a
través de Maven con la versión 4.11
2 EJEMPLO SIMPLE
Veamos un ejemplo simple de testeo. Supongamos que hemos
desarrollado una clase
package [Link].ju00intro;
/**
* Simple class with a method to say hello
* @author Eugenia Pérez, Pello Altadill
*/
public class Hello {
public String sayHello () {
return "hello";
}
}
Esto no crea una clase de testeo fija, simplemente crea un esqueleto que luego
podemos modificar y al que le podremos añadir, quitar, etc.
Tras eso viene un paso final, que no resulta imprescindible si el proyecto es
de tipo Maven, pero que podemos utilizar por si queremos ejecutar los tests al
margen de Maven.
El asistente de Eclipse incluirá esta librería al proyecto para los tests. En
realidad, siendo un proyecto Maven no haría falta, pero esto nos permite
ejecutar los test al margen de Maven.
De todas maneras, a la hora de crear la clase de testeo, si se trata de un
proyecto Maven y queremos que los test se ejecuten de forma automática al
aplicar ejecuciones Maven, debemos meter esa clase en la carpeta que Maven
establece para los test, es decir, src/test/java:
Para eso no tenemos más que mover el fichero:
Si al crear la clase de Test se mete donde no debe la movemos a esa
carpeta arrastrándola.
3.1 Programando el test
Empezamos a programar el test. Lo primero sería definir un atributo de
la clase que vamos a testear e instanciarlo en el método setUp.
De esa manera, tendremos una instancia lista para hacer los tests en
todos los métodos.
También podríamos crear una instancia en cada método de testeo, no
hay ningún problema en hacerlo de esta manera. La elección depende de
cómo se quiera testear una instancia.
Se abrirá un panel de jUnit, con una línea por cada método de testeo. El
color verde nos da buenas vibraciones. Nos indica además el número de tests
ejecutados, el tiempo que ha necesitado cada uno y si están correctos o no. Si
no fuera así, veríamos colores rojos o azules.
public EurosConversor() {
}
public double euros2Pesetas (double euros) {
return euros * EUROS_PESETAS_CHANGE;
}
public double pesetas2Euros (double pesetas) {
return pesetas / EUROS_PESETAS_CHANGE;
}
}
[Link]
Ahora en el test unitario, vemos como en el assertEquals debemos
añadir un parámetro final con la precisión que requerimos.
package [Link].ju01testdecimal;
import static [Link].*;
import [Link];
/**
* Test class for EurosConversor
* @author Eugenia Pérez, Pello Altadill
*/
public class EurosConversorTest {
@Test
public void testEuros2Pesetas() {
EurosConversor target = new EurosConversor();
double expected = 998.316d;
double actual = target.euros2Pesetas(6);
assertEquals("Euros 2 pesetas conversion", expected, actual, 0.01);
}
@Test
public void testPesetas2Euros() {
EurosConversor target = new EurosConversor();
double expected = 6.01d;
double actual = target.pesetas2Euros(1000);
/**
* sets autonomy value
* if passed value is less than 0 we set a default value
* @param double nuevaAutonomia
*/
public void setAutonomy (double autonomy) {
if (autonomy <= 0) {
[Link] = 42.0;
} else {
[Link] = autonomy;
}
}
[Link]
La clase de testeo aplica los distintos tipos de tests explicados
anteriormente.
package [Link].ju02asserttypes;
import static [Link].*;
import [Link];
/**
* TestRobot test class for Robots, show the use of different assert methods
* @author Eugenia Pérez, Pello Altadill
*/
public class TestRobot {
Robot r1;
Robot r2;
Robot r3;
Robot r4;
/**
* TestRobot We init some values for testint purposes. jUnit also offers
* setUp/tearDown methods for this
*/
public TestRobot() {
r1 = null;
r2 = new Robot("R2D2");
r3 = r2;
r4 = new Robot("Arale");
}
/**
* checks whether an instance is null
*/
@Test
public void testIfNull() {
assertNull("Comprobamos que r1 es nulo", r1);
}
/**
* checks whether an instance is NOT null
*/
@Test
public void testIfNotNull() {
assertNotNull("Comprobamos que r2 NO es nulo", r2);
}
/**
* checks whether two variables reference the same instance
*/
@Test
public void testSameObject() {
assertSame("r2 y r3 hacen referencia a lo mismo", r2, r3);
}
/**
* checks whether two variables reference different instances
*/
@Test
public void testNotSameObject() {
assertNotSame("r1 y r2 NO hacen referencia a lo mismo", r1, r2);
}
/**
* checks whether inital autonomy is 42
*/
@Test
public void testInitialAutonomy() {
assertEquals("Initial autonomy is 42", 42.0, [Link](), 0.1);
}
/**
* checks wether an expression is true
*/
@Test
public void testIfItsTrue() {
[Link](-4.0);
4.3 Granularidad
A la hora de pensar los tests que vamos a hacer, hasta que punto
queremos comprobar los métodos por separado o nos basta con comprobar
algo más general
Esta es la clase que vamos a testear, que en principio tiene distintos
métodos que podríamos testear de forma separada:
[Link]
package [Link].ju03granularity;
import [Link];
/**
* Represents a team with players. Players are represented by numbers
* @author Eugenia Pérez, Pello Altadill
*/
public class Team {
private Vector<Integer> numbers;
private String name;
[Link]
En este caso, se testea una clase con un único método de testeo:
package [Link].ju03granularity;
import static [Link].*;
import [Link];
/**
* Test class for Team, testing everything inside a single test method
* @author Eugenia Pérez, Pello Altadill
*/
public class TeamTest {
@Test
public void testAllTeam() {
Team target = new Team("Rial Mandril");
assertEquals("Name assignation is correct", [Link](), "Rial Mandril");
assertEquals("Initial size is 0",[Link](),0);
[Link](0);
[Link](23);
[Link](42);
assertEquals("23 is at position 1",[Link](1),new Integer(23));
[Link](-3);
assertFalse("-3 player number is not present", [Link](-3));
assertEquals("Size is 3",[Link](),3);
}
}
4.4 SetupTearDown
En ocasiones, puede que los tests requieran de alguna instrucción que se
ejecute antes y después de cada test.
[Link]
Esta es una clase que representa un coche, con su tipo, su depósito y su
consumo que varía según el tipo de motor:
package [Link].ju04setupteardown;
/**
* Represents a car, depending on the type of engine
* consumption will be different
* @author Eugenia Pérez, Pello Altadill
*/
public class Car {
private String brand;
private double tankCapacity = 50.0;
private double tankStatus = 0.0;
private int type;
[Link]
Aquí está la clase de testeo.
package [Link].ju04setupteardown;
import static [Link].*;
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
/**
* Testing class for Car class
* @author Eugenia Pérez, Pello Altadill
*/
public class CarTest {
import [Link];
import [Link];
import [Link];
/**
* TestRobotDirectly
* Testing class for Robot
* Testing is launched by this class itself, using
* the main method. So there is no need of executing with junit,
* we just execute this class.
* @author Eugenia Pérez, Pello Altadill
*/
public class TestRobotDirectly extends TestCase {
Robot r1;
Robot r2;
Robot r3;
Robot r4;
/**
* TestRobot We init some values for testing purposes. jUnit also offers
* setUp/tearDown methods for this
*/
public TestRobotDirectly() {
r1 = null;
r2 = new Robot("R2D2");
r3 = r2;
r4 = new Robot("Arale");
}
/**
* checks whether an instance is null
*/
@Test
public void testIfNull() {
…
}
/**
* checks wether an expression is true
*/
@Test
public void testIfItsTrue() {
…
}
/**
* checks whether an expression is true
*/
@Test
public void testFalso() {
…
}
/**
* main
* This launches junit
*/
public static void main (String args[]) {
[Link](new TestSuite([Link]));
}
}
[Link]
La clase de testeo define un test por cada método público de la clase
[Link] y además lleva a cabo test varias veces.
package [Link].ju07repeattest;
import [Link];
import [Link];
import [Link];
import [Link];
import [Link];
/**
* ValidatorText
* Testing class for Validator
* Shows how to perform a test repeatedly
* These comes in handy when we need to test some method
* more than once or when we want to check performance
* @author Eugenia Pérez, Pello Altadill
*/
public class ValidatorTest extends TestCase {
public ValidatorTest(String testMethodName) {
super(testMethodName);
}
/**
* testIsNumber
* tests number checking method
*/
@Test
public void testIsNumber( ) {
Validator validator = new Validator("31337");
assertTrue("Number validation correct",[Link]());
validator = new Validator("r2d2");
assertFalse("False number validation correct",[Link]());
}
/**
* testIsText
* tests text checking method
*/
@Test
public void testIsText( ) {
Validator validator = new Validator("hacker");
assertTrue("Text validation correct",[Link]());
validator = new Validator("h4x0r");
assertFalse("False text validation correct",[Link]());
}
/**
* testIsSomethingElse
* tests text is something else checking method
*/
@Test
public void testIsSomethingElse( ) {
Validator validator = new Validator("h4x0r");
assertTrue("Something else validation correct",[Link]());
validator = new Validator("42");
assertFalse("False something else validation correct",[Link]());
}
/**
* suite
* Uses TestSuite to add repeated tests
*/
@Test
public static TestSuite suite( ) {
// Podriamos hacer simplemente esto:
// RepeatedTest (el_test, número_de_veces)
//return new RepeatedTest(new TestSuite([Link]),5);
// Pero vamos a usar TestSuite
TestSuite mySuite = new ActiveTestSuite( );
[Link](new RepeatedTest(new ValidatorTest("testIsNumber"),50));
[Link](new RepeatedTest(new ValidatorTest("testIsText"),10));
[Link](new RepeatedTest(new ValidatorTest("testIsSomethingElse"),10));
return mySuite;
}
}
import [Link];
import [Link];
import [Link];
@RunWith([Link])
@SuiteClasses({[Link],
[Link],
[Link]})
public class AllTests {
}
5 TEST UNITARIOS EN VISUAL STUDIO
Visual Studio también nos facilita el desarrollo de las pruebas unitarias
para nuestros proyectos. La forma de crear el código de pruebas varía un
poco respecto a Eclipse, ya que Visual Studio genera un proyecto aparte
solamente para las pruebas, dentro de la misma solución.
Lo correcto al usar Visual Studio sería crear soluciones para organizar
por un lado nuestro proyecto de código y dentro de la misma solución
gestionar el proyecto de pruebas unitarias.
/**
* We mock a BufferedReader using mockito
* @author Eugenia Pérez, Pello Altadill
*/
public class MockFile {
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
// We create an instance of a false or mocked BufferedReader
BufferedReader mockBufferedReader = mock([Link]);
// Then we stub readLine() function. It will return "747:John:Doe"
when([Link]()).thenReturn("747:John:Doe");
// We establish that when mocked reader calls close an exception
// will be thrown
doThrow(new IOException()).when(mockBufferedReader).close();
// Let's try
[Link]("First line: " + [Link]());
// Now we close...
[Link]();
BufferedReader anotherMockReader;
}
}
Capítulo 4
Código limpio
1 INTRODUCCIÓN
1.1 Manifiesto
Software
Craftmanship
Se trata de un manifiesto que promueve el desarrollo de software bien
elaborado, hasta el punto de alejarlo del concepto frío de ingeniería y
acercarlo al concepto de maestría de los artesanos.
Este manifiesto es también una consecuencia del auge de las
métodologías ágiles y una extensión del manifiesto ágil. En definitiva, trata
de que prevalezca la habilidad de desarrollar un buen código por encima de
otras consideraciones como los costes, el enfoque más científico del proceso
de desarrollo.
El manifiesto
Como aspirantes a artesanos del software estamos elevando el listón del
desarrollo de software profesional mediante la práctica y ayudando a
otros a aprender el oficio. A través de este trabajo hemos llegado a
valorar:
2 COMENTARIOS
2.1 Comentarios
incorrectos
Son la mayoría de comentarios que podemos encontrar, y algunos son
fácilmente reconocibles:
2.1.1 Captain
Obvious
Se podría llamar así a los comentarios en situaciones tan obvias que no
aportan absolutamente nada, más que ruido y redundancia.
El caso más típico es el de indicar el nombre de una clase o que el
constructor por defecto es, efectivamente, el constructor por defecto.
/**
* Default constructor
*/
public BadComments () {
}
2.1.2 Comentarios
impuestos
por políticas
En las empresas suelen existir ciertas prácticas establecidas que
imponen cómo deben escribirse los programas, y generalmente incluyen la
obligación de documentar todo el código con comentarios. Si esto es una
obligación, no hay más remedio, pero si se hace por pura costumbre sin una
necesidad real, deberíamos evitarlo.
Esto suele incluir desde una cabecera común con un texto que hay que
copiar y pegar junto con el nombre del desarrollador, una referencia a un
documento de análisis, etc.
/*
* Copyright © 2017 Pello Altadill
* All rights reserved under the copyright laws of the United States and
* applicable international laws, treaties, and conventions.
* You may freely redistribute and use this sample code, with or without
* modification, provided you include the original copyright notice and
* use restrictions.
*
* Disclaimer: THE SAMPLE CODE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
También suele abusarse de incluir interminables cláusulas legales o
disclaimers al principio. E incluso las convenciones de los lenguajes
promueven el uso de comentarios y nos dicen cómo debemos hacerlos. Lo
cierto es que ninguno de estos comentarios aporta valor al código, no hacen
más que mancharlo.
2.1.3 Información
histórica
Derivada del anterior, otra política común suele ser que todos aquellos
cambios que se hacen en un código queden registrados en el código fuente,
incluyendo fechas, autor y una breve descripción de los cambios realizados.
/**
* BadComments
* <i>Simple</i> class to show the bad use of comments
* @version 0.11.0
* @author PELLO_ALTADILL
* @email sher@[Link]
* @since 2010/03/11
* @version 0.10.0
* @since 2010/01/15
* Same author
* Code improvements to setters
*
2.1.4 Comentarios
para generar
documentación
Existen mecanismos como los Javadocs que nos permiten generar
documentación a partir de los comentarios de código. Esto nos obliga a
incluir comentarios delante de todas las declaraciones de clases y en todos los
métodos.
En los métodos es peor, ya que nos obliga a meter una descripción del
método además de documentar con una serie de marcas los parámetros, el
retorno y las excepciones, si las hubiera.
/**
* Constructor with arguments
* @param badAttribute
* @param age
*/
public BadComments(String badAttribute, int age) {
[Link] = badAttribute;
[Link] = age;
}
Por si esto fuera poco, encima se suele admitir código HTML que luego
acabará en esa documentación pero que ensucia aún más si cabe, el código.
Salvo que realmente sea precisa esa documentación, este tipo de
comentarios que existen para todos los lenguajes (PHP, JavaScript...) son
puro ruido. La mejor forma de documentar un código, además de un código
que se explique solo, es precisamente es a través de los Test Unitarios.
Por normal general, si ese código no va a ser compartido con el público,
si no es un API abierto, ese tipo de código de documentación es totalmente
innecesario.
2.1.5 Desahogos
Tal cual, los programadores vuelcan sus frustraciones mientras escriben
y son capaces de poner cualquier barbaridad. Incluso pueden estar
reconociendo que hacen chapuzas, y lo que es peor, pueden llegar a hacerlo
en código que es visible a todo el mundo, como en el JavaScript de una Web.
// Generate random numbers
for (int i = 0; i < 10000; i++) {
// HACK, fix this piece of shit
result += "" + [Link](10000);
if ([Link]() > length) {
2.1.6 Posiciones
y cierres
También es frecuente que los desarrolladores incluyan marcas o
barreras para delimitar distintas zonas del código.
/* ATTRIBUTES *********************/
//////////// ATTRIBUTES ////////////
/////////// METHODS ////////////////
Esto no es necesario si se siguen unas mínimas convenciones del
lenguaje, que como en Java indicar el orden en que deben meterse los
atributos, métodos, etc. según sean públicos o privados.
De la misma manera, para evitar la confusión en los cierres de bloques
anidados, hay quien incluye un comentario de una línea:
} //if
} // for
2.1.7 Código
comentado
También podríamos llamarlo el síndrome de Diógenes del
programador. Conforme el código cambia o evoluciona, por precaución y por
si hay que revertir los cambios, o simplemente por no eliminar un trabajo ya
hecho, el desarrollador comenta un código inútil.
for (int i = 0; i < 10000; i++) {
// HACK, fix this piece of shit
result += "" + [Link](10000);
if ([Link]() > length) {
break;
} //if
// Old way to do stuff
// result += [Link]() + " ";
// if ([Link]()/2 > length) { return result }
// else { continue; }
} // for
2.1.8 Comentarios
de carácter
general
A veces, con el propósito de informar se incluyen largos comentarios
que describen:
El propósito del proyecto o de una parte del código
La parte de los requerimientos que cubre ese código
Una explicación de la lógica de negocio
Referencias a RFC u otros estándares
Todos esos comentarios, sobran y son irrelevantes para quien debe
mantener ese código. Además, es probable encontrar detalles en métodos que
tampoco aportan nada por ser una característica general del proyecto.
/**
* Sets config file. Config file must be stored in /etc/config/wdp
*/
public void setConfigFile(String name) {
[Link] = name ;
}
2.1.9 Comentarios
confusos
O comentarios inconexos. Si se quiere incluir un comentario, este debe
ser útil, relevante, informativo, pero nunca lo contrario.
Si encima el código cambia y ese comentario no se mantiene, pierde
todo su valor y se convierte en basura.
2.2 Comentarios
aceptables
2.2.1 Los
obligados
A veces, las circunstancias nos pueden obligar a incluir estos
comentarios, ya sea por contrato o por otros motivos. Esto incluye
información de autoría, copyright, licencias etc.
Si no hay más remedio tendremos que incluirlo. La buena noticia es que
en los IDEs podemos hacer que los comentarios no sean visibles.
2.2.2 Los
útiles
Se trata de comentarios muy precisos en situaciones muy concretas que
realmente aportan información, explican algo y clarifican.
En ocasiones podemos tener parte de un código que cumple su función,
pero no es muy claro cómo lo hace o bajo que circunstancias. Por ejemplo, en
el uso de expresiones regulares, puede ser útil incluir un ejemplo en forma de
comentario.
private String regexDNI = "[0-9]{8}[A-Z]{1}"; // Ex: 31345753L
private String now = new Date().toString();
// Ex: Tue Feb 06 [Link] EST 2017
2.2.3 Advertencias
En determinadas circunstancias, podemos tener un código que hace su
trabajo, pero cuya ejecución puede tener consecuencias para el sistema.
Siendo así no está demás comentarlo, no para explicar el funcionamiento sino
para advertir sobre las consecuencias de uso.
// Use at your own risk. Not very efficient.
for (int i = 0; i < 10000; i++) {
[Link](10000); // 10000 it is the only reasenable way
}
return result;
De la misma manera, si queremos remarcar la importancia de
determinada manera de hacer las cosas para evitar que alguien las altere, un
comentario puede resultar útil a modo de aviso.
2.2.4 TODOs
Podemos marcar pequeñas tareas pendientes dentro del código para que
sean retomadas más adelante. No se trata de tareas grandes como un
requerimiento completo, sino de código que se debe añadir para completar
una funcionalidad.
// TODO, change execution dependency
public String createPassowrd (int length) {
String result = "";
Random random = new Random();
…
return result;
}
3 NOMBRES
3.1.1 Variables
Deben utilizarse sustantivos o nombres, y deben describir claramente
el propósito que tiene esa variable.
3.1.2 Métodos
Los métodos sirven para llevar a cabo acciones así que deben utilizar
verbos que describan su función.
Hay un par de casos especiales:
1. En el caso de tener métodos que retorne un valor booleano,
debe tener forma de predicado “is..”, por ejemplo:
isClosed()
2. En el caso de ser un método de acceso a los atributos de una
clase, usamos la palabra get: getName(), get
3.1.3 Clases
Por lo tanto, en resumen:
3.2 Reglas
Por lo general los nombres deben ser significativos y ajustados al
propósito de lo que nombran.
3.2.1 Revelar
la
intención
Es el motivo esencial de todo nombre de variable o de método. Debe
indicar para que sirve, sin escatimar en caracteres. Por lo tanto, se deben
evitar todos aquellos nombres que no tengan nada que ver con el propósito
que se desempeña:
int foo;
String var;
Por supuesto, hay que evitar los nombres abreviados o los caracteres
únicos:
int p = 0;
ArrayList ar;
Este norma tiene una excepción, en los bucles for cuando usamos
variables como i,j,k para los índices.
Por norma, si ves que un nombre no es suficientemente claro o que has
necesitado comentarlo para aclarar su propósito, entonces debes elegir un
nombre mejor. Quizá no elijamos un nombre perfecto a la primera, pero no
debemos dudar en renombrar hasta dar con el nombre más preciso.
3.2.2 Describir
el
problema
Los nombres deben ser descriptivos, deben tener un significado que
aclare y por supuesto describa el problema que están tratando.
int age;
float amount;
Date birthDate;
3.2.3 Utiliza
nombres
pronunciables
Parece mentira que algo tan obvio haya que recordarlo y, sin embargo,
este es un error en el que caen no pocos desarrolladores. Muchas veces
contagiados por la acumulación de siglas que rodea este ámbito, un
programador tiende a crear abreviaturas o iniciales en lugar de nombres
completos. Por ejemplo:
int initqty;
int fnlam;
Sería mucho más claro:
int initialQuantity;
Float finalAmount;
3.2.4 Evita la
desinformación
Hay que vigilar que los nombres que damos no caigan en
ambigüedades. Quizá en el momento de crear el nombre, su propósito esté
muy claro para el programador, pero otro lo puedo confundir con otros
conceptos. Una variable como:
int *fps;
Y por supuesto, hay que tener especial cuidado con caracteres como el
cero y la letra 0, el número uno y la letra l, ya que pueden dar lugar a
equivocaciones.
3.2.5 Distingue
correctamente
Dentro del ámbito de una clase o incluso dentro de un método, puede
ocurrir que necesitemos utilizar el mismo nombre para un método o variable.
Si no es posible usar el mismo, un desarrollador puede optar por añadir un
número al nombre o incluso distorsionar los nombres con tal de hacer que el
compilador los pueda distinguir. Pero ¿qué hay del significado del nombre?
Por ejemplo, en la siguiente función usamos una variable llamada
theMessage para no liarla con el parámetro message:
public void parse (String message) {
String theMessage = base64(message) ;
…
}
Así como para los métodos de acceso ya está claro que se usa get, el
resto puede dar lugar a todo tipo de usos solapados y no muy claros.
Conviene, por tanto, establecer un criterio y evitar que un mismo nombre o
verbo tenga distintos significados o consecuencias.
Por ejemplo, en los verbos, todos aquellos métodos cuyo nombre sea el
mismo deberían hacer lo mismo, con una signatura de método similar en
parámetros y retorno.
Interfaces e implementaciones
Esto merece un apartado aparte. Las interfaces requieren una clase
como mínimo, que las implemente y esto hace que las dos puedan tener el
mismo nombre. Una de los dos debe distinguirse.
public interface Log
Por norma general, está más aceptado usar un nombre normal para los
interfaces, y en todo caso, añadir un sufijo a las implementaciones. Si hay
más de una implementación, basta con usar un sufijo que las distinga y sea
significativo:
public class LogMemory implements Log
public class LogFile implementes Log
3.3 La
regla
del
ámbito
Esta regla indica lo siguiente:
La longitud de una variable debe ser proporcional a su ámbito
Dicho de otra manera, si el ámbito de una variable es muy reducido,
como por ejemplo el cuerpo de un bucle, podemos usar incluso un único
carácter:
for (Customer c: listCustomers) {
sendEmail(c);
}
Ese ámbito es tan reducido, que el propósito de c está suficientemente
claro y no es necesario darle un nombre más significativo.
Por el contrario, si el ámbito y, por tanto, la importancia de una variable
es mayor, debemos usar nombres más largos.
4 MÉTODOS
Los métodos son bloques de código básicas que forman las clases y a su
vez los proyectos. Inicialmente nacen como forma de organizar los
programas, separando código y permitiendo que este se puede reutilizar y
mantener mucho más fácilmente.
Dependiendo de la complejidad de los mismos, el código de un método
puede acabar extendiéndose sin control, por lo que dejan de ser claros y
mucho menos mantenibles. Vamos a ver a continuación las reglas esenciales
para conseguir métodos que cumplen los principios de código limpio.
4.1 Un
método
solo
debe
hacer
una
cosa
El título ya lo dice todo y es un principio esencial en el desarrollo de
métodos. Un método solo debe servir a un propósito, solamente debe tener
una función. No se deben acumular funcionalidades dentro de un mismo
método. Unos consejos que han sobrevivido durante décadas son los
siguientes:
1. Un método solo debe hacer una cosa.
2. Esa cosa la debe hacer bien
3. Debe hacer eso únicamente
Por lo tanto, nos debemos asegurar de que el método, realmente solo
hace una cosa, y si no es así, debemos plantearnos dividir el método. Esto nos
obliga a simplificar cada método y crear un método específico para cada
necesidad.
4.2 La
extensión
de un
método
La extensión es una característica básica de todo método y que sin duda
influye de manera clave en su legibilidad. ¿Cuánto debe extenderse el código
de un programa? Cuando la longitud adecuada de una función empezó a
cuestionarse y ha habido distintas aproximaciones:
En primer lugar, se decía que el código debía caber en una
pantalla convencional, es decir, no debía exceder los 24
caracteres.
En los textos clásicos de refactorización se mencionaba
aquella regla de que el código de un método debía entrar en
la pantalla si se proyecta en un aula y todo debería ser
legible desde la última fila.
Actualmente, un buen adepto del software craftmanship
afirmará sin pestañear que una función no debería tener más
de… ¡5 líneas!
Lo cierto es que, uno de los mandamientos cruciales del código limpio,
es que en los métodos se debe aplicar la refactorización de extraer todo
lo que se pueda: extract until you drop.
Por ejemplo, el siguiente método podría mejorarse mucho:
public String print(String content) {
String result = "";
result += "<html><head></head><body>";
result += content;
result += "</body></html>";
return result;
}
4.3 Reglas
No solo debemos cuidar la longitud. Las funciones tienen parámetros,
retornos, variables, etc. y debemos cuidar todos los detalles. Estas son
algunas de las reglas que se deben tener en cuenta a la hora de escribir
métodos
4.3.1 No más de
tres
argumentos
Tres argumentos es el máximo que todo método debería admitir.
Cuantos más argumentos, más confuso se hace el método, y mucho más
difícil de manejarlo e invocarlo por parte del programador. Lo que es peor, se
acaban usando parámetros con valores por defecto que rara vez se utilizan.
La ventaja es que, si no hay más remedio que usar hasta 3 o incluso hay
más, es muy sencillo crear una refactorización que convierta todos esos
parámetros en una clase. De tal manera que de tener varios parámetros
pasemos a tener únicamente uno.
Un ejemplo muy habitual sería el de las coordenadas:
public void moveCharacter(Character carácter, int x, int y, int z) {
}
4.3.2 No utilizar
argumentos
booleanos
Cuando se pasa un argumento booleano a un método suele tener un
objetivo claro: que el método haga una cosa u otra dependiendo del valor de
ese booleano:
public void loadFile (boolean isXML) {
if (isXML) {
// code for this case
} else {
// code for the opposite
}
}
Podría ser incluso peor: podría tener un argumento numérico con el que
hiciéramos una estructura switch/case y alargásemos mucho más el método:
public String printFormat(String content, int format) {
String result = "";
switch (format) {
case 0:
result = "<html><head></head><body>" + content + "</body></html>";
break;
case 1:
result = "{ \"content\": \"" + content + "\"}";
break;
default:
result = content;
break;
}
return result;
}
Parámetros nulos
Ocurre un caso similar cuando uno de los parámetros puede ser null y el
método actúa de una forma u otra dependiendo de ese valor.
public void saveCustomerData (Customer customer) {
If (null == customer) {
// code for this case
} else {
// code for the opposite
}
}
4.3.3 No utilizar
argumentos
de salida
Algunos lenguajes de programación permiten el uso de argumentos de
tres tipos:
1. In: solo de entrada, su valor no cambia al terminar la
llamada
2. Out: solo de salida, su valor puede cambiar al terminar la
llamada
3. In/out: tanto entrada como salida
Los parámetros tipo out crean incertidumbre cuando se ven, porque
nunca sabemos si el valor del argumento habrá cambiado o no.
El hecho de que exista esa disponibilidad no significa que debamos
usarla. Solo se deberías usar los parámetros tipo in, los que están
disponibles en todos los lenguajes. Y si hay que devolver algún resultado,
debemos usar el return
4.4 La
programación
defensiva
Se trata de una programación en la que continuamente se comprueba
que los datos de entrada son correctos o no están vacíos. Esto no hace más
que llenar el código de tediosas comprobaciones que nos deberíamos ahorrar.
Una cosa es la validación en el punto del proyecto que es visible a un usuario,
pero… ¿debemos validar en todas partes?
Este sería un simple ejemplo
private int división (Integer x, Integer y) {
if (null != x && null != y && y != 0) {
return x/y;
}
}
4.5 El
orden
de los
métodos
Conforme vayamos separando un método en métodos más pequeños se
nos van a ir acumulando muchos pequeños métodos, y obviamente no
debemos ponerlos en cualquier orden. Si bien podemos usar las facilidades
que ofrece el IDE para movernos por el código a través de saltos directos
(Ctrl-Ratón en llamadas por ejemplo) o un panel de navegación, siempre
debemos mantener un orden en los métodos creados.
La regla de StepDown establece que los métodos que provienen de
extracciones deben situarse justo debajo de los métodos de los que se han
extraído, tabulando si es preciso conforme las extracciones se hacen en
cascada.
De esta manera, por mucho que dividamos un método en muchas
partes, es mucho más sencillo seguir la pila de llamadas y hacer que el código
sea más legible.
4.6 Las
sentencias
Switch
El mecanismo Switch/Case existe y de hecho se vende como algo
ventajoso cuando necesitamos una estructura condicional que lo único que
hace es una cosa u otra dependiendo del valor de una variable.
Pero la presencia de un Switch/Case siempre acaba siendo problemática
y como mínimo hace que el código de un método se extienda excesivamente.
Observa estos métodos:
public int attack() {
int points = 0;
// the attack calculation depends on
// the type of character -code smell
switch (type) {
case 0: // barbarian
points = strength + D6.getD6().roll();
break;
case 1: // elf
points = speed + D6.getD6().roll();
break;
case 2: // dwarf
points = strength + D6.getD6().roll();
break;
case 3: // wizard
points = intelligence + D6.getD6().roll();
break;
default:
points = D6.getD6().roll();
break;
}
// Side-effects
strength = strength - (points/2);
speed = speed - (points/3);
return points;
}
4.7 Separación
de
comando y
consulta
Esta es una división que debe estar bien clara tanto en el nombre como
en sus consecuencias. Se trata en definitiva de que los métodos setters y
getters realmente se ciñan o bien a cambiar un dato o bien a leerlo, pero
nunca a hacer las dos cosas.
Comandos
Un comando es un método que puede cambiar el estado de un objeto,
que cambia algún valor, como los métodos set de una clase.
Consulta
Una consulta no es más que un método que lee unos datos y los retorna.
Pero en ningún caso modifica nada, que es lo que deberían hacer los métodos
get de una clase.
Por ejemplo, una mala separación sería un método como el siguiente:
public class Invoice {
private float total;
…
public float getTotal () {
if ([Link] == null) {
[Link] = 0 ;
}
return [Link] ;
}
public float totalInvoice(float amount) {
[Link] = amount * VAT ;
return [Link] ;
}
}
4.10 Control
de
errores
Los errores son inevitables y obviamente deben ser controlados, pero en
ningún caso debemos permitir que el excesivo control de los mismos, acabe
ocultando la lógica de nuestro código. Como se decía anteriormente, no se
debe caer en la programación defensiva.
4.10.1 Evita
los
códigos
de
error
Algo tan perjudicial como el excesivo celo en comprobar todos los
datos en todas partes, es el de retornar códigos de error numéricos.
Los códigos de error nos obligan a crear programas que de forma
condicional los generan y por otro lado nos obliga a crear estructuras,
posiblemente switch/case, para controlarlos. Por no hablar de que hay que
crear y gestionar un sistema de códigos de errores y su significado. Veamos
el siguiente ejemplo:
private Double getValueFromUser () {
Scanner reader = new Scanner([Link]);
String line = "";
Double result;
[Link]("Please, enter a number");
line = [Link]();
if ([Link]().equals("")) {
return -2d;
} else if (?")) {
return -1d;
} else {
result = [Link](line);
if (result < 0) {
return null;
} else {
return result;
}
}
}
Como se puede ver, el método que lee por la consola lo que introduce el
usuario devuelve distintos valores que tienen un significado según el error
producido.
Eso obliga a que el método que le llama tenga que comprobar cada uno
de esos casos:
public void convert () {
Double value = getValueFromUser();
if (value == -2) {
[Link]("Given value was empty");
} else if (value == -1) {
[Link]("Given value was not numeric");
} else if (null != value) {
[Link]("Conversion: " + value * 0.88d);
} else {
[Link]("Given number was negative");
}
}
Por muy buen programador que sea uno, hay situaciones que siempre
escaparan de su control, situaciones que pueden depender de un servicio
externo, de la entrada de usuario, de la existencia de un fichero, etc. para
controlar eso es preferible lanzar una excepción.
4.10.2 Las
excepciones
Como ocurre con otras malas prácticas, a veces se heredan hábitos de
desarrollo que podían tener sentido hace décadas (como los códigos de error),
cuando no se disponía de determinados mecanismos.
En el caso de los errores, desde hace tiempo contamos con las
excepciones, las cuales nos evitan tener que manejar códigos de error, nos
ofrecen un amplio catálogo de posibles errores a capturar e incluso nos
permite crear nuestras propias excepciones.
En el método anterior podríamos replantearlo para que en lugar de
devolver códigos de error simplemente lance una excepción:
private Double getValueFromUser () throws Exception {
Scanner reader = new Scanner([Link]);
String line = "";
[Link]("Please, enter a number");
line = [Link]();
if (?")) {
throw new Exception("Number format exception: " + line);
} else {
return [Link](line);
}
}
5 CLASES
Una vez que hemos establecido una serie de consejos para desarrollar
un código limpio desde las variables a los métodos, debemos ocuparnos de la
unidad de código más representativa de la programación orientada a objetos:
las clases. La regla esencial se puede deducir fácilmente:
Las clases deben ser pequeñas
Así como los métodos deben ir haciéndose cada vez más pequeños,
también debemos limpiar la clase de tal manera que su tamaño vaya
menguando. En principio, un sistema donde hay muchas pequeñas clases en
lugar de unas pocas clases grandes es preferible, y es señal inequívoca de que
se va por el buen camino.
¿Cómo deben ser de pequeñas? Debemos pensar en las clases no en
función a su número de líneas o métodos, sino a su número de
responsabilidades. Por lo tanto, no hay una regla numérica para controlar el
tamaño de una clase, en este caso hay que reducirla en torno a una ley: el
principio de responsabilidad única.
5.1 Responsabilidad
única
Este principio afirma que una clase solo tiene que tener una
responsabilidad. La forma de saber si eso se cumple o no es, decirlo de otra
manera:
Si se cambia una clase, solo debe haber una razón para modificarla
De esta manera, si tenemos una clase y detectamos que está cubriendo
más funcionalidades de las que debería, la acabaremos separando en otras
clases y creando un sistema cada vez más desacoplado y mejor cohesionado.
Este sería un ejemplo de una clase con demasiadas responsabilidades.
En principio la clase Dungeon debería representar un escenario concreto,
pero sin embargo le otorgamos infinidad de responsabilidades que poco
tienen que ver con la representación de un lugar en si:
5.2 La
cohesión
No se debe confundir con el acoplamiento. La cohesión nos indica la
capacidad de un conjunto de clases de funcionar de forma conjunta sin
solaparse. La cohesión entre clases debe ser lo más alta posible. Además,
para considerar que una clase es cohesiva por si misma, se debe procurar lo
siguiente:
Tener pocos atributos o variables de instancia
Que esas variables sean utilizadas por la mayor parte de
métodos posibles
Si los métodos se mantienen pequeños y con pocos parámetros, puede
tenderse a crear más atributos compartidos entre los métodos. Si se empiezan
a acumular, es probable que se termine necesitando crear una nueva clase.
5.3 Cambios
controlados
Conforme avancemos en el desarrollo de software es probable que
tengamos que modificar el código de las clases para añadir nuevas
funcionalidades. Los cambios son necesarios, pero a la vez introducen el
riesgo de romper, no solo el correcto funcionamiento del código existente,
sino además el principio de responsabilidad única.
Como veremos más adelante en los principios SOLID, una clase debe
estar abierta a tener extensiones, pero cerrada a ser modificada.
5.3.1 Uso de
interfaces
Otra manera de mantener el control sobre el código es tratar de aislarlo
de los cambios. Si alguien está usando directamente una clase, si se modifica
algo en esa clase estamos arriesgando no solo su funcionamiento sino del
cliente o sistema que utiliza esa clase. Puede ser tan simple como modificar la
signatura de un método que lo haga incompatible con la llamada que hace el
cliente.
Veamos el siguiente ejemplo, donde tenemos una clase que representa
un coche con una serie de atributos y métodos. Los atributos son privados,
pero se exponen a través de los métodos set/get que muy frecuentemente se
generan de manera automática:
public class ExposedCar {
private float tank;
private float speed;
private String model;
private String color;
private String plate;
private boolean isStarted;
private boolean isLocked;
public ExposedCar () {
}
public ExposedCar(float tank, float speed, String model, String color, String plate, boolean
isStarted, boolean isLocked) {
[Link] = tank;
[Link] = speed;
[Link] = model;
[Link] = color;
[Link] = plate;
[Link] = isStarted;
[Link] = isLocked;
}
public void startEngine () {
isStarted = true;
}
public void stopEngine () {
isStarted = false;
}
public int accelerate (int sec) {
speed = speed + sec;
tank = tank - sec;
return (int) (speed/2);
}
public int brake (int sec) {
if (sec < speed) {
speed = speed - sec;
} else {
speed = 0;
}
return (int) (speed/2);
}
// getters/setters
}
5.4 Estructuras
de datos y
objetos
En un sistema complejo no todas las clases tienen la misma naturaleza o
cumplen el mismo cometido. En general, en la POO se dice que toda clase
representa una entidad con sus atributos y métodos, y que, en esencia, todo
puede representarse como un objeto. Sin embargo, existen dos formas de
clases, que se distinguen según lo que exponen y lo que ocultan: las
estructuras de datos y los objetos.
Una estructura de datos
Vemos el siguiente ejemplo, una clase que representa a un jugador. Como se
puede ver, no es más que una clase con una serie de atributos que se exponen
a través de get/set e incluso de una implementación de toString(). Se parece a
lo que se suele conocer como POJO (Plain Old Java Object), y lo que hace
está clase es exponer datos:
[Link]
public class Player {
private String name;
private int strength;
private int speed;
private int intelligence;
private int type;
private int life;
Position position;
public Player () {
}
Un objeto
Ahora veamos la misma clase, planteada de otra manera. En lugar de
tener un simple conjunto de atributos expuestos (datos), esta clase expone
sus métodos:
[Link]
public class Player {
private String name;
private int strength;
private int speed;
private int intelligence;
private int type;
private int life;
Position position;
private Random random;
public Player(String name, int type) {
[Link] = name;
[Link] = type;
init();
}
private void init() {
strength = [Link](10);
speed = [Link](10);
intelligence = [Link](10);
life = strength * 2;
position = new Position();
}
public String getName() {
return name;
}
public void move (int steps) {
[Link](steps);
}
public int attack () {
return (speed + strength)/2;
}
public int defend () {
return ((speed + strength) * intelligence)/2;
}
public int spell () {
return (speed * intelligence)/2;
}
}
5.4.1 Ley de
Demeter
Si tratamos de seguir las reglas de la programación orientada a objetos,
debemos procurar en definitiva NO exponer los datos sino las funciones que
realiza una clase. Por tanto, no se deben usar métodos de acceso como get/set
a la ligera y no debe exponerse al exterior.
La Ley de Demeter es un conjunto de reglas que nos puede servir para
medir la cualidad de encapsulación de nuestra clase. Dice así:
En una clase C un método f solo puede llamar a:
1. Métodos de C
2. A objetos creados por f
3. A objetos pasados como argumentos a f
4. A objetos contenidos en variables de instancia o atributos.
5.4.2 DTOs y
Registros
activos
En sistemas donde se lleva a cabo el acceso a bases de datos, es común
utilizar clases que representan una forma muy clara de estructuras de datos.
Las DTO o Data Transfer Object. Se trata de clases que mapean las mismas
estructuras que existen en los registros de una tabla de la base de datos. Esto
es lo que hace el Patrón DAO o lo que hacen los frameworks ORM como
Hibernate, Doctrine, etc.
Se trata de clases que no tienen más que atributos, un constructor vacío
y los métodos set/get. Cada atributo se corresponde con un campo de una
tabla de la BD.
En Java por ejemplo, se les llama POJOs o incluso, si utilizan
anotaciones hay quien los llama AJO (Annotated Java Object).
Existens estructuras de datos que van un poco más allá de las DTO
como los Active Records, las cuales representan datos y además disponen de
métodos para moverse por los registros. No son una opción adecuada si
queremos distinguir estructuras de datos puras de objetos.
6 ARQUITECTURA
6.1 Introducción
Hasta ahora nos hemos centrado en aspectos del software muy
específicos como variables, funciones, clases, etc. Pero para tener un
proyecto que sea realmente limpio, conviene tener claro qué es una
arquitectura.
Al desarrollar un proyecto desde cero, resulta imprescindible establecer
unas bases sólidas sobre las que fundamente un proyecto. Como veremos más
adelante, hay veces que se toman unas decisiones prematuras que no deberían
ser tan vitales.
Generalmente presenta una arquitectura como un diseño de alto nivel
que dirige todo el desarrollo. Una arquitectura debería ser la forma que toma
el sistema para cumplir con los casos de uso y en definitiva dar respuesta a
los requerimientos del cliente.
La arquitectura que se muestra como ejemplo puede parecer, para los
ojos de un profano, que supone demasiado trabajo. Pero lo cierto es que
invertir tiempo en crear una arquitectura en que todo está perfectamente
compartimentado y aislado es mucho mejor a la larga porque:
En todo momento sabemos dónde se encuentra lo que
necesitamos
Hace que podamos retrasar decisiones acerca de las
herramientas a usar
Si cambiamos algo en una parte de esa arquitectura no
afecta al resto
Hace que el sistema sea más fácilmente testable
6.2 Los
casos
de
uso
Al ver el diseño de la arquitectura de un proyecto de software no
deberíamos ver un proyecto Web, de escritorio o de consola o de móvil. Lo
ideal es que una arquitectura exponga sencillamente el uso que se le da al
software. A fin de cuentas, el objetivo de toda aplicación es hacer lo que el
usuario ha pedido.
Por ejemplo, si entramos en un supermercado, lo que deberíamos
observar es que, no se trata simplemente de un lugar amplio donde se apilan
estanterías y pasillos sin sentido alguno. Lo que expresa un supermercado es
un sitio donde se pueden adquirir productos y que además estos se encuentran
ordenados por secciones.
Una buena arquitectura debería mostrar, por tanto, los casos de uso y
estos no deberían mezclarse con la forma de llegar al usuario.
MVC no es una arquitectura
Por ejemplo, en aplicaciones empresariales es frecuente utilizar JEE
con un framework MVC y una BD relacional. Ese tipo de arquitecturas
exponen de manera muy clara su interfaz web a través de la Vista y los
controladores.
La vista es lo más claro, ya que seguramente se trata de un
conjunto de páginas JSP con sus estilos, JavaScript, etc.
Los controladores, por su parte, utilizan métodos que
claramente exponen el carácter Web de la arquitectura, con
parámetros y métodos GET, POST, redirecciones, etc.
Por tanto, el modelo, es considerado erróneamente por
muchos desarrolladores, como el lugar donde se encuentra
la lógica de negocio.
Pero obviamente es una arquitectura errónea porque está totalmente
condicionada por estar orientada al interfaz de usuario. En el código apenas
se ve el propósito de uso, por todas partes vemos web.
6.3 Retrasar
decisiones
Cuando se va a comenzar un proyecto no es raro encontrarse con que
hay una serie de consideraciones que se toman de manera prematura. En
ocasiones un proyecto ya viene marcado por la imposición de utilizar un
lenguaje, un SGBD, un entorno de desarrollo, un interfaz de usuario
específico, un framework, etc.
Por ejemplo, sin conocer aún las necesidades concretas de un cliente,
puede haber gestores de proyectos que de forma prematura decidan que van a
utilizar:
Java
InteliJIDEA
Oracle
Spring e Hibernate
JSP and Apache Tiles
Bootstrap para los estilos
De hecho, todas esas tecnologías son las típicas que se requieren en las
ofertas de empleo. Pero el hecho de usar un framework de inversión de
control o Dependency Injection, de usar MVC, de usar un interfaz web, etc.
no tiene nada que ver con una arquitectura. Y todas esas decisiones deberían
ser pospuestas para ser tomadas más adelantas.
Una buena arquitectura debería permitir
Dejar las opciones abiertas hasta el final
Maximizar el número de opciones disponibles
Por lo tanto, decisiones como la BD no deberían ser clave en el
desarrollo del sistema, pero lamentable en muchos proyectos todo gira en
torno a la BD. Todas las decisiones de ese estilo no deben ocultar los casos
de uso.
¿Cómo podemos hacer para ser capaces de retrasar esas decisiones? A
través del desacoplamiento, para que el interfaz de usuario, la BD, etc. resulte
algo irrelevante. Al final debemos centrarnos precisamente en los casos de
uso.
6.4 Separación
de valor
¿Dónde está el valor del sistema? En el interfaz, en la forma en la que
se llega al usuario. ¿O debería estar en los casos de uso? Erróneamente
alguien le podría dar valor a la forma en la que se llega al usuario, la UI o
User Interface.
Haciendo esto así, podemos acabar contaminando todo el sistema y
haciéndolo depender de ese UI. La lógica de negocio acabaría acoplada a
código de un interfaz web o a un interfaz de usuario de escritorio.
El problema de darle valor al UI es que hace que la arquitectura no
exponga los casos de uso sino su interfaz de usuario. Una buena arquitectura
debería ser adaptable a cualquier UI, tanto de consola como Web como
cualquier otro.
Y eso se consigue a través de la separación de valores, lo cual nos
permitiría diferenciar el coste y valor de cada elemento del proyecto: casos de
uso, UI, BD, frameworks, etc.
6.5 Una
arquitectura
de alto nivel
A continuación, y paso a paso, veremos el aspecto que debería tener
una buena arquitectura, sus componentes esenciales y cómo debería
relacionarse con los elementos externos o “detalles”.
6.5.1 Casos de
uso
El hecho de estar utilizando la web, tablets o el salpicadero de un coche
debería ser un detalle de nuestro sistema. Nuestro sistema debe mostrar para
qué sirve, y eso se hace a través de los casos de uso.
Un caso de uso es, en cierto modo, una descripción formal de cómo el
usuario interactúa con el sistema para poder conseguir una meta. Pero el caso
de uso no debería tener nada que ver con controles de interfaz, botones, listas,
etc. esos son detalles finales que nada tienen que ver con la arquitectura.
Podemos ver la arquitectura como lo siguiente
Sacar dinero
Datos:
DNICliente
CuentaCorriente
Cantidad
Proceso Convencional
1. El operador lanza la operación con los
datos
2. El sistema verifica los datos
3. El sistema modifica el depósito
4. El sistema confirma la operación al
operador
Como se puede ver, este caso de uso no hace referencia a ningún tipo
de elemento de interfaz de usuario. Simplemente se ciñe a unos datos de
entrada, un procedimiento y una salida.
¿Y si algo va mal?
Si alguno de los datos provoca un error o no. En ese caso, se puede
extender un proceso de excepción que extienda el caso de uso normal, por lo
tanto, añadiríamos:
Proceso de Excepción
1. El sistema traslada el error al
operador
6.5.2 Particionado
Basándonos en referencias como la arquitectura de Ivar Jacobson
(Object Oriented Architecture). Crearemos tres tipos de objetos para nuestra
arquitectura.
1. Business objects: Entities
2. UI objects: Boundaries
3. Use case: Controls/Interactor
Entities
Son en definitiva las entidades que pueden ser independientes de una
aplicación. Contienen métodos que pueden utilizarse en distintos escenarios o
aplicaciones y NO deberían tener métodos concretos de una aplicación y
deberían ser útiles en cualquier escenario.
Por ejemplo, el objeto que representa a un usuario puede estar presente
en una aplicación de un banco, de reserva de entradas o de una red social. Los
métodos independientes de la aplicación podrían ser los getters/setters.
Boundaries
Los casos de uso reciben unos datos de entrada y devuelven un
resultado. Los boundaries son los encargados de esta labor, son los
intermediarios entre el caso de uso y el mecanismo de entrega o UI. Son
quienes trasladan los datos de entrada y de salida entre los dos mundos.
¿Cómo se efectúa esa entrada y salida? A nuestra arquitectura no lo
debe importar. Ese flujo de información deberá pasar a través de los
boundaries. Son quienes aíslan en definitiva el interfaz de usuario de nuestros
casos de uso.
Interactors/Use Case
Cualquier método específico de una aplicación tiene que estar aquí.
Los casos de uso SÍ son específicos de una aplicación y por lo tanto toda la
lógica de un caso de uso debe estar presente como Interactors.
Por ejemplo, los casos de uso de hacer un depósito, sacar dinero, hacer
una transacción de un sistema bancario son específicos. En su caso utilizarán
entidades llamando a métodos que son independientes de la aplicación:
Y este sería el aspecto que toma nuestra arquitectura:
Si tomamos un caso concreto, el flujo que se seguiría en esta
arquitectura debería ser así:
Un cliente inicia una petición a través del interfaz de usuario:
6.5.3 Aislamiento
Para que el aislamiento sea total, los datos deben ir en una única
dirección.
Por ejemplo, en una aplicación web MVC, todas sus partes, Modelo,
Vista y Controlador, deben estar fuera de nuestra arquitectura, y solo deben
interactuar a través de los Boundaries. Incluso el modelo actual de los
frameworks MVC no debe confundirse con las entidades de nuestra
arquitectura.
Ahora los modelos de MVC deben obtener los datos que se generan en
el Interactor. No son más que estructuras de datos que salen de la
arquitectura, pero no tienen que tener relación con las entities.
Y debe ser precisamente el Interactor o Use Case el encargado de
recibir un Request model (una estructura de datos) y responder con un
Response Model o respuesta, otra estructura de datos:
Esa respuesta sale de la arquitectura y en un framework MVC o en un
Model View Presenter, el response Model se adecúa para las vistas de la Web
si es el caso.
La relación con MVC más al detalle
Si lo que tenemos fuera de la arquitectura es un MVC, en ningún caso
pueden ir los datos en dirección opuesta y lo que se utilizará como boundaries
serán interfaces.
El Controller es quien genera un Request Model que debe pasar a través
del interface del Boundary.
Si tenemos un Presenter, este debe implementar el Boundary y
encargarse de procesar un Response Model a través de ese interface.
6.6 Base de
datos
¿Como encaja la BD en nuestra arquitectura? A primera vista,
podríamos esperar que los Entities vayan a ser persistidos en la BD o que por
ejemplo, si usamos Hibernate, las Entities provengan de la BD. Pero no es
así. Ojo, las bases de datos contienen estructuras de datos, y NO objetos de
negocio.
Necesitaremos por lo tanto otro Boundary para poder interactuar con la
BD. En este caso les llamaremos Entity Gateway. Es decir, aplicamos el
mismo principio que hemos usado para separar el MVC para interactuar con
la BD:
La BD, sin duda, contendrá datos que será utilizada tanto por los
Interactors y Entities. Pero cuidado, la forma en que los datos están en la BD
no tiene porqué coincidir con el tratamiento que se le da en la arquitectura.
Por eso se hace esta separación. Al igual que se hace con la interacción del
MVC, todas las dependencias deben apuntar desde fuera hacia dentro.
¿Quién accede a la BD desde la arquitectura?
Es el Interactor o Use Case el que se encarga de usar la interface que
nos abstrae de la BD, pero que en definitiva hace uso de ella.
6.7.1 Clean
Bank
Se trata de una aplicación que permite a los clientes de un banco
interactuar con su cuenta, conocer su estado, ingresar dinero y sacar dinero.
Se trata de una aplicación multiusuario que requiere los siguientes casos de
uso:
1. Identificar al usuario
2. Sacar los detalles de un usuario y su cuenta (que es única
por usuario)
3. Meter dinero en la cuenta
4. Sacar dinero de la cuenta
Veamos ahora cada uno de los componentes de las particiones de la
arquitectura.
6.7.2 Las
entidades
En este caso, definimos dos entidades muy claras. User y Account Un
usuario tiene una cuenta y la cuenta dispone de métodos para poder meter o
sacar dinero de ella.
[Link]
public class User {
private String login;
private String password;
private String name;
private Account account;
public User(String login, String password, String name, Account account) {
[Link] = login;
[Link] = password;
[Link] = name;
[Link] = account;
}
// getters/setters ...
}
[Link]
public class Account {
private String accountNo;
private Float balance;
public Account(String accountNo, Float balance) {
[Link] = accountNo;
[Link] = balance;
}
public String getAccountNo() {
return accountNo;
}
public void setAccountNo(String accountNo) {
[Link] = accountNo;
}
public Float getBalance() {
return balance;
}
public void deposit (Float money) {
balance = balance + money;
}
public void withdraw (Float money) {
balance = balance - money;
}
}
6.7.3 Los
casos de
uso
Tenemos cuatro casos de uso muy concretos, que convertimos en clases
muy sencillas que directamente utilizan las entidades. Como veremos,
también hacen uso de interfaces que les conectan a elementos externos, como
la BD y la interacción con el usuario.
[Link]
Se ocupa de la identificación del usuario.
import [Link];
import [Link];
import [Link];
public class Login implements LoginBoundary {
private UserEntityGateway userEntityGateway;
public boolean login (String userName, String password) {
return [Link](userName, password);
}
@Inject
public void setUserEntityGateway (UserEntityGateway userEntityGateway) {
[Link] = userEntityGateway;
}
}
[Link]
Se ocupa de sacar toda la información del usuario, incluyendo su cuenta
asociada
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
public class UserDetail implements UserDetailBoundary {
private UserEntityGateway userEntityGateway;
public User find (String userName) {
return [Link](userName);
}
@Inject
public void setUserEntityGateway (UserEntityGateway userEntityGateway) {
[Link] = userEntityGateway;
}
}
[Link]
Se ocupa de hacer un ingreso en la cuenta
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
public class DepositMoney implements DepositMoneyBoundary {
private AccountEntityGateway accountEntityGateway;
public Float deposit (Account account, Float money) {
[Link](money);
[Link](account);
return [Link]();
}
@Inject
public void setAccountEntityGateway(AccountEntityGateway accountEntityGateway) {
[Link] = accountEntityGateway;
}
}
[Link]
Se ocupa de sacar dinero de la cuenta
package [Link];
import [Link];
import [Link];
import [Link];
import [Link];
public class WithdrawMoney implements WithdrawMoneyBoundary {
private AccountEntityGateway accountEntityGateway;
public Float withdraw (Account account, Float money) {
[Link](money);
[Link](account);
return [Link]();
}
@Inject
public void setAccountEntityGateway(AccountEntityGateway accountEntityGateway) {
[Link] = accountEntityGateway;
}
}
6.7.4 Boundaries
Estas son los interfaces que nos permiten conectarnos a elementos del
exterior, que son quienes los implementan.
[Link]
package [Link];
import [Link];
public interface LoginBoundary {
public boolean login (String userName, String password);
}
[Link]
package [Link];
import [Link];
public interface UserDetailBoundary {
public User find (String userName);
}
[Link]
package [Link];
import [Link];
public interface DepositMoneyBoundary {
public Float deposit (Account account, Float money);
}
[Link]
package [Link];
import [Link];
public interface WithdrawMoneyBoundary {
public Float withdraw (Account account, Float money);
}
6.7.5 Gateways
de
entidades
Si queremos permitir que nuestras entidades se puedan persistir de
alguna forma, por ejemplo, en una BD relacional, también debemos definir
unos interfaces que será implementados desde fuera de la arquitectura.
Definimos un interface por cada una de las clases Entity, con aquellas
operaciones que precisen los casos de uso.
[Link]
package [Link];
import [Link];
public interface UserEntityGateway {
public boolean checkUser (String userName, String password);
public User find (String userName);
}
[Link]
package [Link];
import [Link];
public interface AccountEntityGateway {
public Account find (Integer customerId);
public int update (Account account);
}
7 PRINCIPIOS
SOLID
7.1 Single
Responsibility
Una clase de tener una única responsabilidad dentro de todo el
proyecto, de tal manera que se facilite la cohesión entre clases. Si hay algo
que cambiar en la clase, debería existir una única razón para hacerlo.
¿Qué ocurre si una clase tiene más de una responsabilidad? Si se
producen cambios, es probable que tengan influencia sobre otras
funcionalidades, por lo que siempre debemos procurar separar las
responsabilidades. Una clase que asume muchas responsabilidades acaba
acoplando las responsabilidades y es mucho más sensible a los cambios.
Supongamos que tenemos una clase que se utiliza para convertir un
fichero CSV a un texto JSON. La clase tiene la responsabilidad de abrir y leer
el fichero y además hacer la conversión de formato.
7.2 Open/Closed
Una clase, debe estar abierta a las extensiones, pero cerrada para las
modificaciones. La definición se refiere en general a cualquier entidad de
software como clases, módulos y funciones.
En principio, a finales de los 80, se proponía llevar a cabo esa extensión
a través del mecanismo de herencia, mediante la creación de subclases. Más
adelante se proponía aplicar este principio mediante el uso de interfaces
abstractas, de las que se podían crear múltiples implementaciones.
Se trata, en definitiva, de que se pueda extender la funcionalidad de una
pieza de software sin tocar su código.
Un ejemplo
Veamos un ejemplo muy sencillo, tenemos una clase crea mensajes
secretos, y hace uso de otra para usar un cifrado concreto:
[Link]
public class SecretMessage {
private String message;
private String from;
private String to;
public SecretMessage(String from, String to) {
[Link] = from;
[Link] = to;
}
public String encrypt () {
Base64Encryption encrypter = new Base64Encryption();
String textToEncrypt = "From: " + from + "\n";
textToEncrypt = "To: " + to + "\n";
textToEncrypt = "Message: \n" + message + "\n";
return [Link](textToEncrypt);
}
public String decrypt () {
Base64Encryption encrypter = new Base64Encryption();
return [Link](message);
}
public void setMessage(String message) {
[Link] = message;
}
}
7.3 Liskov
Substitution
Los objetos de un programa deben poder ser reemplazables por sus
subtipos sin alterar el correcto funcionamiento de ese programa. En un
programa, los objetos de determinado tipo se deberían poder intercambiar por
objetos que sean su propio subtipo.
Por ejemplo, supongamos que tenemos una clase que representa un
cliente llamada Customer y una subclase llamada VipCustomer:
Y supongamos que, en nuestro programa, aplicamos un descuento a un
cliente en el caso de que ese día coincida con su cumpleaños. Según el
principio de sustitución, daría igual que el objeto fuera tanto de tipo
Customer o de VipCustomer.
A la hora de escribir código, el principio de Liskov requiere que se
cumplan los siguientes requerimientos en los métodos:
Contravarianza en los argumentos de métodos en el subtipo.
Covarianza en el tipo de retorno de los subtipos.
No lanzar nuevas excepciones en los métodos de los
subtipos.
A este principio también se le conoce como diseño por contrato.
[Link]
7.4 Interface
Segregation
Es preferible tener muchos interfaces específicos de clientes que un
único interface de propósito general. Dicho de otra manera, ningún cliente
debería depender de métodos que no esté utilizando.
La segregación de interfaces promueve la separación de un interface
grande en interfaces más pequeños que se ajusten mejor a clientes
específicos.
Supongamos que una televisión tiene una única clase para llevar a cabo
multitud de operaciones como: ver canales de TDT, grabación, reproducción
de DVD o distintos inputs, configuración, etc. y agrupamos todas esas
funcionalidades en una única clase. Sin duda tendría un tamaño que haría su
mantenimiento muy complicado.
[Link]
public class RssReader {
public ArrayList<Article> loadNews (String url) {
…
return new ArrayList<Article>();
}
}
Invirtiendo la dependencia
Para que la clase NewsClient no depende de una clase de bajo nivel, lo
que vamos a hacer es que, en lugar de utilizarla directamente, NewsClient
defina un interface, es decir, una abstracción, llamada NewsReader:
public interface NewsReader {
public ArrayList<Article> loadNews (String url);
}
8.1 Eliminate
waste
Este principio afirma que todo aquello que incluyamos en el proyecto
y que no aporte valor al usuario debe ser considerado como un
desperdicio. Por ejemplo, a veces los desarrolladores se empeñan en añadir
funcionalidades extra que no han sido requeridas y esto no tiene que ser
positivo. Se consideran desperdicios entre otros:
1. El trabajo parcial o incompleto
2. Los procesos añadidos
3. Las funcionalidades añadidas
4. El cambio de tareas
5. La espera
6. El movimiento
7. Los defectos
8. Actividades de gestión
En el caso específico del desarrollo de software, se consideran como
desperdicios:
1. Desarrollar una funcionalidad o producto incorrecto
2. No gestionar bien el backlog
3. Volver a hacer el mismo trabajo
4. Desarrollar soluciones innecesariamente complejas
5. Carga cognitiva
6. Estrés psicológico
7. Espera/Multitarea
8. Perdida de comunicación
9. Comunicación inefectiva
8.2 Amplify
learning
Tal y como promueven las métodologías ágiles, el propio desarrollo de
software debe verse como un proceso de aprendizaje continuo, basado en
iteraciones en las que se produce código. El propio diseño del software es
otro proceso en el que los desarrolladores escriben código basado en lo que
asimilan. En definitiva, se consigue dar valor al software gracias a que se
ajusta al uso que se precisa.
En lugar de perder el tiempo con la clásica documentación de análisis
de requerimientos, tras arduas reuniones, las ideas se plasman directamente
escribiendo código y mostrando al usuario final pantallas para que lo pruebe,
recibiendo un feedback inmediato y mucho más valioso. En esas reuniones o
demos, los desarrolladores se someten a un aprendizaje permanente tanto del
dominio como de las necesidades del cliente, pudiendo así satisfacer mucho
mejor y de una forma directa y real las necesidades de los clientes.
Ese proceso se debe hacer en ciclos de desarrollo cortos en lo que
deben aplicarse refactorizaciones y testing, tanto unitarios como de
integración, de tal manera que se prevengan errores.
Se promueve, por tanto, escribir y reescribir código en todo momento,
pero lejos de suponer un riesgo, es un proceso perfectamente controlado y
verificado a través de los test unitarios.
8.3 Decide
as late
as
posible
Cualquier desarrollo de software siempre tiene una parte de
incertidumbre, y resulta peligroso tomar decisiones desde el inicio; más
aún cuando el proyecto puede evolucionar y hacer que esas decisiones,
además de poder tener un coste, no tengan sentido.
En una aproximación en pequeños ciclos, el proyecto se va ajustar
mucho mejor a los hechos que a predicciones y se va a desarrollar una
solución mucho más flexible que permite que esas decisiones críticas
puedan posponerse. Además, permite probar y evaluar distintas opciones
que facilitan dar forma de forma progresiva y una toma de decisiones más
acertada.
Por ejemplo, ante la creación de un nuevo proyecto, uno puede pensar
que va a necesitar determinadas herramientas: un framework, un sistema
gestor de bases de datos concreto, etc. ¿realmente es necesario que esas
decisiones se hagan desde el primer minuto, con la previsión de que van a ser
necesarios? Pongamos por ejemplo un proyecto de aplicación
multiplataforma. Existen, hoy en día, herramientas variadas para crear una
aplicación que sirva tanto para navegadores como para móviles. Si se decide
desde el inicio que debe existir una BBDD relacional o una NoSQL, puede
resultar que al final no se ajuste a las necesidades del proyecto.
8.4 Deliver
as fast
as
posible
En la última década, poco después de la resaca de la burbuja de
Internet, una gran mayoría de las aplicaciones que se desarrollan son para la
web o para dispositivos móviles cuyos usuarios se cuentan por millones.
Las aplicaciones de hoy en día precisan estar sometidas a un
desarrollo y revisión continuo, un ciclo que se alimenta por el feedback de
los propios usuarios, la competencia, la competición y adaptación a nuevas
necesidades.
Una gestión de proyectos tradicional con unos pasos en cascada muy
rígidos donde se pone gran esfuerzo en un análisis y diseño y el usuario no ve
el resultado hasta el final, no tiene sentido en este tipo de proyectos. Resulta
evidente, por tanto, que nos encontramos en un escenario en el que el primero
en llegar es quien triunfa.
La rapidez en la entrega de un producto permite además recibir más
feedback. La organización y desarrollo de un proyecto se puede hacer a
través de pequeñas tareas que se asumen en cada iteración o ciclo de
desarrollo. Tal y como se propone en Scrum, basta con una rápida reunión
diaria para hacer el seguimiento añadiendo comunicación y transparencia a
todo el proceso.
En cierto modo, se trata de aplicar el principio de Just-In-Time
Manufacturing de la industria al desarrollo de software.
8.5 Empower
the team
Tradicionalmente, en las empresas existe la figura de un gestor que es
quién dice a los empleados qué es lo que deben hacer y asume la toma de
decisiones. Esto forma de trabajar deja de tener sentido con Lean y las
métodologías ágiles, ya que es el propio equipo quien asume su
organización, reparte sus tareas y se autogestiona de manera horizontal.
Lo que se necesita es formar un equipo de buenos profesionales
capaces, a los que simplemente se debe dejar hacer su trabajo. Quienes
forman parte del equipo, deben dejar de considerarse como un recurso, a la
par que un servicio de hosting o un servidor. Para que el desarrollo lleve a
buen puerto conviene contar con un grupo motivado, con tareas y metas
asumibles y realizables, siempre en contacto con el cliente.
También es clave la figura de un líder de equipo, que no debe ser un
simple gestor, debe proteger en cierto modo al grupo y velar porque la
métodología se siga aplicando, mitigando las injerencias externas.
8.6 Build
integrity
in
Todo el proyecto puede estar formado por distintos componentes que
deben funcionar de manera coordinada, dando al cliente/usuario una
imagen de conjunto robusta. La integridad del proyecto debe oscilar entre
la flexibilidad y la facilidad de su mantenimiento. ¿Cómo se consigue esa
eficiencia?
Una vez más, con un contacto constante con el cliente y sus
requerimientos y constantes ciclos de desarrollo, donde la información
fluye en ambas direcciones en pequeñas dosis, no en interminables
especificaciones escritas en un documento de análisis de requerimientos. Las
métodologías tradicionales no encajan bien en este ámbito, ya que se basan
en ciclos muy rígidos en los que no hay contacto con el cliente desde la
recogida de requerimientos a la presentación, donde pueden producirse
multitud de sorpresas y desengaños.
Obviamente, un desarrollo ágil, en el que continuamente se cambia el
código y pretende dar respuesta inmediata a nuevos requerimientos a corto
plazo, precisa de una serie de herramientas para crear un producto fiable:
Por un lado, se debe aplicar refactorizaciones para que ese
código no se ofusque y sea más fácilmente mantenible.
En consecuencia y a la vez, deben aplicarse tests unitarios
y de integridad en todo momento para asegurar de que todo
aquello que se modifica sigue funcionando, y desde luego
cualquier nuevo desarrollo debe ir acompañado de sus
correspondientes tests.
Y para controlar que la aplicación funciona de forma global,
no solamente a nivel de test unitario, pueden aplicarse
sistemas de integración continua que de forma
automatizada sometan al proyecto a pruebas, y lo preparan
para ser desplegado de forma directa.
1.1. Pasos
comunes
A lo largo de los años se han ido inventando distintas métodologías de
desarrollo, pero todas ellas comparten una serie de pasos comunes para
abordar un proyecto de software. Estos son los pasos clásicos bien conocidos:
Análisis
Diseño
Implementación
Pruebas
Pueden existir algunas variantes, como por ejemplo la Planificación
previa al Análisis, o el Mantenimiento posterior a las Pruebas. Vamos a
desglosar un poco cada uno de estos pasos para tener claro sus propósitos
básicos.
Todo proyecto se debe poner en marcha en el momento que alguien
tiene la necesidad de un software. Un cliente, una empresa u otro
departamento necesitan un programa. Aplicando una métodología de
desarrollo debemos seguir unos pasos mucho antes de ponernos a teclear
código. Lo primero de todo, el usuario nos debe transmitir lo que necesita.
1.1.1. Análisis
Recogida de requerimientos
Acotar el problema
Comprender el dominio
Identificar funciones, conceptos y sus relaciones
En definitiva, se trata de enterarnos de qué va la cosa. Tratamos de
responder a una pregunta ¿Qué? ¿Qué es lo que nos están pidiendo? ¿De qué
va esto?
1.1.2. Diseño
Estructuras de datos
Arquitectura de software
Detalles procedimentales
Representaciones de interfaz
1.2. Modelos
de
desarrollo
Sin embargo, muchas veces es necesario hacer estos mismos pasos de otra
manera. Es tan simple como que muchas veces necesitamos dar marcha atrás
porque, por ejemplo, en pleno diseño nos damos cuenta de que parte del
análisis no está claro, o porque al programar el diseño sobre el que nos
basamos no es del todo preciso.
Estas son otras formas de llevar a cabo el proyecto aplicando los mismos
pasos de distinta manera:
1.2.1. El
modelo
en
cascada
Es el modelo de cuatro pasos en el que en el que no podemos iniciar
un paso sin haber terminado el anterior. Si bien es un modelo muy rígido
todavía puede tener aplicación según el tipo de proyectos.
1.2.2. El
modelo
de
cascada
1.2.4. El
modelo
en
espiral
El modelo en espiral consiste en repetir los pasos una y otra vez añadiendo
cada vez una mayor funcionalidad al proyecto. Se parte de una pequeña base
y conforme se completa un ciclo se aborda una parte más extensa del
proyecto.
2. DOCUMENTACIÓN
DE PROYECTOS
2.1. El
lenguaje
UML
UML o Universal Modeling Language fue desarrollado en la década de
los 90 como un lenguaje común para poder crear modelos de cualquier
ámbito, no solo de Ingeniería del Software. UML se ha convertido en un
estándar aceptado por todos los fabricantes y comunidad de desarrolladores
del mundo y está plenamente aceptado sea cual sea el lenguaje y la
métodología que se utilice.
UML define una serie de diagramas y normas que permiten crear modelos
de distintas etapas y ámbitos de un proyecto de software: desde los requisitos
o casos de uso, las clases y sus relaciones, la interacción entre componentes,
etc. Algunos de esos diagramas corresponderían a la fase de análisis y otros a
las de diseño. En cualquier caso, son esquemas visuales que ayudan a
entender mejor el proyecto.
2.2. Programas
para
dibujar
diagramas
UML
Existen distintos programas para poder diseñar diagramas UML, desde
los sencillos, libres y gratuitos, hasta las herramientas de pago que incluso
permiten generar código a partir de diagramas y la ingeniería inversa.
2.2.1. UMLet
[Link]
Esta sencilla herramienta está desarrollada íntegramente en Java por lo
que podemos ejecutarla como programa standalone en cualquier plataforma
que tenga la JVM. Por otro parte también dispone de un plugin de Eclipse
que se puede instalar simplemente copiando el fichero de plugin en el
directorio plugins de eclipse. El manejo del mismo es bastante simple y si lo
único que necesitamos es hacer diagramas esta sin duda sería muy buena
opción.
Los diagramas de este documento están hechos en su gran mayoría con
UMLet.
También dispone de una versión online:
[Link]
2.2.1. ObjectAid
UML
Sencillo plugin de Eclipse que genera diagramas UML a partir de las
clases del proyecto.
Los pasos de instalación son los siguientes:
[Link]
2.2.2. Eclipse
UML
Designer
[Link]
Eclipse dispone de más de una herramienta o plugin para UML. Entre
las que tienen licencia EPL (Eclipse Public License) tenemos Eclipse UML
Designer la cual permite crear diagramas desde 0 y también otra opción
importante: genera el diagrama a partir del código existente.
2.2.3. Rational
Modeler
[Link]
Esta herramienta de IBM dispone de una versión gratuita y otra de
pago. Para descargarse la versión gratuita hay que darse de alta en la web de
IBM.
2.2.4. Enterprise
Architech
[Link]
Una herramienta profesional y de pago, pero sin duda de las más
completas y con infinidad de funcionalidades que incluso van mucho más allá
de UML la generación de código a partir de los diagramas. Quizá por ello su
uso no sea tan evidente como otros programas.
2.2.5.
Microsoft
Visio
[Link]
2.2.7. [Link]
[Link]
Esta herramienta no es tan sencilla, pero crea unos efectos un poco
distintos a los habituales.
3. DIAGRAMAS
DE CASOS
DE USO
3.1. Toma de
requerimientos
El objetivo principal del desarrollo de software, sea el que sea, no es
otro que satisfacer las necesidades demandadas por el cliente. Algo que
parece muy obvio pero que conviene no perder de vista en ningún momento.
Por ese motivo desde el principio debe aclararse y poner en claro qué es lo
que el programa debe hacer no solo de forma general sino de forma más
concreta a través de las operaciones que tendrán que realizar los futuros
usuarios con ese software.
Todas esas demandas se recopilan en la toma de requerimientos, que es
una de las primeras fases del desarrollo del proyecto a veces considerada de
forma independiente y otras veces como parte del análisis. Básicamente se
trata de apuntar y acordar una serie de funcionalidades y capacidades que se
esperan del programa. Esta lista de demandas permite varias cosas:
1. Establecer de forma clara y consensuada los objetivos
2. Permitir hacer estimaciones y planificación en base a las tareas
3. Sirve para controlar y limitar el trabajo a realizar
Los requerimientos deben trabajarse directamente con el cliente y se
deben completar de forma conjunta. El hecho de que queden apuntados
permite que las responsabilidades queden acotadas para los desarrolladores y
que las funcionalidades no vayan más allá de lo establecido para los clientes.
3.2. Los
diagramas
de casos
de uso
UML define unos diagramas que permiten reflejar de forma gráfica y
fácilmente entendible los requerimientos de un programa. De hecho, es un
diagrama que podrían ver las dos partes implicadas en el proyecto. Los
diagramas de casos de uso muestran las funciones que deben realizar el
programa y qué usuario es el que las debe llevar a cabo.
Un caso de uso se representa con una elipse cuyo texto define la
función del mismo:
Para indicar que ese actor en concreto será quien utilice esa función o caso de
uso, simplemente los unimos con una línea:
3.2.1. Más
de un
caso
de
uso
Generalmente tendremos más de un caso de uso. El diagrama debe
cubrir, a grandes rasgos, todas las funcionalidades del sistema o software que
debemos desarrollar.
3.2.2.
Uno o
más
actores
A veces, dependiendo del programa que se debe desarrollar y de los
requerimientos del mismo podremos tener más de un actor. Es muy normal
que un software sea utilizado por distintos perfiles de usuario: unos pueden
ser usuarios simples, otros pueden tener más permisos, otros pueden tener
acceso a otras funcionalidades, etc. veamos un ejemplo.
3.2.3. Extensión
de
Actores
Es habitual que un mismo software tenga distintos niveles de acceso
en los que a determinados usuarios se les permite hacer más cosas que a
otros. En ese escenario, los usuarios con más privilegios pueden hacer lo
mismo que el resto de usuarios más otros casos de uso restringidos solo a
ellos. En lugar de repetir todas las relaciones, podemos resumirlas
extendiendo los actores para indicar que un actor “hereda” los casos de uso
de otro.
Por ejemplo, supongamos que tenemos un software para controlar el
stock de un almacén. A través de ese software, los clientes deben ser capaces
de comprobar el stock. Los operarios de almacén deben poder comprobar el
stock y además gestionarlo. Tenemos que indicar que existe un usuario que
puede hacer lo mismo que el operador y además puede gestionar los usuarios
del sistema. Eso lo indicamos con la flecha de extensión:
3.2.4.
Extensión
y
utilización
de casos
de uso
En ocasiones puede haber casos de uso relacionados entre si, bien
porque uno usa al otro o bien porque uno extiende la funcionalidad de otro
caso de uso. Ese tipo de relaciones pueden ser reflejadas en el diagrama.
Includes: supongamos que un caso de uso tiene que hacer
determinadas acciones, una de las cuales es precisamente otro caso de uso.
Podemos reflejar esa relación entre los dos casos de uso con una flecha
indicando la palabra <<includes>>. Por ejemplo, en el software de control de
stock podríamos tener un caso de uso para que un cliente pidiera un producto.
Dentro de esa funcionalidad podría ser necesario notificar la petición con un
email que a su vez es otro caso de uso de forma independiente.
3.2.5.
Use Case
Overview
A veces puede resultar interesante mostrar un diagrama que resuma a
grandes rasgos cómo interactúan los distintos Actores con el sistema. En
proyectos grandes, donde incluso puede haber más de un Sistema o
subsistemas, podemos representar la relación de los actores con esos
sistemas. Este sería un diagrama en el que no se representan casos de uso en
concreto, se trata más bien de una vista general de todo el proyecto.
Supongamos que tenemos una aplicación tipo Amazon donde además
de vender cosas dejamos espacio para que otras tiendas tengan su espacio. En
un proyecto así existirían seguramente varios sistemas independientes entre sí
que los actores usarían de forma transparente. Podríamos distinguir por
ejemplo el sistema de tienda y el sistema de pagos.
Este podría ser un Use Case Overviews para representar cómo se
relacionan los distintos actores con el sistema.
3.3. Ejercicios
3.3.1. Ejercicio
1
Dibuja un diagrama de casos de uso para los siguientes requerimientos.
Un programa muy sencillo para que un usuario haga conversiones de Euros a
Dólares y de Dólares a Euros.
3.3.2. Ejercicio
2
Dibuja un diagrama de casos de uso para los siguientes requerimientos.
Un programa para gestionar un parking donde el responsable del parking
debe ser capaz de dar de alta coches que entran, borrarlos, consultar la
disponibilidad de plazas y listar el estado del parking.
3.3.3. Ejercicio
3
Dibuja un diagrama de casos de uso para los siguientes requerimientos.
Un programa para gestionar un videobank en el que el administrador debe
ser capaz de dar de alta películas, darlas de baja y gestionar usuarios. Los
clientes deben ser capaces de darse de alta, alquilar películas y devolver
películas.
3.3.4. Ejercicio
4
Dibuja un diagrama de casos de uso para los siguientes requerimientos.
Un programa para gestionar un club deportivo donde el responsable debe ser
capaz de gestionar las pistas, consultar la disponibilidad de pistas, reservar
pistas a determinada hora, liberar cualquiera de las reservas y gestionar
usuarios. Los usuarios de las pistas deben ser capaces de consultar
disponibilidad de pistas, reservarlas y liberar sus reservas.
3.3.5. Bibliografía
Learning UML 2.0. Chapter 2. Modeling requirements: Use
Cases
[Link]
4. DIAGRAMAS
DE
ACTIVIDAD
4.1. Introducción
Una parte fundamental del análisis de un proyecto es la de comprender
qué pasos se deben llevar a cabo en el programa para conseguir determinado
objetivo. Esto no se refiere a sentencias concretas ni mucho menos sino a los
procesos generales que debe seguir el proyecto. Si bien el diagrama de casos
de uso nos dice qué debe hacer el proyecto, un diagrama de actividad nos
dice cómo son los procesos de negocio. Esto nos permite entrar en un nivel
más de detalle para comprender el dominio.
Por ejemplo, entre los casos de uso puede que exista uno llamado alta
de usuario, lo cual no deja de ser un requerimiento que no da muchos
detalles. Es probable que el alta de usuario lleve consigo un proceso con
ciertas pautas e incluso variantes:
7. Se recibe un alta de usuario
8. Se valida que el nombre cumple unos requisitos
9. Se comprueba que el usuario no existe
10. Si no existe:
a) Se añade el usuario
b) Se añaden sus permisos
11. Si existe se notifica el rechazo
Este podría ser un proceso relativamente sencillo y común en muchos
proyectos. Pero lo más común es que haya que analizar procesos de negocio
mucho más complejos.
4.2. Diagramas
de
Actividad
UML define los diagramas de actividad que nos permiten representar
gráficamente los procesos que debe llevar a cabo nuestro sistema. Los
diagramas son muy similares a los clásicos diagramas de flujo, donde se
encadenan una serie de pasos con algunas posibles alternativas condicionales.
UML simplifica enormemente este tipo de diagramas.
Como se puede ver es muy sencillo, siempre con su peculiar inicio y fin
de UML y unas flechas uniendo cada paso.
Por ejemplo, si quisiéramos representar el proceso que debe seguir un
sistema de cesta de la compra para hacer un pedido podría ser algo así:
4.3. Decisiones/Condicionales
Los procesos no suelen ser tan directos. Muchas veces existen
alternativas porque pueden surgir casos o excepciones donde hay que variar
el comportamiento del sistema. Una forma de representar esa condición es
con un rombo:
Si los caminos del proceso se bifurcan, más
adelante pueden unirse utilizando el mismo
símbolo:
4.5.1. Ejercicio1
Dibuja un diagrama muy sencillo en el que se muestre el proceso en el
que un usuario solicita un menú en un McDonald’s. Primero selecciona
hamburguesa, luego selecciona bebida y luego selecciona patatas. No hay
alternativas.
4.5.2. Ejercicio2
Crea un diagrama que muestre el proceso de cambio de contraseña en
el sistema. Primero se solicita el nombre de usuario. Luego se solicita una
contraseña. Mientras la contraseña no sea correcta no se sigue adelante.
Luego se solicita introducir la nueva contraseña dos veces. Mientras no
coincidan se vuelve a introducir. Una vez que coincidan se muestra un
mensaje al usuario indicándole que el cambio se ha hecho correctamente.
4.5.3. Ejercicio3
Dibuja un diagrama que muestre el proceso que sigue una máquina
expendedora cuando se introduce dinero y se selecciona un producto: el
usuario introduce dinero, elige un producto, si el importe no es suficiente se
le indica que introduzca más. Si el dinero es suficiente se sirve el producto y
a continuación si se introdujo dinero de más se devuelven los cambios. No
hay más casos alternativos.
4.5.4. Ejercicio4
Dibuja un diagrama que muestre el proceso que sigue un alumno del
ciclo de Desarrollo de Aplicaciones informáticas hasta que completa sus
estudios. Estudia primer curso, si suspende alguna hace un examen de
recuperación en junio, si suspende otra vez hace un examen de recuperación
en septiembre. Si Suspende vuelve a hacer primero. Si aprueba primero pasa
a segundo curso. En segundo sigue un proceso similar. Si suspende alguna
hace un examen de recuperación en marzo. Si suspende marzo hace una
recuperación en junio. Si suspende repite segundo, si aprueba consigue el
título.
4.5.5. Ejercicio5
Dibuja un diagrama que muestre el proceso de alta de un usuario en un
sistema. Primero se solicita un usuario, luego una contraseña y tras eso
comienzan dos pasos en paralelo:
Por un lado, se crea un nuevo usuario
Se añade el nuevo usuario en la lista de control de acceso. Se le
asigna el rol por defecto.
Al terminar los dos pasos paralelos se manda al usuario a la página de
inicio.
4.5.6. Bibliografía
Learning UML 2.0. Chapter 3. Modeling system workflows:
Activity Diagrams
Aprendiendo UML en 24 horas. Capítulo 11. Diagramas de
actividad
5. DIAGRAMAS
DE CLASES
5.1. Introducción
En la fase de análisis se trata de conocer los requerimientos del cliente
y lo que es igual de importante, conocer el dominio. Es decir, se trata de
entender qué es lo que nos están pidiendo. El análisis nos va a llevar de
manera irremediable a:
1. Identificar, concretar y detallar las clases que forman parte del
dominio
2. Darnos pie a cómo vamos a dar una solución con esas clases
A la hora de representar las clases se pueden representar con dos tipos
distintos de diagramas según estemos en la fase de análisis o de diseño.
1. Diagrama de modelo de dominio: también llamados
conceptuales, es un diagrama de clases ligado a la fase de análisis
ya que simplemente trata de mostrar qué clases existen, qué
atributos tienen y qué relación tienen entre sí.
2. Diagrama de Implementación: es un diagrama más ligado a la
fase de diseño ya que da más detalles sobre las clases y sus
métodos, y en más concreto las relaciones.
El diagrama de modelo de dominio trata de describir el dominio, el
problema, dicho de forma resumida: EL QUÉ.
El diagrama de implementación trata de acercase a la solución del
problema, al diseño, en definitiva: EL CÓMO. El cómo es algo de lo que se
encarga la fase de Diseño. En la fase de diseño es donde podríamos detallar
cómo son esas clases que vamos a necesitar, qué atributos tienen, qué
métodos, qué accesibilidad, y lo que es casi más importante, qué relación
tienen entre si esas clases.
A pesar de ser dos diagramas diferenciados comparten la mayoría de
elementos para crear los diagramas.
5.2. Diagrama
de
Modelo
de
Dominio
Estos diagramas muestran las clases que se identifican en el problema
que se nos propone.
5.2.1. Una
clase en
UML
La representación de una clase en UML es tan simple como un
rectángulo con el nombre de la clase en su interior.
5.2.3. Relaciones
En el diagrama de modelo también podemos expresar que relaciones
existen entre las clases que hemos identificado. ¡OJO! Simplemente estamos
en el análisis y por tanto indicando QUÉ es lo que hay, todavía no estamos
pensando en la solución o el programa.
Supongamos que en el enunciado se nos dice lo siguiente:
El parking tiene un par de barreras, una para la entrada y otra para
la salida
El parking tiene tres plantas
Cada planta tiene 50 plazas disponibles
Cada plaza puede tener uno o ningún coche.
Esto con las relaciones podría quedar así:
5.3. Diagramas de
Implementación
Son básicamente como los diagramas de modelo de dominio, solo que
se detallan los métodos y el diagrama describe lo que será el diseño final de
la solución. Por tanto, cada uno de los diagramas se muestra junto a su código
correspondiente.
5.3.1. Una
clase en
UML
La representación de una clase en UML es la misma que en el diagrama
de modelo de dominio:
Estos sería una clase de forma muy resumida, algo que puede resultar
útil en un diseño inicial o una visión general del sistema. Pero se puede hacer
mucho más concreto.
En código podría representarse así:
public class Car {
5.3.2. Atributos
y
métodos
Las clases se componen de atributos y métodos, y tal cual se incluyen
en el cuadro de la clase. Esta sería una forma informal de representar una
clase Car con el atributo model y los métodos start y stop:
Una forma mucho más formal sería indicando los tipos, la visibilidad,
los parámetros y los retornos. Lo primero que se incluye es la visibilidad con
un sencillo símbolo:
+ : atributos o métodos públicos
- : atributos o métodos privados
# : atributos o métodos protegidos
En cuanto a los tipos, al contrario que en Java y la mayoría de lenguajes
de programación, el tipo se incluye tras el nombre y los dos puntos:
+ model: String
Lo mismo se trasladaría a los métodos, tanto para los parámetros como
para el tipo de retorno, que se indicaría al final del mismo. Por ejemplo, si un
método suma dos parámetros enteros y devuelve el resultado de tipo entero
en UML lo representaríamos así:
+ add (a: int, b: int): int
Vamos a ver la clase Car de forma más formal:
5.4. Relaciones
Cualquier proyecto desarrollado con programación orientada a objetos
tiende a tener muchas clases. Tan importante como las clases en si es la
relación entre ellas. UML nos permite indicar esa relación con distintos tipos
de flecha. Nótese que en la mayoría de ejemplos se omiten detalles de las
clases (atributos y métodos) para centrarse en la relación.
5.4.1. Uso
Si una clase simplemente hace uso de otra lo podemos indicar con una
flecha normal y la palabra uses. Esta situación es muy habitual:
}
}
5.4.2. Agregación
Cuando una clase contiene elementos de otra clase puede indicarse una
relación de agregación. La clase agregada es independiente, pero puede
formar parte de otra. Un ejemplo podría ser un juego en el que un jugador
puede tener un arma o más:
5.4.3. Composición
}
Otro ejemplo sería la del velero. Un velero no sería tal sin sus velas.
Para distinguir bien entre composición y agregación se puede recordar
el ejemplo del coche y su relación con las ruedas y los pasajeros:
Un coche, para ser tal tiene que tener ruedas.
Un coche puede tener o no pasajeros, pero si no tiene pasajeros no deja
de ser un coche:
En código podría ser:
public class Car {
private Vector<Wheel> wheels;
private Vector<Passenger> passengers;
}
Otro ejemplo similar sería el de un hotel:
El Hotel se compone de habitaciones, sin ellas no tiene sentido. Sin
embargo, puede tener huéspedes… o no. Su representación en código podría
ser:
public class Hotel {
private Vector<Room> rooms;
private Vector<Guest> guests;
}
5.4.4. Herencia
La herencia es una de las características fundamentales de la
programación orientada a objetos. UML nos permite representarla con una
flecha triangular vacía. Por ejemplo, podríamos tener una clase llamada Fuel
con determinadas características, y de ella podrían heredar otras clases más
específicas:
En código:
public abstract class Fuel {
}
Clase abstracta
En el caso anterior, se puede observar que Fuel está escrito en cursiva.
Con eso estamos indicando explícitamente que esa clase es abstracta. Una
clase abstracta NO se puede instanciar directamente, pero sirve para crear
una clase común para que sea extendida por otras y así se reutilicen atributos
y métodos comunes. La superclase o clase padre no tiene por qué ser
abstracta siempre, pero suele ser habitual.
En el siguiente ejemplo también se indica que hay una clase abstracta
pero de forma explícita utilizando un estereotipo <<Abstract>>:
}
protected void fillTank () {
}
}
[Link]
public class Car extends Vehicle {
private String plate;
public void accelerate () {
}
public void brake () {
}
public void turn () {
}
}
[Link]
public class Train extends Vehicle {
private String type;
public void stop () {
}
public void load () {
}
}
[Link]
public class Plane extends Vehicle {
private String company;
public void takeOff () {
}
public void land () {
}
5.4.5. Implementación
Los interfaces son como clases plantilla que no tienen ningún código.
Lo único que tienen los interfaces es una declaración de métodos, con sus
parámetros y su tipo de retorno si lo hay. Una interfaz debe ser
implementada por una clase. Lo que hace esa clase no es más que declarar
esos métodos del interfaz y rellenarlos con código.
¿Qué sentido o qué utilidad tienen los interfaces? Permiten definir una
especie de contrato para interactuar entre componentes. Obliga a que las
clases tengan que interactuar entre si utilizando solo las llamadas que indica
el interfaz.
Por ejemplo, un proyecto podría dar opción a que los mensajes del
programa se gestionasen de distinta manera. El proyecto definiría un interfaz
para hacerlo e internamente haría referencia a ese interfaz. Ahora un
desarrollador podría implementar distintas maneras de gestionar esos
mensajes, concretando implementaciones. En el diagrama se aprecia como el
interfaz Logger, que sirve para indicar los métodos que se van a llamar para
guardar mensajes, es implementado de tres maneras:
Llevando mensajes a la consola: LoggerConsole
Llevando los mensajes a un fichero: LoggerFile
Llevando los mensajes a una BD: LoggerDB
5.5. Instancias/Objetos
A veces en determinados diagramas nos interesará representar las
instancias de clase u objetos concretos. Esos se también se representan con un
cuadrado, pero con un pequeño cambio: en lugar de indicar el nombre de
clase se indica el nombre de instancia ¡y se subraya!
Para dar mayor detalle a veces también se indican los valores concretos
de atributos.
Por ejemplo, supongamos que tenemos una clase llamada Player,
representada con el siguiente diagrama:
5.5.1. ¿Debemos
incluir el
constructor
y los
métodos
set/get en
el
diagrama?
En el diagrama anterior no se habían incluido algunos métodos como
los setter/getter. Por lo general, este tipo de métodos se omiten en los
diagramas, salvo que el nivel de detalle que se precise sea muy alto. En cierto
modo son métodos que se dan por supuestos.
Lo mismo ocurre con el constructor por defecto. Este existe siempre y
por tanto no se suele indicar. En cambio, si existe más de un constructor o
algún constructor distinto al vacío sí que puede resultar interesante indicarlo
en el diagrama.
5.6. Paquetes
Los paquetes son unos diagramas que representan una especie de
carpeta y nos permiten agrupar o indicar que hay una agrupación de
elementos. Se representan así:
Tanto para los diagramas de clase como incluso para los de casos de uso nos
puede interesar agrupar los elementos de alguna forma, generalmente por
funcionalidad en el caso de las clases y por actores en el caso de los
diagramas de casos de uso. Por ejemplo, en una aplicación donde agrupamos
lo que es interfaz de usuario por un lado y la lógica de negocio (lo que
realmente hace la aplicación) en otro:
Otro ejemplo más completo, una aplicación para gestionar una BD de
clientes puede estar dividida en distintas áreas:
1. Configuración de la aplicación
2. GUI, clases relativas Graphic User Interface
3. Acceso a BBDD
4. Lógica de negocio
5. Modelo: clases POJO que representan los objetos de la BD
Podemos incluso ir un poco al detalle e incluir dentro del paquete las clases
que contiene:
5.7. Ejercicios
5.7.1. Ejercicio1
Crea un diagrama de modelo de dominio para el siguiente caso:
Necesito un programa que simule como un jugador tira un dado. El
dado puede tener caras variables.
5.7.2. Ejercicio2
Crea un diagrama de modelo de dominio para el siguiente caso:
Una asociación de consumidores quiere ofrecer una aplicación para
sus socios. Se trata de una aplicación que permite gestionar una lista de la
compra virtual. La idea es que el usuario elabora la lista conforme se vaya
acordando de lo que necesita.
El usuario va añadiendo productos para luego ir tachándolos en el
momento de hacer la compra. También tiene que tener la opción de ver toda
la lista de productos. Se supone que la lista no podrá contener más de 100
productos.
5.7.3. Ejercicio3
Crea un diagrama de clases para una clase llamada Customer, con los
atributos públicos String name, String surname, los atributos protegidos int
age y los atributos privados boolean single y String country. También debe
tener un constructor público vacío, un método público toString que devuelva
un String y un método privado llamado doSomething.
5.7.4. Ejercicio4
Crea un diagrama de clases para estas dos clases
4. Product: tiene los atributos privados String name, int qty, float
price y los métodos: constructor vacío público, constructor
con todos los atributos como parámetros, el método toString
público y por último un método público llamado total que
devuelve float.
5. Invoice: tiene el atributo privado String customer y los
siguientes métodos públicos: constructor con un String como
parámetro, un método llamado add con un Product como
parámetro, un método llamado remove con un int como
parámetro y un método total que devuelve un float.
(Relación: la clase Invoice está compuesta de 1 a N elementos de
la clase Product)
5.7.5. Ejercicio5
Crea un diagrama de clases para las siguientes clases:
Unit: tiene los atributos protegidos String name, int age, int
legion y los métodos públicos attack y defend que
devuelven un entero.
Subclase Princeps: tiene los atributos privados int
experience e int armor, y el método público command que
retorna un String.
Subclase Equite: tiene los atributos privados String
horseName e int food y los métodos públicos charge y ride.
Subclase Velite: tiene el atributo privado String rank y los
métodos públicos run y patrol
Legion: tiene un atributo String name y una legión se
compone entre 100 y 200 Unit
5.7.6. Ejercicio6
Crea un diagrama de clases para las siguientes clases e interfaces:
Una clase llamada Task que contenga un atributo tipo int llamado id,
otro String llamado taskText, un constructor para esos atributos y los métodos
get/set.
Un interfaz llamado ManageTasks que defina los siguientes métodos:
1. public int addTask(String taskText);
2. public void removeTask(int id);
3. public void updateTask(Task task);
4. public Task searchTask(int id);
5. public Vector<Task> showTasks();
Crea una clase llamada ManageTasksMemory que implemente el
interfaz ManageTasks
Crea una clase llamada ManageTasksBD que implemente el interfaz
ManageTasks
5.8. Bibliografía
Learning UML 2.0. Chapters 4, 5. Modeling a system’s logical
structure
Aprendiendo UML en 24 horas. Capítulo 3, 4, 5. Orientación a
Objetos, Relaciones
UML y Patrones. Capítulo 29. Paquetes, organización de los
elementos
[Link]
6. DIAGRAMAS DE
COMUNICACIÓN
6.1. El diagrama
de
comunicación
[Link]
public class TaskList {
private Vector<Task> tasks;
public TaskList () {
tasks = new Vector<Task>();
}
[Link]
public class Task {
private String taskName;
public Task(String taskName) {
[Link] = taskName;
}
}
[Link]
public class TaskList {
private Vector<Task> tasks;
...
public Task getTask(int i) {
if (null == [Link](i)) {
return null;
} else {
return [Link](i);
}
}
}
[Link]
public class Task {
[Link]
public class TaskList {
private Vector<Task> tasks;
...
public String showAll() {
String result = "";
for (Task t : tasks) {
result = result + [Link]() + "\n";
}
return result;
}
}
[Link]
public class Task {
6.2. Ejercicios
Para hacer los ejercicios vamos a partir de un mismo proyecto de ejemplo
de que tendremos que hacer distintos diagramas. El proyecto consiste en la
gestión de un parking donde tenemos las siguientes clases:
Main: clase principal que contiene el menú o el interfaz del
usuario
Parking: clase que contiene plazas de parking.
Space: clase que representa la plaza de aparcamiento.
El proyecto debe permitir llevar a cabo las siguientes operaciones: añadir
un coche a una plaza de parking, sacar un coche de una plaza de parking,
mostrar el estado de una plaza de parking y mostrar todas las plazas de
parking
6.2.1. Ejercicio1
Crea un diagrama de comunicación en la que muestre cómo se lleva
a cabo la introducción de un coche en una plaza de parking.
6.2.2. Ejercicio2
Crea un diagrama de comunicación en la que muestre cómo se lleva
a cabo la eliminación de un coche de una plaza de parking.
6.2.3. Ejercicio3
Crea un diagrama de comunicación en la que muestre cómo se
muestra el contenido de una plaza de parking.
6.2.4. Ejercicio4
Crea un diagrama de comunicación en la que muestre cómo se
muestra el contenido de todas las plazas de parking.
6.2.5. Ejercicio5
Crea un diagrama de comunicación que muestre los mensajes que
intercambian las clases (Main, Post y Blog) a partir de este pseudocódigo. Lo
que se hace es crear una instancia de la clase Post y se le pasa a una instancia
de blog.
[Link]
…
Post post = new Post();
[Link](post);
…
6.2.6. Ejercicio6
Crea un diagrama de comunicación que muestre los mensajes que
intercambian las clases (Main, Post y Blog) a partir de este pseudocódigo. Lo
que se hace es sacar una instancia de post llamando a un método de blog y
después se muestra por pantalla.
[Link]
…
Post post = [Link](1);
[Link]();
…
6.2.7. Ejercicio7
Crea un diagrama de comunicación que muestre los mensajes que
intercambian las clases (Main, Post y Blog) a partir de este pseudocódigo. Lo
que se hace es comprobar si una instancia de post existe. Si es así le decimos
a la instancia de blog que la elimine:
[Link]
…
Post post = [Link](1);
if (null != post) {
[Link](1);
}
…
6.2.8. Ejercicio8
Crea un diagrama de comunicación que muestre los mensajes que
intercambian las clases (Main, Post y Blog) a partir de este pseudocódigo. Lo
que se hace es ver las diez primeras entradas del blog.
[Link]
…
for (int i = 0; i < 10; i ++) {
[Link](i).toString();
}
…
6.3. Bibliografía
Learning UML 2.0. Chapter 8. Focusing on Interaction Links
Aprendiendo UML en 24 horas. Capítulo 10. Diagramas de
colaboraciones
[Link]
7. DIAGRAMAS
DE
SECUENCIA
7.1. Objetos
y su
línea
de vida
Los objetos o instancias se representan tal y como se describe en los
diagramas de clases. Cómo se puede ver tienen una línea y un rectángulo que
son la clave en los diagramas de secuencia:
La línea que aparece por debajo del objeto es la línea vital del objeto,
permite describir los pasos que sigue esa instancia. Estas líneas de vida tienen
periodos de activación, que es donde los objetos están en plena ejecución:
7.2.
Mensajes
Generalmente los objetos no actúan solos, y suelen mandarse mensajes
los unos a los otros. Esos mensajes no son sino llamadas a los métodos de
otros objetos. Para representar eso, simplemente ponemos esos objetos e
indicamos esa llamada con una flecha. Por ejemplo, supongamos que desde
una clase Main hacemos una llamada a [Link]():
result = [Link](333,2);
[Link](result);
}
}
7.3. Objetos
creados o
destruidos
Dependiendo del caso puede que determinado objeto sea creado a partir
de un mensaje. En ese caso, el objeto se representa desde que el mensaje ha
sido creado. Supongamos que una clase llamada GUI crea un objeto llamado
Customer:
[Link]
public class Business {
public void addCustomer (String name) {
Customer customer = new Customer(name);
}
}
Estos nuevos objetos se suelen representar cuando se generan en algún
momento relevante o cuando es como consecuencia de un mensaje. Muchos
otros objetos ya están creados desde el principio.
¿Y qué pasa si un objeto se destruye? De la misma forma que podemos
representar el nuevo objeto, una instancia que se destruye o se borra de
memoria se puede representar así:
7.4. Opcional
A veces puede que la ejecución de la secuencia no sea perfectamente
lineal ya que depende de condiciones. En los diagramas de secuencias
podemos representar las condiciones o alternativas simplemente añadiendo
un marco encabezado por esa condición.
Vamos a suponer un caso de un programa que almacena tareas. El
programa tiene una clase principal que pasa una tarea la clase que hace de
gestor de tareas. Esta a su vez, guarda la tarea en un fichero a través de una
clase gestora de ficheros. La primera vez es fichero no existirá, así que para
ese caso en el diagrama se refleja que se debe crear un fichero. Una vez
hecho, ya se puede guardar la tarea como en un caso normal:
7.5.
Bucles
En la ejecución, además de condiciones especiales o alternativas
también puede haber pasos que se tengan que repetir en un bucle.
A pesar de que este tipo de diagramas trata de representar una ejecución
secuencial, podemos indicar que hay un bucle simplemente metiendo
utilizando un marco. De esa manera, todo lo que haya contenido en el marco
se repite mientras dure el bucle.
7.6. Recursividad
Dentro de un objeto puede haber un método que se llame a sí mismo
una o varias veces. Eso es una llamada recursiva que soluciona de forma muy
elegante algunas situaciones en las que es más complejo usar iteraciones. Este
caso también puede representarse en UML utilizando una flecha que vuelve
sobre el propio objeto
El caso típico es el de programa que calcula el factorial de un número,
por ejemplo. El factorial de 5 sería:
5! = 5 * 4 * 3 * 2 * 1
En código podría ser:
[Link]
public class Main {
public static void main(String[] args) {
Factorial myFactorial = new Factorial();
int result = [Link](5);
}
}
[Link]
public class Factorial {
public int factorial (int number) {
if (number == 1) {
return 1;
} else {
return number * factorial(number - 1);
}
}
}
7.7. Ejercicios
Para hacer los ejercicios vamos a partir de un mismo proyecto de ejemplo
del que tendremos que hacer distintos diagramas. El proyecto consiste en la
gestión de un parking donde tenemos las siguientes clases:
Main: clase principal que contiene el menú o el interfaz del
usuario
Parking: clase que contiene plazas de parking.
Space: clase que representa la plaza de aparcamiento.
El proyecto debe permitir llevar a cabo las siguientes operaciones: añadir
un coche a una plaza de parking, sacar un coche de una plaza de parking,
mostrar el estado de una plaza de parking y mostrar todas las plazas de
parking
7.7.1. Ejercicio1
Crea un diagrama de secuencia en la que muestre cómo se lleva a
cabo la introducción de un coche en una plaza de parking.
7.7.2. Ejercicio2
Crea un diagrama de secuencia en la que muestre cómo se lleva a
cabo la eliminación de un coche de una plaza de parking.
7.7.3. Ejercicio3
Crea un diagrama de secuencia para mostrar el contenido de una
plaza de parking.
7.7.4. Ejercicio4
Crea un diagrama de secuencia para mostrar el contenido de todas
las plazas de parking.
7.7.5. Ejercicio5
Crea un diagrama de secuencia que muestre los mensajes que
intercambian las clases (Main, Post y Blog) a partir de este pseudocódigo. Lo
que se hace es crear una instancia de la clase Post y se le pasa a una instancia
de blog.
[Link]
…
Post post = new Post();
[Link](post);
…
7.7.6. Ejercicio6
Crea un diagrama de secuencia que muestre los mensajes que
intercambian las clases (Main, Post y Blog) a partir de este pseudocódigo. Lo
que se hace es sacar una instancia de post llamando a un método de blog y
después se muestra por pantalla.
[Link]
…
Post post = [Link](1);
[Link]();
…
7.7.7. Ejercicio7
Crea un diagrama de secuencia que muestre los mensajes que
intercambian las clases (Main, Post y Blog) a partir de este pseudocódigo. Lo
que se hace es comprobar si una instancia de post existe. Si es así le decimos
a la instancia de blog que la elimine:
[Link]
…
Post post = [Link](1);
if (null != post) {
[Link](1);
}
…
7.7.8. Ejercicio8
Crea un diagrama de secuencia que muestre los mensajes que
intercambian las clases (Main, Post y Blog) a partir de este pseudocódigo. Lo
que se hace es ver las diez primeras entradas del blog.
[Link]
…
for (int i = 0; i < 10; i ++) {
[Link](i).toString();
}
…
7.8. Bibliografía
Learning UML 2.0. Chapters 7. Modeling ordered interactions:
sequence diagrams
[Link]
[Link]
Aprendiendo UML en 24 horas. Capítulo 9, diagramas de
secuencias.
8. DIAGRAMAS
DE
COMPONENTES
8.1. Componentes
UML
Este componente llamado DataSource podría ser una parte del software
que se encarga de conectarse a un origen de datos como una BD relacional.
8.3. Representación
compacta
Tenemos otra forma alternativa de representar esta relación, poniendo
más énfasis en las dependencias y mostrando los artefactos o ficheros
implicados.
8.4. Representación
interna
Si queremos entrar en más detalles, podemos descubrir los interiores
del componente. Se trata de mostrar el diagrama de clases que se esconde
dentro del componente:
Como se puede ver, además de mostrar las "tripas" del componente,
también podemos detallar quién utiliza o precisa de cada interfaz.
8.5. Ejercicios
8.5.1. Ejercicio1
Crea un diagrama de componentes de un programa de gestión de datos
que muestre los siguientes componentes conectados entre sí de la forma que
te parezca más coherente:
GUI
Base de Datos
Configuración del programa
Copia de Seguridad de BD
8.5.2. Ejercicio2
Crea un diagrama de componentes que muestre los componentes
necesarios para jugar al Minecraft. Debes distinguir el cliente, el servidor y
algún componente para la autenticación.
8.6. Bibliografía
Learning UML 2.0. Chapters 12. Managing and reusing your
system's parts
[Link]
9. DIAGRAMAS
DE
DISTRIBUCIÓN
9.1. Nodos
9.2. Una
distribución
más
compleja:
Una tienda
virtual
Supongamos que queremos representar una tienda virtual online en un
diagrama de distribución. En una tienda virtual, sea el lenguaje que sea el
utilizado para desarrollarla tendremos los siguientes nodos implicados:
1. Nodo cliente. Puede ser un PC, un dispositivo móvil
2. Nodo servidor, con la web que contiene la aplicación.
3. Nodo BD, puede estar aparte del servidor
4. Nodo de envío de correos, necesario para notificaciones
5. Nodo de pagos. Generalmente un sistema de TPV o Terminal
Punto de Venta que proporciona alguna entidad bancaria
9.3. Ejercicios
9.3.1. Ejercicio1
Crea un diagrama de distribución para un programa de escritorio que
gestiona una BBDD de un servidor.
9.3.2. Ejercicio2
Crea un diagrama de distribución para un programa de gestión de
clientes que se valida en un servidor LDAP y cuyos datos se encuentran en
un servidor SQLServer.
9.3.3. Ejercicio3
Crea un diagrama de distribución que represente todo lo necesario
para tener un entorno de desarrollo de plugins Minecraft.
9.3.4. Ejercicio4
Crea un diagrama de distribución que represente un sistema de tienda
como el de Amazon, teniendo en cuenta que es una tienda virtual que puede
tener más de una tienda y varios sistemas de pago (paypal, visa,...)
9.3.5. Ejercicio
5
Crea un diagrama de distribución que represente el sistema de peaje de
una autopista, teniendo en cuenta sus distintos tipos de pasos.
9.4. Bibliografía
Aprendiendo UML en 24 horas. Capítulo 13. Diagramas de
distribución
[Link]
[Link]
10. DESARROLLO
DE
PROYECTOS
Una buena forma de ver cómo abordar un proyecto y cómo aplicar UML
en ellos es haciéndolo a través de ejemplos. En la siguiente parte de este
documento veremos un proyecto muy sencillo donde se pondrán en práctica
los pasos principales que se aplican en el desarrollo de software:
Toma de requerimientos: se parte de una entrevista simulada
con un cliente
Se hace un análisis del dominio
Partiendo de los datos anteriores se diseña la solución
A partir de la solución se muestra la implementación en forma
de código
Se muestra una métodología muy simplificada que toma algunos
elementos del método unificado de desarrollo de proyectos orientados a
objetos. Sobre este método puedes consultar la bibliografía.
10.1. Los
diagramas
por fase
Cada una de las fases incorpora uno o más diagramas UML y unas fichas
a modo de documentación. Los diagramas son el resultado de la tarea de
analizar, diseñar, etc. y serían los siguientes:
6. En la toma de requerimientos:
Diagrama de casos de uso
Ficha de cada uno de ellos
7. Análisis:
Diagrama de actividad
Diagramas de modelo de dominio
8. Diseño:
Diagrama de clases
Diagramas de comunicación
Diagramas de secuencia
Aunque a primera vista pueda parecer que el desarrollo de un proyecto va
a consistir en dibujar hacer dibujos de rectángulos y flechas, todos y cada uno
de esos diagramas deben justificar el código o la solución que se va a dar al
problema inicialmente planteado. Los diagramas deben reflejar las
conclusiones extraídas de la toma de requerimientos, deben reflejar el
dominio que se deduce análisis del problema y, por último, ofrecer un
resumen claro de la solución, que por supuesto debe estar basada en el trabajo
anterior.
10.2. Un
proyecto
simple:
el diario
Este es un proyecto muy sencillo en el que tendremos que desarrollar un
software para una única persona. A través de este simple ejemplo se trata de
poner en práctica y aplicar todo lo aprendido hasta ahora.
10.2.1. El
enunciado
Un amigo nos ha citado porque quiere que le desarrollemos un
programa. Este amigo está siempre muy liado y a la vez tiene tendencia a
olvidarse de sus citas. Para evitar tener más despistes necesita un software
que le permita organizarse mejor. El encuentro tiene lugar en su oficina.
-Hola, ¿qué tal?
-Bien, bien, ¿qué tal por esta oficina?
-Pues ya sabes- dice el cliente- muy liado como siempre.
-Bueno, para eso estoy aquí. No perderé ni un minuto, así que me cuentas
y voy tomando nota.
Siempre hay que tomar nota de alguna forma. En ese tipo de reuniones
hay que tomar notas, pueden ser más o menos formales, pueden convertirse
posteriormente en un documento que haya que firmar, pero, en cualquier
caso, hay que tomar nota.
-Perfecto. Pues mira, necesito algo para poder apuntarme las tareas. Me
han regalado agendas de papel, pero siempre las acabo perdiendo.
-Así que supongo que ahora lo que quieres es una especie de agenda, pero
digital...
-Efectivamente. El portátil siempre lo llevo conmigo a todas partes y está
abierto desde que me levanto hasta que me acuesto. Así que para mí lo ideal
sería tener una agenda en mi propio portátil.
La idea básica está clara, pero hay que entrar en detalles de lo que esa
agenda debe hacer. Una de las habilidades básicas del entrevistador es
sacarle información al cliente.
-Una agenda, pero qué necesitas hacer con ella exactamente.
-Pues obviamente apuntar las tareas.
Ahí está el que puede ser el primer caso de uso. Hay que ir detectándolos
a través de esta entrevista.
- ¿Y nada más? ¿Con apuntarlas te basta? Pero cómo las anotarías,
¿supongo que por fecha quizás?
-Sí, sería por fecha. Básicamente me gustaría anotar la fecha y un texto
que indique lo que tengo que hacer.
De ahí estamos sacando atributos...
-Entiendo. ¿Y luego cómo verías esas tareas? ¿Te gustaría buscarlas por
fecha?
-No, no quiero usar la fecha. Quiero ver la agenda como una especie de
lista numerada.
Sí, es raro. Pero si el cliente la quiere así...de paso nos sirve para
simplificar la solución a este problema.
-Ah vaya, O sea, que a la hora de ver las tareas o entradas que hay para
esa agenda, tiene que verse una lista.
-Eso es. Me gustaría poder ver todas las tareas que tengo de forma
resumida en las que se muestre solo la fecha y un resumen de la mista.
-Entiendo. Y para ver el detalle de una entrada de la lista supongo que...
-También me gustaría poder seleccionar una de esas tareas para
visualizar los detalles.
-OK, correcto. O sea, que quieres poder ver todas las entradas y también
poder ver cada una de forma individual.
Se repite en alto para que quede claro para los partes.
-Efectivamente. Bueno, habría algo más. Me gustaría poder anular
entradas de la agenda.
¿Qué es eso de anular? Esto es otro caso de uso, pero hay que aclararlo.
- ¿Quieres decir posponer o cambiar?
-No. Yo las agendas las manejo así. Prefiero borrar completamente una
tarea si ya no la voy a hacer.
-Pero ¿si la retomas más tarde?
-Pues nada, la apunto de nuevo.
-OK, bueno, así es más sencillo. ¿Alguna cosa más?
-No, en principio es eso.
-Muy bien, pues ya tengo una idea bastante precisa de lo que necesitas.
Lo pasaré a limpio y lo redactaré de tal forma que quede perfectamente
identificado todo lo que se debe hacer.
-Perfecto, ya me dirás. Gracias.
-Encantado.
10.2.2. Los casos
de uso
Tras la primera entrevista los casos de uso quedan más o menos
claros. Definir claramente los casos de uso es de vital importancia ya que son
quienes dan inicio a todo el proceso de desarrollo. Con las notas que se han
tomado en la entrevista podemos elaborar un primer diagrama de casos de
uso que resume todo lo que se espera que haga el sistema.
A fin de que sean bien comprendidos y estén bien documentados, cada caso
de uso se identificará y completará una ficha con una descripción más
detallada.
[Link] FICHAS
DE
CASO
DE USO
Una ficha tiene el siguiente aspecto. Primero indicamos sus datos
básicos y luego mostramos un resumen de la interacción entre el actor y el
sistema.
3. Caso de uso: un identificador y un nombre corto.
4. Actores: los actores o usuarios que lo hacen.
5. Propósito: un resumen de lo que se espera que haga el sistema.
6. Tipo: esencial, secundario, auxiliar de otro,.
7. Referencias: Se indica si se utiliza o depende de otro caso de uso
Interacción: A continuación, se indica cómo sería el diálogo entre el
actor y el sistema, es decir, entre el usuario y el programa. Primero indicamos
la interacción en circunstancias normales y luego la interacción en casos
alternativos o infrecuentes.
Casos de excepción
El sistema solicita un
1'
dato...
Casos de excepción
Casos de excepción
10.2.3. El
análisis
Una vez que tenemos claro qué es lo que nos piden, tenemos que
llevar a cabo el análisis que en definitiva no es más que:
Acotar el problema
Comprender el dominio
Identificar funciones, conceptos y sus relaciones
Una forma de resumir de forma gráfica es a través del diagrama de
Actividad y el diagrama de modelo de dominio.
[Link] DIAGRAMA
DE
ACTIVIDAD
Antes de nada, hay que tener claro los procesos que se llevan a cabo en
el problema que se nos plantea. Debemos tomar cada caso de uso y
representar los pasos que se llevan a cabo para cumplirlos. Tomaremos cada
caso de uso y veremos qué pasos se precisan:
Caso de uso: 1. Add entry
Caso de uso: 2. Remove entry
Caso de uso: 3. Show one entry
Caso de uso: 4. Show all entries
[Link] EL
DIAGRAMA
DE
MODELO
DE
DOMINIO
Es una representación de conceptos del dominio para la comprensión
del problema. Trata de:
1. Identificar conceptos
2. Asociaciones
3. Atributos que forman parte del dominio
Este podría ser un primer conceptual para nuestro caso, en una serie de
pasos en el que lo iremos puliendo:
En este caso el User está identificado como parte del dominio, pero eso sería
un error. El usuario es único y en este caso no es más que el actor. Lo que es
el dominio, los conceptos que forman parte del problema son:
Como se ve esto no es más que un resumen de qué es lo que forma
parte del modelo. Es probable, que más adelante en el diseño, lo que aquí se
ha puesto como el usuario se convierta en la clase principal, o el interfaz de
usuario. Pero de momento los conceptos quedan así.
Hay un par de atributos que deberíamos resaltar en el concepto Entry que son
fecha y texto, con lo que nuestro diagrama definitivo quedaría así:
10.2.4. El
diseño
¿Qué hacemos en la fase de diseño?
5. Pensar las clases
6. Arquitectura de software
7. Detalles procedimentales
8. Estructuras de datos
9. Representaciones de interfaz
En definitiva: hacemos los planos de la casa. Una vez que hemos
entendido y analizado el problema, tenemos claro lo que se nos pide y ya
hemos identificado las posibles clases el modelo conceptual. Ahora no
tenemos más que poner esas clases a trabajar para que cumplan cada uno de
los requerimientos exigidos.
Por lo tanto, por cada caso de uso haremos un diagrama de
comunicación y otro de secuencia que nos acercará mucho al diseño final.
Ese diseño final de la solución quedará plasmado en un diagrama de clases,
que es algo imprescindible para poder empezar a escribir código.
[Link] EL DIAGRAMA
DE
COMUNICACIÓN
Hasta ahora habíamos identificado varias clases, y ahora en la fase
de diseño debemos aclarar qué mensajes se deben mandar entre las clases
para cumplir con cada caso de uso. Lo que refleja un diagrama de
comunicación precisamente son los mensajes que se intercambian las
clases entre sí. Esos mensajes van a convertirse en los futuros métodos de
la implementación.
Caso de uso: 1. Add entry
La clase principal recibe la orden en todos los casos desde el actor. En Add
Entry la principal genera una nueva instancia de Entry, y una vez hecho la
manda a la instancia de Diary para añadirla.
Al igual que el anterior llega una orden de leer una entrada, el método
principal solicita un número y se manda la orden de lectura a la instancia de
Diary. Ésta solo recupera el dato si existe una entrada con ese índice.
[Link] EL
DIAGRAMA
DE CLASES
Tras comprobar cómo deben interactuar las clases entre ellas en los
diagramas anteriores, el diagrama de clases se puede deducir fácilmente.
10.2.5.
La implementación
Basándonos en los diagramas del diseño y fundamentalmente en el
diagrama de clases ya estamos en disposición de implementar el proyecto en
código.
[Link]
Este código es el más sencillo ya que muestra lo que es una clase que
representa una entrada de la agenda y prácticamente es un POJO.
package [Link];
import [Link];
/**
* Represents an entry in the diary
* @author Eugenia Pérez, Pello Altadill
*/
public class Entry {
private String text;
private Date date;
/**
* @param text
* @param date
*/
public Entry(String text, Date date) {
[Link] = text;
[Link] = date;
}
/**
* shows Entry with date as heading followed by text
* @return
*/
public String show () {
return [Link]() + "\n" + text;
}
/**
* @return the text
*/
public String getText() {
return text;
}
/**
* @param text the text to set
*/
public void setText(String text) {
[Link] = text;
}
/**
* @return the date
*/
public Date getDate() {
return date;
}
/**
* @param date the date to set
*/
public void setDate(Date date) {
[Link] = date;
}
[Link]
Esta clase representa el diario que contiene varias entradas y por
tanto contiene un atributo que le permite acumular instancias de la clase
Entry.
package [Link];
import [Link];
/**
* Represents a diary with entries
* @author Eugenia Pérez, Pello Altadill
*/
public class Diary {
private Vector<Entry> entries;
/**
* default constructor, inits entry vector
*/
public Diary () {
entries = new Vector<Entry>();
}
/**
* adds entry into diary
* @param entry
*/
public void addEntry (Entry entry) {
[Link](entry);
}
/**
* removes entry from Diary
* @param index
* @return if entry was successfully removed or not
*/
public boolean removeEntry (int index) {
if (index < [Link]()) {
[Link](index);
return true;
} else {
return false;
}
}
/**
* reads one entry from diary
* @param index
* @return entry referenced by index, null otherwise
*/
public Entry readEntry (int index) {
if (index < [Link]() ) {
return [Link](index);
} else {
return null;
}
/**
* shows all entries in diary
* @return
*/
public String readAll() {
String result = "";
return result;
}
[Link]
Este es el programa principal, el que muestra un menú al usuario para
que este pueda llevar a cabo las operaciones que necesita para gestionar su
agenda. Main crea una instancia de Diary y cuando lo necesita crea una
instancia de Entry.
package [Link];
import [Link];
import [Link];
/**
* Main program with user console interface to manage the diary
* @author Eugenia Pérez, Pello Altadill
*/
public class Main {
/**
* @param args
*/
public static void main(String[] args) {
// variables for new entries
Date date = new Date();
String text = "";
Entry entry = null;
do {
[Link]("------------ MyDiary --------------");
[Link]("Select one option:\n");
[Link]("1. Add new entry");
[Link]("2. Remove entry");
[Link]("3. Read one entry");
[Link]("4. Show all entries");
[Link]("5. Exit");
option = [Link]();
switch (option) {
case "1":
[Link]("Please, enter text.");
text = [Link]();
date = new Date();
entry = new Entry(text,date);
[Link](entry);
break;
case "2":
[Link]("Please, enter index
number.");
text = [Link]();
index = [Link](text);
[Link](index);
break;
case "3":
[Link]("Please, enter index
number.");
text = [Link]();
index = [Link](text);
entry = [Link](index);
if (null != entry) {
[Link]("Entry #" + index +
"\n" + [Link]());
}
break;
case "4":
[Link]([Link]());
break;
case "5":
[Link]();
[Link]("Bye");
break;
default:
[Link]();
[Link]("TRY AGAIN");
break;
}
} while ();
}
}
11. BIBLIOGRAFÍA,
RECURSOS
1 DEFINICIÓN
SCRUM
Scrum es un marco de trabajo para el desarrollo de proyectos y
productos. Por un marco de trabajo se entiende una serie de pautas para guiar
este proceso tan complejo. Aunque se habla mucho de Scrum en el desarrollo
de proyectos de tecnologías de la información, es también utilizado en otras
industrias como por ejemplo la automovilística, destacando el caso de Toyota
entre otros.
Scrum nación en Japón en los años 80, pero no fue hasta los 90 cuando
se extendió definitivamente. En este sentido destaca la presentación del
documento Scrum Development Process, por Ken Schwaber en 1995.
Aunque se suele decir que Scrum es muy fácil de entender, también es
cierto que es muy complicado de llegar a dominar con maestría.
1.1 FUNDAMENTOS
Scrum se basa en la práctica para predecir el desarrollo de los
acontecimientos futuros. Es decir, asume que seremos mucho más precisos en
nuestras estimaciones si los hechos a valorar ya los hemos experimentado con
anterioridad.
Scrum se fundamenta en 3 pilares:
Transparencia: todos los implicados en un proyecto deben
poder tener acceso al estado del desarrollo, y todos deben
compartir la misma visión de lo que significa que algo esté
“Terminado” (definición de done).
Inspección: el proceso de desarrollo debe ser analizado de
manera continua para detectar carencias.
Adaptación: en caso de encontrar carencias (algo muy
habitual en la gran mayoría de los desarrollos), el equipo
debe estar abierto y dispuesto a implementar mejoras que las
solucionen.
2 EL
EQUIPO
SCRUM
2.1 PRODUCT
OWNER
A alto nivel podríamos definir al Product Owner (PO) como aquella
persona que pide funcionalidades al equipo de desarrollo. Es decir, es aquel
que presenta su lista de necesidades, se las explica al equipo hasta que se
asegure su completa comprensión, y finalmente, valida las funcionalidades
que el equipo le entrega.
Para poder validar los entregables, antes de comenzar el desarrollo el
Product Owner y el equipo deben acordar cuándo se considera que una
funcionalidad está terminada. Es decir, si el PO pide al equipo que el sitio
Web en desarrollo necesita un formulario de login, antes de comenzar el
trabajo debe acordarse todo lo que este formulario debe incluir: ¿estilos?,
¿validación de los campos?, ¿recuperación de contraseña?, etc. Es decir,
ambos deben tener un contrato al que atenerse cuando la funcionalidad se
termine.
Generalmente este rol suele estar desempeñado por personas que
conocen muy bien las necesidades de los clientes o usuarios finales del
producto.
El PO es sólo una persona, y es la encargada de crear, administrar y
priorizar el Product Backlog. El Product Backlog no es más que la lista de
tareas que se deben acometer un un proyecto para su finalización. Estas tareas
deben estar ordenadas de mayor a menor prioridad.
2.2 DEVELOPMENT
TEAM
El equipo de desarrollo está compuesto por 3-9 miembros encargados
de construir el producto (en este cálculo no cuentan el PO y el SM), es decir,
llevar a cabo la aplicación. Este equipo debe estar compuesto por personas
con las destrezas necesarias para acometer el desarrollo, por lo que es
conveniente que haya variedad de perfiles.
2.3 SCRUM
MASTER
Es la figura que se asegura de que se realiza Scrum correctamente. Es
decir, vela por el cumplimiento de las pautas recomendadas por la
métodología. Generalmente entre sus tareas destaca la mediación ante el
equipo de desarrollo para eliminar obstáculos que impidan el progreso de
este. Es decir, si por ejemplo el equipo necesita acceso a una máquina de la
organización, y esto está retrasando su trabajo, se asegura de hablar con quien
sea necesaria para realizar esta gestión. Es por ello que suele recomendarse
que este rol lo desempeñe alguien con cierto “peso” específico dentro de la
organización, ya que debe ser un mediador y conseguidor.
Otras tareas no menos importante serían:
Ayudar al PO en la depuración y priorización del Product
Backlog.
Guiar al equipo en la aplicación de Scrum.
Guiar a la organización (empresa) en la adopción de Scrum.
Poner en valor los beneficios de seguir esta métodología y
garantizar su cumplimiento.
3 EVENTOS
Scrum define una serie de eventos o reuniones con el fin de que todo
esté planificado y se minimizen las reuniones no planificadas y sus
consecuentes pérdidas de tiempo.
Los eventos de Scrum se caracterizan por tener una duración máxima
de tiempo. Es decir, a excepción del sprint, pueden durar menos de lo
estipulado pero nunca más.
3.1 EL
SPRINT
Es el evento principal de Scrum. Su duración máxima es de 1 mes y su
duración mínima 1 semana. Es muy habitual que se utilicen sprints de 2
semanas, ya que es el punto medio suficiente para que se pueda terminar una
cantidad de trabajo considerable pero a su vez no ser demasiado largo para
que luego las demostraciones y planificaciones se alarguen demasiado en el
tiempo.
El objetivo primordial de un sprint es completar el trabajo que se ha
planificado para acometer durante el mismo, generando así una versión
entregable del proyecto en curso. Es decir, hacer funcionalidades completas y
no dejar trabajo a medias que no se pueda poner en producción.
3.2 DAILY
SCRUM
Es una reunión de un máximo de 15 minutos que tiene lugar todos los
días. Se trata de que los miembros del equipo de desarrollo se actualicen entre
sí sobre qué está haciendo cada uno. Cada miembro del equipo toma la
palabra por turnos para responder a 3 preguntas:
¿Qué hice ayer?
¿Qué voy a hacer hoy?
¿Hay algún impedimento que me bloquee?
El Scrum Master debe asegurarse que esta reunión tiene lugar todos los
días pero no es necesaria su presencia. De hecho, sólo los miembros del
equipo de desarrollo toman la palabra durante el daily Scrum. Así mismo, el
PO puede asistir, pero nunca intervenir.
Generalmente esta reunión tiene lugar siempre en el mismo lugar y a la
misma hora (habitualmente a primera hora de la mañana para planificar el
resto del día) y los miembros del equipo suelen permanecer de pie durante
toda la reunión (esto ayuda a mantener la brevedad).
3.3 SPRINT
PLANNING
Esta reunión marca el inicio del sprint, y en ella se planifican todas las
tareas que se acometerán durante el mismo. Tiene una duración máxima de 8
horas si el sprint es de 1 mes (4 horas para un sprint de 2 semanas).
En esta reunión se estudia el Product Backlog y se extraen de él las
tareas que el equipo de desarrollo cree que será capaz de terminar durante el
sprint. Así, se creará un Sprint Backlog con estas tareas. El PO se asegurará
de que el equipo comprende el alcance de las tareas y se definen los criterios
de aceptación de cada una de ellas. Es decir, ¿qué se debe hacer en cada una
para que se consideren terminadas?
Para saber cuántas tareas pueden entrar en el sprint el equipo debe
estimar cada una de ellas. Las estimaciones en Scrum se hacen por coste
estimado de complejidad y no por horas de desarrollo. Es decir, por cada una
de ellas se debe decidir cómo de compleja es, y en función de eso asignarle
un peso. Suele utilizarse mucho una baraja de cartas que siguen la sucesión
de Fibonacci.
3.4 SPRINT
REVIEW
Al finalizar cada sprint se hace una revisión. Suele consistir en una
demo en la que el equipo de trabajo expone las funcionalidades terminadas.
Es decir, los miembros del equipo demuestran el producto en funcionamiento
al PO. Tiene una duración máxima de 4 horas (2 horas para sprints de 2
semanas).
Por lo general esta reunión tiene lugar en una sala que cuente con
equipo informático y monitor para que todo el mundo pueda ver el software
funcionando de manera real.
En esta reunión las tareas que no se den por aprobadas porque no
cumplan los criterios acordados volverán al Product Backlog.
3.5 SPRINT
RETROSPECTIVE
Es una reunión de un máximo de 3 horas (1,5 horas para sprints de dos
semanas) en las que el Scrum Master junto con el equipo de desarrollo
analizan qué ha ido bien y mal durante el sprint. No se trata sólo de analizar
el sprint desde el punto de vista técnico, sino también desde la perspectiva de
las personas, relaciones, herramientas, etc.
Es decir, en caso de que por ejemplo haya habido varios días en que la
Daily Scrum no se haya realizado, o se hubiese realizado fuera de hora,
debería señalarse este problema, analizar las causas y tomar acciones para
que en el próximo sprint no se vuelva a repetir.
4 ARTEFACTOS
4.1 PRODUCT
BACKLOG
Es una lista ordenada (por prioridad) de todo lo que podría ser necesario
hacer para completar un producto o proyecto. La persona encargada de
gestionarlo es el PO.
El product backlog está en constante evolución. Es decir, al inicio de un
proyecto no es habitual que se disponga de un backlog completo con todas las
tareas que se terminarán realizando. Lo normal es que a medida que se
avance en el desarrollo, nuevas tareas entren en el backlog y otras
desaparezcan porque ya no sean necesarias.
Cada elemento de esta lista normalmente tiene al menos una
descripción (lo más detallada posible), la prioridad que define su orden y la
estimación en puntos de esfuerzo de lo que costará implementarlo.
Así mismo, es frecuente que aquellos elementos de la lista que están
situados más arriba (más prioritarios) estén mejor definidos. Esto sucede
porque son los que el PO desea que se realicen de manera inminente, por
tanto los tiene más claros y necesita detallarlos de una manera más profunda
al equipo. En cambio, los menos prioritarios aún no necesitan tanto nivel de
detalle, ya que lo irán adquiriendo según vayan subiendo en la pila.
4.2 SPRINT
BACKLOG
Es el subconjunto del Product Backlog que se acometerá en el sprint
actual. Para elaborarlo, el equipo de desarrollo, a partir del Product Backlog
selecciona aquellas tareas, en orden, que cree que le dará tiempo a finalizar
en un sprint. Para ello, se basa en la estimación de cada elemento. La suma de
la estimación de todos los elementos del Sprint Backlog tiene que ser cercana
a la cantidad de puntos de esfuerzo que el equipo ha venido completando en
cada sprint anterior.
Es decir, supongamos que el equipo de desarrollo, durante los últimos 3
sprints ha completado 12, 14 y 15 puntos respectivamente. Lo lógico es que
el equipo meta en el sprint backlog tareas por un valor aproximado de 14
puntos.
4.3 BURNDOWN
CHART
Este gráfico muestra el progreso del equipo de desarrollo durante el
sprint. Es un gráfico simple que relaciona los días restantes para finalizar el
sprint y la cantidad de trabajo que resta por completar del sprint backlog.
Es un gráfico muy sencillo, pero que permite ver en todo momento
cómo transcurre el sprint, y permite hacer predicciones sobre si se completará
todo el trabajo antes de que el sprint finalice. Por ejemplo, un buen momento
para actualizarlo puede ser la Daily Scrum.
5 EJEMPLO
PRÁCTICO
5.1 ROLES
Ya que el tamaño mínimo del equipo de desarrollo en Scrum es de 3
personas, formaremos grupos de 3 alumnos. Los roles de Scrum Master y
Product Owner los desempeñará la profesora. Aunque no suele ser
recomendable que la misma persona posea ambos roles, por las
circunstancias del contexto se adoptará esta decisión.
5.2 ARTEFACTOS
Product Backlog: como ya hemos dicho, es el listado de
todas las tareas que se consideran necesarias para terminar
el proyecto. Es una lista de alto nivel, por lo que no tiene
que ser completa al inicio del sprint pero sí suficiente. Por
ejemplo, en el caso de una agenda, podríamos necesitar:
Insertar contacto
Borrar contacto
Editar contacto
Buscar contacto
Listar contactos
Exportar agenda a fichero
Ordenar contactos
Permitir guardar varios números de teléfono por
contacto
Importar contactos (de fichero)
Esta lista la deberíamos organizar mediante una herramienta que
puede ser tan simple como una hoja Excel u otras herramientas
más especializadas como TFS, Jira, tinyPM easyBacklog o
Rally software.
La siguiente imagen muestra un el Product Backlog generado en
rallydev:
5.4 LAS
ESTIMACIONES
Una de las principales diferencias de las métodologías ágiles con
respecto a las tradicionales son las estimaciones y la manera de hacerlas. En
Scrum, las estimaciones corren a cargo del equipo de desarrollo. Por tanto,
son los desarrolladores, que son los que realmente terminarán haciendo el
trabajo, los que hacen la estimación. Y además esta no es en horas, sino en
puntos de esfuerzo, que es un concepto de más alto nivel.
Es decir, es muy difícil saber si un formulario de login nos va a llevar
40 o 50 horas, pero sí podemos determinar si nos parece una tarea más fácil o
más difícil. Los puntos de esfuerzo buscan precisamente eso, expresar lo fácil
o difícil que nos parece lo que estamos estimando. De ahí que las cartas que
se suelen utilizar en las estimaciones no tengan números correlativos.
En la siguiente imagen podemos ver un ejemplo de baraja que se suele
utilizar para estimar (planning poker cards). Una estimación de 0 sería para
algo que no nos cuesta nada de esfuerzo, 0,5 para algo muy fácil y rápido, 1
para algo muy fácil y bastante rápido, 2 para una tarea normal, y así
sucesivamente. No obstante, cabe destacar que un 5 no tiene por qué
representar el mismo esfuerzo para un equipo que para otro.
También existen aplicaciones móviles que simulan las cartas. Por
ejemplo, para Android, Scrum Poker Cards:
[Link]
id=[Link]&hl=es
Para estimar una tarea, lo primero que se hace es definir sus requisitos.
El PO explica la tarea al equipo, y los miembros del equipo hacen todas las
preguntas que necesiten para clarificarla. Todo el mundo tiene que
comprender perfectamente el alcance para que la estimación sea válida. Una
vez que ha sido bien definida, cada miembro del equipo piensa cuántos
puntos de esfuerzo cree que cuesta, y selecciona una carta, pero sin enseñarla
a los demás (de ahí lo de poker). Cuando todos los miembros del equipo han
decidido qué carta van a sacar, todo el mundo pone la carta sobre la mesa a la
vez y la enseña a los demás.
En este momento pueden pasar dos cosas:
Todas las estimaciones son similares, con lo cual se suele
coger la media o mediana de todas las estimaciones.
Hay varias estimaciones que distan mucho del resto. Por
ejemplo, dos miembros del equipo sacan un 3 y uno un 13.
Queda claro que, con tal diferencia, alguien está
equivocado. O bien la persona que ha sacado un 13 ha
tenido en cuenta algo que los otros han pasado por alto y
dificulta mucho la tarea, o bien no la ha comprendido bien y
la ha sobreestimado. En estos casos hay que volver a
discutir la tarea y cada uno expondrá por qué ha sacado su
estimación (sobre todo el que ha sacado un 13). Tras esta
conversación todos volverán a estimar hasta que todas las
estimaciones sean bastante próximas entre sí (no hace falta
que todas sean iguales, ni mucho menos).
4 BIBLIOGRAFÍA
Scrum guide, Ken Schwaber & Jeff Sutherland.
The Agile Manifesto, Kent Beck & otros.
Agile Product Management with Scrum, Roman Pilcher
A Practical Guide to Distributed Scrum, Elizabeth Woodward, Steffan
Surdek, Mathew Ganis.
Scrum Project Management, Kim Pries, Jon Quigley.
Agile Estimating and Planning: Planning Poker, Mike Cohn.
Explicado Scrum a mi abuela, Jorge Serrano.
Capítulo 7
Refactorización
1 INTRODUCCIÓN
Los programadores deberían escribir código para que los humanos lo
puedan entender.
La refactorización de código consiste en reescribir el código para que
sea más inteligible y facilite el mantenimiento del mismo. Un código debe ser
suficientemente claro como para NO necesitar comentarios. Pero no se puede
reescribir de cualquier manera: la refactorización debe hacerse paso a paso y
debe ir acompañada de pruebas unitarias para asegurar que los cambios
no afectan a la funcionalidad.
1.1 Entornos
de
desarrollo
Todos los entornos de desarrollo modernos dispones de funciones de
refactorización para poder llevar a cabo las refactorizaciones de una manera
segura. Suele encontrarse como una opción principal del menú siempre que
estemos editando código:
2.1 Renombrar
variables y
métodos
Esta refactorización es tan simple y evidente como renombrar los
nombres de variables, objetos y métodos para que con un simple vistazo
conozcamos su propósito en el programa.
Hay que evitar cualquier variable con nombre genérico, nombres
abreviados o ininteligibles:
public class Conversor {
public float conv (float c) {
float x = c * 166.386f;
return x;
}
}
2.2 Ocultar
propiedades
y métodos
Una de las propiedades fundamentales de la POO es que la
encapsulación. Los detalles internos de una clase no deben estar a la vista de
las otras clases. Es frecuente que todos los atributos de la clase se conviertan
en privados y también aquellos métodos que nunca se vayan utilizar por otras
clases. Por tanto:
public class Customer {
String name;
int id;
public Customer() {
init();
}
public void init() {
name = "Eugene Krabs";
id = 42;
}
public String toString() {
return id + ":" + name;
}
}
Todo debe ser privado salvo aquellos métodos que se usen desde fuera.
Si queremos que desde fuera se pueda ver o cambiar el valor de esos atributos
hay que implementar los métodos set y get.
Dentro de eclipse podemos utilizar el encapsulate field, que además nos
agregará los métodos set y get.
public class Customer {
private String name;
private int id;
public Customer() {
init();
}
private void init() {
setName("Eugene Krabs");
setId(42);
}
public String toString() {
return getId() + ":" + getName();
}
String getName() {
return name;
}
void setName(String name) {
[Link] = name;
}
int getId() {
return id;
}
void setId(int id) {
[Link] = id;
}
}
2.3 Los
Magic
Numbers
Muchas veces en los programas no encontramos con operaciones en los
que aparece un número cuyo origen y significado es un misterio. Pero ese
número no se puede tocar porque el programa casca. ¡Es un número mágico!
no sabemos para qué sirve, pero tiene alguna función.
Por ejemplo:
public class PasswordGenerator {
private Random random = new Random();
private String characters = "abcdefghijkmnopqrstuvwxyz23456789";
public String generatePassword(int length) throws Exception {
if (length < 6 || length > 15) {
throw new Exception("Wrong password length: " + length);
} else {
String password = "";
for (int i = 0; i < length; i++)
password += [Link]([Link]([Link]()));
return password;
}
}
}
Eclipse
Dentro de eclipse podemos extraer un método fácilmente seleccionando
parte de un método y refactorizando con "Extract method"
2.5 Inline
method
Esto es exactamente lo contrario a la refactorización extraer método.
¿Cómo es posible? ¿no habíamos quedado en que había que separar código?
El hecho de que existan dos refactorizaciones antagónicas es algo muy
normal en la refactorización. Un código nunca queda definitivo si se
introducen cambios o se mejora y a veces se terminan rectificando los
cambios.
Por tanto, lo que teníamos separado en un método, quizá porque es
simple y el método solo se usa desde un sitio lo podemos volver a meter
donde corresponda.
public class UrlCleaner {
public String clean(String title) {
String url = trimSpaces(title);
url = removeSpecialChars(url);
url = replaceSpaces(url);
url = [Link]();
return url;
}
private String replaceSpaces(String url) {
return [Link]("[\\s]+", " ").replaceAll("[\\s]", "-");
}
private String removeSpecialChars(String url) {
return [Link]("[\\.\\:\\,\\?\\!\\_\\;]", "");
}
private String trimSpaces(String url) {
return [Link]();
}
}
Podemos volver a la situación anterior:
public class UrlCleaner {
public String clean (String title) {
return [Link]()
.replaceAll("[\\.\\:\\,\\?\\!\\_\\;]", "")// Replaces special chars
.replaceAll("[\\s]+"," ") // Replace duplicated spaces
.replaceAll("[\\s]","-"); // Replace spaces with hyphen
}
}
Eclipse
Dentro de eclipse podemos llevar a cabo esta refactorización
seleccionando todo un método y utilizando la opción "Inline..."
2.6 Convertir
parámetros
en objetos
Muchas veces un método tiene una lista de parámetros larga lo cual
complica la vida de todos aquellos que quieren usar el método.
Los parámetros, si tienen cierta relación, se pueden convertir en una
clase donde cada parámetro es un atributo. De esa forma simplificamos la
método y además podemos introducir mejoras (inicializaciones,
comprobaciones,.) dentro del objeto.
public class Order {
private Hashtable<String, Float> items = new Hashtable<String, Float>();
public void addItem(Integer productID, String description, Integer quantity, Float price, Float
discount) {
[Link](productID + ": " + description,
(quantity * price) - (quantity * price * discount));
}
public float calculateTotal() {
float total = 0;
Enumeration<String> keys = [Link]();
while ([Link]()) {
total = total + [Link]([Link]());
}
return total;
}
}
Podemos convertirlo en un Objeto llamado OrderItem:
public class OrderItem {
private Integer productID;
private String description;
private Integer quantity;
private Float price;
private Float discount;
public OrderItem(Integer productID, String description, Integer quantity, Float price, Float
discount) {
[Link](productID);
[Link](description);
[Link](quantity);
[Link](price);
[Link](discount);
}
//getters y setters
Eclipse
Dentro de eclipse podemos llevar a cabo esta refactorización
seleccionando unos parámetros y con "Introduce Parameter Object"
2.7 Replace
Temp
with
Query
Una práctica frecuente entre los programadores es utilizar variables
temporales en los métodos, y el problema con ellas es que, además de
temporales, son locales y obviamente solo sirven en el contexto del método.
Otro problema añadido es que es estas variables hacen que el método sea más
largo y lo que es peor, dificulta otras refactorizaciones como Extraer Método.
Supongamos que tenemos un método que calcula una nota media entre
tarea y examen, a la que suma un punto extra en caso de buena actitud.
public class Student {
private String name;
private boolean hasGoodAttitude;
public Student(String name, boolean hasGoodAttitude) {
[Link] = name;
[Link] = hasGoodAttitude;
}
public float calculateAverage(float homework, float exam) {
float mark = (homework + exam) / 2;
if (hasGoodAttitude) {
return mark + 1;
} else {
return mark;
}
}
}
return true;
} else {
return false;
}
}
return true;
} else {
return false;
}
}
2.9 Evita
variables
temporales
Una mala práctica o descuido de algunos desarrolladores es utilizar una
variable para distintos propósitos dentro de un mismo bloque. Se trata de
variables que muchas veces se destinan a contener valores temporales y que
se convierten en una especie de variable de usar y tirar.
public class Invoice {
public float totalPrice (float price, float vat, float discount) {
float temp = 0;
temp = (vat * price) / 100;
[Link]("Applied vat: " + temp);
temp = price + temp;
[Link]("Total with vat: " + temp);
return temp - discount;
}
}
2.11 Replace
Method
with
Method
Object
Determinadas operaciones complejas de un proyecto pueden estar
concentradas en un método lleno de variables locales, de tal manera que se
dificulta llevar a cabo refactorizaciones como extraer método.
En el siguiente ejemplo vemos un método que trata de aplicar un
descuento a un precio. Este varía en función del tipo de cliente y del precio y
en definitiva es relativamente complejo porque puede manejar diferentes
variantes:
[Link]
public class Customer {
private boolean isVip;
private boolean isSpecial;
private int type;
public static final int NORMAL = 0;
public static final int SPECIAL = 1;
public static final int VIP = 2;
public Customer(boolean isVip, boolean isSpecial, int type) {
[Link] = isVip;
[Link] = isSpecial;
[Link] = type;
}
public int getType() {
return type;
}
public boolean isVip() {
return isVip;
}
public boolean isSpecial() {
return isSpecial;
}
}
[Link]
public class DiscountCalculator {
private double finalPrice;
private double appliedVat;
private double price;
private Customer customer;
private double discount;
public DiscountCalculator(double price, Customer customer, double discount) {
finalPrice = 0;
appliedVat = 0;
[Link] = price;
[Link] = customer;
[Link] = discount;
}
public double applyDiscount() {
switch ([Link]()) {
case [Link]:
appliedVat = 1.21f;
break;
case [Link]:
appliedVat = 1.15f;
break;
case [Link]:
appliedVat = 1.04f;
break;
default:
appliedVat = 1.21f;
break;
}
if (price > 50 && [Link]()) {
finalPrice = price * 0.5;
} else if (price > 10 && [Link]()) {
finalPrice = price * 0.1;
} else {
finalPrice = price;
}
return finalPrice * appliedVat - discount;
}
}
2.12 Descomponer
condicional
En ocasiones nos encontramos con expresiones condicionales que
resultan complejas tanto en la condición como en el cuerpo de las mismas.
[Link]
public class Customer {
private int age;
public Customer (int age) {
[Link] = age;
}
public int getAge() {
return age;
}
public void setAge(int age) {
[Link] = age;
}
}
[Link]
public class Invoice {
private Customer customer;
public Invoice (Customer customer) {
[Link] = customer;
}
public float calculatePayment (float price, float discount, float vat) {
float payment = 0;
if ([Link]() < 18 || [Link]() > 65 ) {
payment = price * discount * vat;
} else {
payment = price * vat;
}
return payment;
}
}
Además de simplificar el código también podemos aclarar su cometido.
2.13 Consolidar
expresión
condicional
Muchas veces hay que hacer una determinada operación siempre que no
exista alguna circunstancia excepcional. Para eso a veces se llevan a cabo una
serie de comprobaciones con expresiones condicionales en las que se acaba
devolviendo el mismo valor.
Por ejemplo, supongamos que en determinados casos no queremos que
un recibo sea cobrado: por edad del cliente, porque la fecha está pasada o por
que la cantidad es pequeña.
public class Invoice {
private Customer customer;
private int year;
public Invoice(Customer customer, int year) {
[Link] = customer;
[Link] = year;
}
public float calculateTotal(float subtotal, float vat) {
if ([Link]() < 18)
return 0;
if (new GregorianCalendar().get([Link]) > year)
return 0;
if (subtotal < 0.5f)
return 0;
return subtotal * vat;
}
}
Vamos a convertir esas tres comprobaciones en una única condición
extrayendo el método
public class Invoice {
private Customer customer;
private int year;
public Invoice(Customer customer, int year) {
[Link] = customer;
[Link] = year;
}
public float calculateTotal(float subtotal, float vat) {
if (isUselessToCharge(subtotal)) {
return 0;
} else {
return subtotal * vat;
}
}
private boolean isUselessToCharge(float subtotal) {
return ([Link]() < 18)
|| (new Date().getYear() > year) || (subtotal < 0.5f);
}
}
2.14 Consolidar
fragmentos
de
condicional
duplicados
En los métodos o fragmentos de código donde tenemos el típico bloque
if else se supone que las instrucciones que se ejecutan son distintas según la
condición del if. Puede darse el caso de que alguna de esas instrucciones se
repita dentro de la condición y que además sea la última instrucción del
bloque.
public class Invoice {
private Customer customer;
private float price;
private int qty;
public Invoice (Customer customer, float price, int qty) {
[Link] = customer;
[Link] = price;
[Link] = qty;
}
public float calculateTotal (float vat, float discount) {
float subtotal = 0;
if ([Link]()) {
subtotal = (price * qty) - discount;
subtotal = subtotal * (1 + (vat/100));
return subtotal;
} else {
subtotal = (price * qty);
subtotal = subtotal * (1 + (vat/100));
return subtotal;
}
}
}
En ese caso, está claro que podemos trasladar esa instrucción fuera del
bloque ya que en cualquiera de los dos casos se va a ejecutar.
public class Invoice {
private Customer customer;
private float price;
private int qty;
public Invoice (Customer customer, float price, int qty) {
[Link] = customer;
[Link] = price;
[Link] = qty;
}
public float calculateTotal (float vat, float discount) {
float subtotal = 0;
if ([Link]()) {
subtotal = (price * qty) - discount;
} else {
subtotal = (price * qty);
}
return subtotal * (1 + (vat/100));
}
}
2.15 Eliminar
chivato
de
control
Es frecuente que en se vean expresiones condicionales generalmente
dentro de bucles donde tras determinada comprobación se modifica un flag
para salir de ese bucle.
public class Friends {
private String[] friends;
public Friends (String[] friends) {
[Link] = friends;
}
public int indexOf (String friend) {
boolean found = false;
int i = 0;
while (i < [Link] && !found ) {
if (friends[i].equals(friend)) {
found = true;
}
i++;
}
if (found) {
return (i-1);
} else {
return -1;
}
}
}
[Link]
public class Flight {
private static final float CHILDREN_DISCOUNT = 0.9f;
private static final float UNEMPLOYED_DISCOUNT = 0.8f;
private static final int BASE_PRICE = 20;
private int distance;
public Flight (int distance) {
[Link] = distance;
}
public float priceForPassenger (Passenger passenger) {
float price = 0;
if ([Link]()) {
price = childDiscount();
} else {
if ([Link]()) {
price = unemployedDiscount();
} else {
if (isChristmas()) {
price = 0;
} else {
price = normalPrice();
}
}
}
return price;
}
private float unemployedDiscount() {
return BASE_PRICE * distance * UNEMPLOYED_DISCOUNT;
}
private float normalPrice() {
return BASE_PRICE * distance;
}
private boolean isChristmas() {
return false;
}
private float childDiscount() {
return BASE_PRICE * distance * CHILDREN_DISCOUNT;
}
}
[Link]
public class Bike extends Vehicle {
public Bike(int vehicleType, int speed, int acceleration) {
super(vehicleType, speed, acceleration);
}
@Override
public int move () {
return speed * 10;
}
}
[Link]
public class Plane extends Vehicle {
public Plane(int vehicleType, int speed, int acceleration) {
super(vehicleType, speed, acceleration);
}
@Override
public int move() {
return acceleration * 2;
}
}
2.18 Introducir
objeto
nulo
Cuando se usan objetos en una aplicación suele ser necesario
comprobar de forma frecuente si una instancia es nula. De hecho, se acaba
haciendo esa comprobación cada vez que se va a llamar a alguno de los
métodos de ese objeto.
Por ejemplo, supongamos que en un juego un guerrero puede hacer uso
o no de un arma para atacar. El arma es una clase muy simple:
public class Weapon {
private int damage;
public Weapon(int damage) {
[Link] = damage;
}
public int getDamage () {
return damage + new Random().nextInt(3);
}
}
Podemos crear por tanto una subclase de weapon que sobrescribe ese
método y simplemente retorna un valor mínimo fijo.
public class NullWeapon extends Weapon {
public NullWeapon(int damage) {
super(damage);
}
@Override
public int getDamage () {
return 1;
}
}
De esta manera el código queda mucho más simple ya que ahora sea
cual sea el arma podemos omitir la condicional:
public class Warrior {
private Weapon weapon;
public Warrior(Weapon weapon) {
[Link] = weapon;
}
public int attack() {
return 2 + [Link]();
}
}
2.19 Separar
consulta de
modificación
En la programación orientada a objetos siempre se busca el
desacoplamiento y por tanto debe diseñarse de tal manera de que cada clase
tenga un propósito concreto. Y a nivel de clase debe conseguirse también que
cada método tenga una función precisa, sin que pretenda hacer varias cosas a
la vez.
En ocasiones podemos encontrarnos con métodos que cambian valores
de los atributos y además retornar valores. Son métodos que quizá se fueron
complicando o no se separaron en distintos bloques por no crear métodos
distintos y por hacer "todo en uno".
Supongamos que tenemos un método que nos inicia un Vehículo y
además nos retorna el tipo:
public class Vehicle {
private int horsePower;
private String type;
public Vehicle (int power) {
initVehicleAndGetType(power);
}
private String initVehicleAndGetType(int power) {
horsePower = power;
if (power >= 10) {
type = "Truck";
} else if (power > 5 && power < 10) {
type = "Car";
} else {
type = "Bike";
}
return type;
}
public int getHorsePower() {
return horsePower;
}
public String getType() {
return type;
}
}
Ese método es como hacer un set y get a la vez, cuando lo suyo sería
separar esas funciones claramente.
public class Vehicle {
private int horsePower;
private String type;
public Vehicle(int power) {
setPower(power);
}
public void setPower(int power) {
horsePower = power;
}
public String getType() {
if (horsePower >= 10) {
return "Truck";
} else if (horsePower > 5 && horsePower < 10) {
return "Car";
} else {
return "Bike";
}
}
}
2.20 Parametrizar
método
Hay muchos métodos que pueden estar haciendo prácticamente lo
mismo cambiando únicamente un valor. Cuando veamos que tenemos varios
métodos común cuerpo similar es probable que podamos resumir todos esos
métodos en uno añadiendo un parámetro.
Supongamos que queremos cobrar algo distinto en función de la edad,
si se paga en metálico, etc.
[Link]
public class Customer {
private int age;
private boolean payInCash;
public Customer(int age, boolean payInCash) {
[Link] = age;
[Link] = payInCash;
}
public boolean payInCash() {
return payInCash;
}
public int getAge() {
return age;
}
}
[Link]
public class Invoice {
private float subtotal;
private Customer customer;
public Invoice(float subtotal, Customer customer) {
[Link] = subtotal;
[Link] = customer;
}
public float charge() {
if ([Link]() < 18) {
return chargeWithUnderageDiscount();
} else if ([Link]()) {
return chargeWithCashDiscount();
} else {
return chargeNormal();
}
}
private float chargeWithUnderageDiscount() {
float total = subtotal * 0.5f;
return total;
}
private float chargeWithCashDiscount() {
float total = subtotal * 0.8f;
return total;
}
private float chargeNormal() {
return subtotal;
}
}
2.21 Reemplazar
parámetro
con método
explicito
Este sería el caso inverso a la refactorización de parametrizar método.
Se da en situaciones en las que se pasa más de un parámetro a un método
según los valores de ese parámetro y se hace una cosa distinta según esos
valores. Lo correcto es crear métodos distintos para evitar esas condicionales.
Supongamos que tenemos un método de inicialización en las que se
inicia un atributo en función del valor del parámetro type:
public class Vehicle {
private int acceleration;
private int speed;
public Vehicle(int acceleration, int speed) {
[Link] = acceleration;
[Link] = speed;
}
public void initVehicle (int type, int value) {
if (type == 1) {
acceleration = value;
return;
}
if (type == 2 || type == 3) {
speed = value;
return;
}
}
public int getAcceleration() {
return acceleration;
}
public int getSpeed() {
return speed;
}
}
2.22 Sustituir
algoritmo
En programación existe más de una solución para el mismo problema,
generalmente unas mejores que otras. Puede que alguno de los pasos que
utilicemos no sean los más óptimos as que podemos optar por reescribir
completamente ese código.
Por ejemplo, supongamos que tenemos un código que devuelve un tipo
de iva según un código.
public class Order {
public float applyVAT (int vatType) {
float result = 0;
switch (vatType) {
case 1:
result = 1.04f;
break;
case 2:
result = 1.18f;
break;
case 3:
result = 1.21f;
break;
default:
result = -1;
break;
}
return result;
}
}
2.23 Extraer
clase
En algunas clases puede que estemos haciendo un trabajo extra que
quizá pueda hacerse por otra clase. Por norma general una clase tiene que
hacer algo muy concreto, pero puede ocurrir que acabe acumulando cierta
complejidad.
Pongamos por ejemplo una clase que representa un cliente, y uno de sus
atributos es la tarjeta de crédito. Ese es un dato complejo que además de más
de un atributo puede tener métodos en exclusiva para tratarlo, como por
ejemplo la validación.
public class Customer {
private String name;
private String dni;
private String creditCard;
private Date creditCardDate;
private int creditCardControlNumber;
public Customer(String name, String dni) {
[Link] = name;
[Link] = dni;
}
public boolean isCardExpired () {
return [Link](new Date());
}
public boolean isValid () {
boolean result = false;
// Some code here...
return result;
}
// getters/setters…
}}
Es normal que una clase crezca, pero hay un punto en el que las
responsabilidades pueden sobrepasar lo razonable. Así que conviene separar
aquellas partes que puedan constituir una clase por si misma:
public class Customer {
private String name;
private String dni;
private CreditCard creditCard;
2.24 Inline
class
Este sería la refactorización inversa de Extraer Clase. Si una clase no
tiene mucho sentido porque apenas tiene atributos o no tiene funcionalidad
especifica alguna. Suele suceder que a una clase se le van quitando
responsabilidades y finalmente se deja tan vacía de contenido que al final se
opta por devolver esos atributos a la clase inicial.
Supongamos que tenemos una clase para representar el Sexo de una
persona.
public class Person {
private String name;
private Date birth;
private Sex sex;
public Person(String name, Date birth, int sexCode) {
[Link] = name;
[Link] = birth;
[Link] = new Sex(sexCode);
}
// getters/setters…
}
[Link]
public class Sex {
public static final int MALE = 0;
public static final int FEMALE = 1;
private int sex;
public Sex (int sex) {
[Link] = sex;
}
public int getSex () {
return sex;
}
@Override
public String toString() {
return [Link](sex);
}
}
No merece la pena tener una clase solo para eso as que hacemos lo
siguiente:
public class Person {
private String name;
private Date birth;
public static final int MALE = 0;
public static final int FEMALE = 1;
private int sex;
public Person(String name, Date birth, int sexCode) {
[Link] = name;
[Link] = birth;
[Link] = sexCode;
}
// getters/setters…
}
2.25 Ocultar
delegado
Una clase puede estar haciendo llamadas directas a otra clase que no es
ms que una delegada o de menor importancia que depende de otra. La
encapsulación, una de las claves de la programación orientada a objetos
promueve que se creen componentes reutilizables como cajas negras, cuyos
detalles de implementación no se deben conocer. Así que conviene que una
clase no conozca muchos detalles del sistema y, por lo tanto, una clase no
debería poder utilizar directamente una clase que ya depende de otra a la que
ya accede.
Supongamos que tenemos un sistema como el siguiente donde una clase
Main utiliza una clase Player que a su vez maneja una clase Die.
[Link]
public class Main {
private Player player;
private Die die;
public Main () {
init();
}
private void init () {
player = new Player();
die = new Die();
}
public int roll () {
return [Link]();
}
}
[Link]
public class Player {
private Die die;
public Player () {
[Link] = new Die();
}
public int roll () {
return [Link]();
}
}
[Link]
public class Die {
private Random random = new Random();
public int roll() {
return [Link](6) + 1;
}
}
2.26 Introducir
método
externo
A veces puede que estemos utilizando libreras desarrolladas por otros
que están compiladas o cuyo código fuente no está disponible. Esta
refactorización se necesita cuando se requiere un método en una de esas
clases que no se puede modificar. Llamaremos servidor a esa clase.
Lo que se hace en esos casos es crear un método en nuestra clase que
utilice una instancia de Servidor como primer argumento.
2.27 Introducir
Extension
Local
De forma similar a la anterior, nos puede pasar que una clase que
utilizamos necesitaría tener métodos adicionales, pero no tenemos posibilidad
de editarla. Supongamos por ejemplo que tenemos una clase que nos hace las
conversiones monetarias y necesitamos añadir otras:
[Link]
public class Conversor {
public double euro2Dollar (double qty) {
return qty * 1.4d;
}
public double dollar2Euro (double qty) {
return qty * 0.6d;
}
}
[Link]
public class Main {
private Conversor conversor = new Conversor();
public double convert (double amount) {
return conversor.euro2Dollar(amount);
}
}
[Link]
public class Main {
private CoolConversor conversor = new CoolConversor();
public double convert (double amount) {
return conversor.euro2Dollar(amount);
}
}
2.28 Reemplazar
un dato con
un objeto
En las clases puede haber un dato que en un principio hemos
representado como un dato simple que al cabo del tiempo requiere datos o
comportamiento adicional. Por ejemplo, supongamos que tenemos una clase
que representa a un cliente:
public class Customer {
private String name;
private String email;
private String address;
public Customer(String name, String email, String address) {
[Link] = name;
[Link] = email;
[Link] = address;
}
// getters/setters…
// getters/setters…
[Link]
public class Address {
private String street;
private String po;
private String city;
public Address(String street, String po, String city) {
[Link] = street;
[Link] = po;
[Link] = city;
}
@Override
public String toString() {
return city;
}
private int proximity(Address address) {
...
}
}
2.29 Reemplazar
array con
objeto
Las estructuras de arrays se deberían utilizar simplemente para guardar
un conjunto de elementos del mismo tipo que representen un único concepto
o cuyo sentido sea estar agrupados. Si se hacen las cosas mal puede darse la
situación en la que se acabe creando un array para acumular distintos datos
sin una relación entre sí.
public class Airplane {
private String model;
private String pilotData[] = new String[3];
public Airplane(String model) {
[Link] = model;
}
public void initPilot(String name, String license, int flightHours) {
pilotData[0] = name;
pilotData[1] = license;
pilotData[2] = [Link](flightHours);
}
@Override
public String toString() {
return "Airplane [model=" + model + ", pilot=" + pilotData[0] + "]";
}
}
[Link]
public class Pilot {
private String name;
private String license;
private int flightHours;
public Pilot (String name, String license, int flightHours) {
[Link] = name;
[Link] = license;
[Link] = flightHours;
}
public String toString() {
return name;
}
}
2.30 Encapsular
colección
La encapsulación es una vez más la clave para entender esta
refactorización. Las clases no deben exponer al exterior nada que no sea
realmente requerido, y por lo tanto una de las primeras medidas es hacer
privados todos los atributos. En el caso de tipos primitivos u objetos simples
basta con hacerlos privados y proveer de métodos get/set. Pero ¿qué pasa si el
atributo es una colección o un conjunto de valores? Supongamos la clase
Team que representa un equipo de deportistas y que contiene una colección
de jugadores:
public class Team {
private String name;
private Date creation;
private ArrayList<Player> players = new ArrayList<Player>();
public Team(String name, Date creation) {
[Link] = name;
[Link] = creation;
}
public String getName() {
return name;
}
public Date getCreation() {
return creation;
}
public ArrayList<Player> getPlayers() {
return players;
}
public int totalPlayers() {
return [Link]();
}
}
[Link]
public class Player {
private String name;
private int number;
public Player(String name, int number) {
[Link] = name;
[Link] = number;
}
// getters/setters…
2.31 Pull
up
En las clases que mantienen una relación de herencia, por lo general, la
superclase contiene atributos y métodos que son comunes a todas las clases
descendientes y, por su lado, estas subclases contienen atributos o métodos
que son específicamente suyos (más posibles métodos sobrescritos).
Conforme las clases se van desarrollando más es probable en todas las
subclases se acabe poniendo un miembro que es común en todas y, por tanto,
conviene subir o hacer push up de ese elemento a la superclase. De esta
forma simplificamos, centralizamos y facilitamos el mantenimiento de la
jerarquía de clases.
Supongamos que tenemos una jerarquía como la siguiente:
[Link]
public class Vehicle {
protected String name;
}
[Link]
public class MotorBike extends Vehicle {
private String plate;
private String helmet;
public void start() {
}
}
[Link]
public class Car extends Vehicle {
private String plate;
private String trunk;
private boolean isTrunkOpened;
public void start() {
}
public boolean isTrunkOpen() {
return isTrunkOpened;
}
}
Como se puede ver tanto Car como Bike tienen un atributo y un método
comunes. Lo lógico es que siendo así se traslade a la superclase.
[Link]
public class Vehicle {
protected String name;
protected String plate;
public void start() {
}
}
[Link]
public class MotorBike extends Vehicle {
private String helmet;
}
[Link]
public class Car extends Vehicle {
private String trunk;
private boolean isTrunkOpened;
public boolean isTrunkOpen() {
return isTrunkOpened;
}
}
2.32 Push
Down
Este sería el contrario al Push up. En una jerarquía de clases tenemos
una superclase que contiene algún atributo o método que solamente es
utilizado por alguna de las subclases. En esos casos lo que hacemos es sacar
ese elemento de la clase padre y bajarlo (pull down) a la subclase o subclases
que lo necesiten.
Supongamos que tenemos una jerarquía similar a la anterior pero que
esta vez incluye la clase que representa a la bicicleta:
[Link]
public class Vehicle {
private String name;
private String plate;
private Insurance insurance;
public void start() {
}
}
[Link]
public class MotorBike extends Vehicle {
private String type;
}
[Link]
public class Bicycle extends Vehicle {
private String helmet;
}
[Link]
public class Car extends Vehicle {
private String trunk;
private boolean isTrunkOpened;
public void start() {
}
public boolean isTrunkOpen() {
return isTrunkOpened;
}
}
Como se puede apreciar la clase Vehicle tiene un atributo para
representar el seguro y un método de arrancar, ambos sin mucho sentido para
una bicicleta convencional. Así que debemos sacar esos miembros
específicos de la superclase y dejarlos únicamente en aquellas subclases
donde encajen.
[Link]
public class Vehicle {
private String name;
private String plate;
public void start() {
}
}
[Link]
public class MotorBike extends Vehicle {
private String type;
private Insurance insurance;
}
[Link]
public class Bicycle extends Vehicle {
private String helmet;
}
[Link]
public class Car extends Vehicle {
private String trunk;
private boolean isTrunkOpened;
private Insurance insurance;
public void start() {
}
public boolean isTrunkOpen() {
return isTrunkOpened;
}
}
Obviamente esto puede variar en función del número de subclases
afectadas. También se podría plantear crear una jerarquía más compleja
donde por ejemplo se distinguieran vehículos de motor y no motor. Según la
situación y según la evolución, el push down y el pull up se tendrán que
aplicar.
1 LISTADO DE
REFACTORIZACIONES
MÁS RECONOCIDAS
A continuación, se enumeran todas las refactorizaciones más habituales
aportando su nombre en inglés con la intención de facilitar la búsqueda de
ejemplos en la red.
1. Extraer método: Extract Method
Si un método se vuelve demasiado largo hay que procurar dividirlo
tratando de crear métodos que además sean reutilizables.
2. Método inline: Inline method
3. Temporal Inline: Inline Temp
4. Introducir variable explicativa: Introduce Explaining Variable
5. Dividir variable temporal: Split Temporary Variable
6. Eliminar asignaciones a parámetros: Remove Assignments to
Paramenters
7. Reemplazar método con Método de Objeto: Replace Method with
Method Object
8. Sustituir algoritmo: Substitute Algorithm
9. Mover método: Move Method
10. Mover campo: Move Field
11. Extraer clase: Extract Class
12. Clase Inline : Inline Class
13. Ocultar Delegado: Hide Delegate
14. Eliminar intermediario: Remove Middle Man
15. Introducir método externo: Introduce Foreign Method
16. Introducir extension local: Introduce Local Extension
17. Auto encapsular campos: Self Encapsulate Field
18. Reemplazar valores de datos con objeto: Replace Data Value with
Object
19. Cambiar de valor a referencia: Change Value to Reference
20. Cambiar de referencia a valor: Change Reference to Value
21. Reemplazar array con objeto: Replace Array with Object
22. Duplicar datos observados: Duplicate Observed Data
23. Cambiar asociación unidireccional a bidireccional: Change
Unidirectional Association to Bidirectional
24. Cambiar asociación bidireccional a unidireccional: Change
bidirectional Association to Unidirectional
25. Reemplazar número mágico con constante simbólica: Replace
Magic Number with symbolic Constant
Se trata de no mostrar ningún valor numérico constante cuyo
significado se desconozca. Para ello basta con cambiarlo por una constante y
poder poner así un texto en lugar del número:
Ejemplo:
totalFactura = subtotal * 0.16;
2.33 Refactorizaciones
a gran escala:
Separar Herencia: Tease Apart Inheritance
Convertir diseño procedimental en objetos: Convert
Procedural Design to Objects
Separar dominio de presentación: Separate Domain from
Presentation
Extraer jerarquía: Extract Hierarchy
5 BIBLIOGRAFÍA
Sobre refactorización solo cabe mencionar un libro, imprescindible, que
sigue estando de actualidad y que además cuya lectura resulta amena:
Refactoring: improving the design of existing code. Martin Fowler [Link].
Capítulo 8
Patrones de Software
1 INTRODUCCIÓN
La programación orientada a objetos es un paradigma que nos permite
desarrollar soluciones de software dividiendo un problema en objetos. En un
proyecto orientado a objetos se diseñan componentes que reflejan la
naturaleza del problema o dominio y de todos los elementos que forman parte
del él.
La mayor parte del trabajo de un proyecto de software se hace en su
mantenimiento a lo largo del tiempo. Para que ese mantenimiento y el
desarrollo de nuevas funcionalidades se lleve a cabo de forma eficiente, los
componentes deben procurar cumplir una serie de requisitos, entre los que
destacan, además de la encapsulación, el desacoplamiento entre sus
elementos. Todo eso nos permite crear componentes reutilizables,
independientes, extensibles y, en definitiva, mucho más fáciles de mantener.
No todos los proyectos son iguales, pero en el desarrollo de soluciones
sin duda se producen escenarios que, abstrayéndonos de lo concreto, son
similares en cuanto a la relación y la forma de interactuar entre los elementos.
Es ahí donde se pueden aplicar soluciones que llevan décadas demostrando
ser las más apropiadas: los patrones de software.
Los patrones de software son, en cierto modo, recetas bien conocidas
que han demostrado ser muy eficaces. Pueden aplicarse en situaciones muy
concretas y nos ofrecen una solución probada y bien conocida por la
comunidad de desarrolladores.
1.3 Gang
of
Four
Una de las publicaciones más influyentes en el ámbito del desarrollo de
software fue Design Patterns, elements of reusable design (E. Gama, R.
Helm, R. Johnson y John Vlissides), en 1995. Aunque obviamente los
patrones ya estaban presentes en la industria mucho antes, esta publicación
agrupó de forma sistemática los más relevantes.
No todos los patrones siguen con la misma vigencia, y sin embargo
muchos de ellos se siguen aplicando al margen del lenguaje y los nuevos
entornos de proyectos (Web, móviles, etc.). En este caso haremos un repaso a
todos ellos utilizando el lenguaje Java.
1.1 Conceptos
Antes de mostrar el catálogo de patrones conviene aclarar algunas de
las palabras que aparecen a lo largo del texto, ya que representan conceptos
distintos a los de la definición normal
Interface: se refiere al elemento de software, que representa
una abstracción sin implementación alguna. No tiene que
ver con el concepto de interfaz de usuario.
Sistema: se refiere al proyecto de software, al conjunto de
clases que estamos desarrollando.
Cliente: se refiere a un software, una clase, que utiliza otra
clase. Puede ser un software que hace llamadas a nuestro
sistema.
2 PATRONES
Los patrones de software clásicos se agrupan en tres grandes familias:
1. Creaciones o Creational
2. Estructurales o Structural
3. Comportamiento o Behavioral
Existen diferentes escenarios donde los patrones son aplicables. En
algunos casos pueden plantearse desde el inicio, pero a veces se aplican
cuando nos encontramos uno o varios proyectos ya hechos y necesitamos
poder vincularlos sin necesidad de modificar el código existente.
A continuación, se muestra cada uno de los patrones, con su definición,
escenarios donde puede usarse y su código de ejemplo.
2.1 Patrones
creaciones
Se trata de los patrones que se aplican en escenarios donde se necesita
crear objetos que por distintos motivos no puedes hacerse de manera
convencional.
2.1.1 Abstract
Factory
Provee un interfaz para crear conjuntos de objetos relacionados o
dependientes sin necesidad de especificar sus clases específicas.
Escenarios
Cuando la creación, composición y representación de
instancias debe ser independiente del sistema que las utiliza.
Cuando los sistemas deben ser capaces de utilizar diferentes
familias de objetos.
Las familias de objetos deben ser utilizadas juntas y
queremos reforzar esta condición.
Para la publicación de librerías sin exponer los detalles de
implementación, únicamente los interfaces.
Cuando las clases concretas deben ser desacopladas de los
clientes.
Diagrama
Tenemos una clase abstracta Factory que es capaz de generar otras
clases abstractas:
Y por otro tenemos las implementaciones para los tipos Human y
Orcish
[Link]
public abstract class Character {
protected String name;
protected int strength;
protected int speed;
protected int armor;
public abstract int attack();
public abstract int defend();
public abstract void walk(int position);
public abstract String toString();
}
[Link]
public abstract class Weapon {
protected String name;
protected int type;
public abstract int attackPoints();
public abstract int defensePoints();
}
[Link]
public abstract class GameElementFactory {
public GameElementFactory(){}
public abstract Character createCharacter ();
[Link]
public class HumanElementFactory extends GameElementFactory {
@Override
public Character createCharacter() {
return new HumanCharacter();
}
@Override
public Building createBuilding() {
return new HumanBuilding();
}
@Override
public Weapon createWeapon() {
return new HumanWeapon();
}
}
[Link]
public class OrcishElementFactory extends GameElementFactory {
@Override
public Character createCharacter() {
return new OrcishCharacter();
}
@Override
public Building createBuilding() {
return new OrcishBuilding();
}
@Override
public Weapon createWeapon() {
return new OrcishWeapon();
}
}
[Link]
public class HumanWeapon extends Weapon {
@Override
public int attackPoints() {
return 0;
}
@Override
public int defensePoints() {
return 0;
}
}[Link]
[Link]
public class Hero {
private String name;
private String race;
private Armor armor;
private Weapon weapon;
private Spell spell;
// getters/setters…
}
[Link]
public class Director {
public void createHero (HeroBuilder heroBuilder) {
[Link]();
[Link]();
[Link]();
[Link]();
}
}
[Link]
public class HumanHeroBuilder extends HeroBuilder {
@Override
public void createHero() {
[Link] = new Hero();
[Link]("human");
}
@Override
public void buildArmor() {
Armor armor = new Armor();
[Link]("Human armor");
[Link](3);
[Link](100);
[Link](armor);
}
@Override
public void buildSpell() {
Spell spell = new Spell();
[Link]("Heal");
[Link](20);
[Link](spell);
}
@Override
public void buildWeapon() {
Weapon weapon = new HumanWeapon();
[Link](weapon);
}
@Override
public Hero getHero() {
return hero;
}
}
[Link]
public class BuilderClient {
public Hero createHero() {
HeroBuilder heroBuilder = new HumanHeroBuilder();
Director director = new Director();
// here we pass our builder, and an extra parameter.
[Link](heroBuilder);
return [Link]();
}
}
2.1.3 Factory
Method
Define un interface para crear un objeto, pero déjà en mano de las
subclases qué clase concreta instanciar. Este patrón, por tanto, deja en manos
de las subclases la creación de instancias.
Escenarios
Cuando una clase no sabe que tipo de clases necesitará
crear.
Cuando las subclases podrían especificar qué objetos
deberían ser instanciados.
Las clases padre delegan la creación de objetos a las
subclases.
Diagrama
Tenemos una clase abstracta de la que heredan tres subclases
específicas:
Y utilizamos una clase Factory para poder generar instancias de esas
subclases:
Código
[Link]
public abstract class Treasure {
private String name;
private int value;
// getters/setters…
}
[Link]
public class Diamond extends Treasure {
private int mana;
@Override
public String getType() {
return "Diamond";
}
@Override
public String toString() {
return [Link]() + " Mana: " + mana;
}
}
[Link]
public class GoldCoin extends Treasure {
private int amount;
@Override
public String getType() {
return "GoldCoin";
}
@Override
public String toString() {
return [Link]() + " Amount: " + amount;
}
}
[Link]
public class Jewel extends Treasure {
@Override
public String getType() {
return "Jewel";
}
}
[Link]
public class TreasureFactory {
private static final int DIAMOND = 0;
private static final int JEWEL = 1;
private static final int GOLDCOIN = 2;
public Treasure createTreasure(int type) {
switch (type) {
case DIAMOND : return new Diamond();
case JEWEL : return new Jewel();
case GOLDCOIN : return new GoldCoin();
default: return new GoldCoin();
// or this could be an extension
// return [Link](type);
}
}
}
[Link]
import [Link];
import [Link];
public class FactoryClient {
private static final int TOTAL_TREASURES = 10;
public void createTreasures () {
TreasureFactory treasureFactory = new TreasureFactory();
Random random = new Random();
ArrayList<Treasure> treasures = new ArrayList<Treasure>();
for (int i = 0; i< TOTAL_TREASURES;i++) {
[Link]([Link]([Link](3)));
}
for (Treasure t : treasures) {
[Link]("Treasure created: " + [Link]());
[Link]("\tTreasure details: " + [Link]());
}
}
}
2.1.4 Prototype
Especifica los tipos de objetos a crear utilizando un prototipo de
instancia, y utiliza una copia de ese prototipo para crear nuevos objetos.
Escenarios
Cuando la creación, composición y representación de los
objetos debe desacoplarse del sistema
La creación inicial de cada objeto es una operación costosa
Las clases que deben crearse se especifican en tiempo de
ejecución.
En los objetos existen un número limitado de
combinaciones de estados. Por tanto, compensa definir unos
prototipos y clonarlos, ajustando ese estado posteriormente.
Para evitar crear una jerarquía de clases Factory en paralelo
a las clases que se deben producir.
Objetos o estructuras de objetos deben ser idénticos o deben
parecerse a otros objetos ya existentes.
Diagrama
Tenemos una clase abstracta que hace de prototipo para el resto:
Y por otro tenemos una clase Factory que es la que utilizará el cliente
para clonar las extensiones del prototipo. Observa que la Factory no tiene
relación directa con la clase abstracta. Simplemente clona instancias de Boar,
Seal y Sheep.
Código
[Link]
public abstract class BeastPrototype {
protected int defense;
protected int mana;
public abstract BeastPrototype clone();
public abstract String getType();
public int getDefense() {
return defense;
}
public void setDefense(int defense) {
[Link] = defense;
}
public int getMana() {
return mana;
}
public void setMana(int mana) {
[Link] = mana;
}
}
[Link]
public class Boar extends BeastPrototype {
private static final int BOAR_DEFENSE = 5;
private static final int BOAR_MANA = 5;
public Boar() {
defense = BOAR_DEFENSE;
mana = BOAR_MANA;
}
@Override
public Boar clone() {
return new Boar();
}
@Override
public String getType() {
return "Boar";
}
}
[Link]
public class Sheep extends BeastPrototype {
private static final int SHEEP_DEFENSE = 2;
private static final int SHEEP_MANA = 4;
public Sheep () {
defense = SHEEP_DEFENSE;
mana = SHEEP_MANA;
}
@Override
public Sheep clone() {
return new Sheep();
}
@Override
public String getType() {
return "Sheep";
}
}
[Link]
public class BeastPrototypeFactory {
private Boar boar;
private Sheep sheep;
private Seal seal;
public BeastPrototypeFactory () {
boar = new Boar();
seal = new Seal();
sheep = new Sheep();
}
public Boar createBoar () {
return [Link]();
}
public Sheep createSheep () {
return [Link]();
}
public Seal createSeal () {
return [Link]();
}
}
[Link]
public class PrototypeClient {
public void createPrototypes () {
ArrayList<BeastPrototype> beasts = new ArrayList<BeastPrototype>();
BeastPrototypeFactory beastFactory =
new BeastPrototypeFactory();
for (int i=0;i<3;i++) {
[Link]([Link]());
[Link]([Link]());
[Link]([Link]());
}
for (BeastPrototype beast : beasts) {
[Link]();
}
}
}
2.1.5 Singleton
Nos permite garantizar que solamente existe una instancia de una clase,
ocultando el constructor y solo permitiendo un punto de acceso a su instancia
única.
Escenarios
Se requiere que una clase tenga únicamente una instancia
Se precisa un acceso controlado a una instancia única a
través de un único punto o llamada.
Cuando la instancia única debe ser extendida mediante una
subclase y los clientes deben ser capaces de hacerlo sin
modificar el código de la clase base.
Diagrama
Código
[Link]
public class Die {
// The reference to itself
private static Die die;
private final static int SIDES = 6;
private Die () { }
/**
* This method is called first whenever
* we want to roll. Using getInstance is rather formal
*/
public static Die getInstance() {
if (null == die) {
die = new Die();
}
return die;
}
public int roll () {
return (int) ([Link]() * SIDES) + 1;
}
}
[Link]
public class SingletonClient {
/**
* method to roll the die 10 times
*/
public void rollDie () {
[Link]("Let's roll the die");
for (int i=0;i<10;i++) {
[Link]([Link]().roll());
}
}
public static void main (String args[]) {
new SingletonClient().rollDie();
}
}
2.2 Patrones
estructurales
Son los patrones que permiten crear estructuras para poder relacionar
clases de distinta manera.
2.2.1 Adapter
Convierte el interface de una clase en otro interface que se ajusta más a
lo que precisa el cliente. Este patrón permite que clases que eran
incompatibles puedan relacionarse
Escenarios
Necesitamos usar una clase existente que no cumple con los
requerimientos de un interface.
Necesitas crear una clase reutilizable que sea capaz de
interactuar con clases no relacionadas o imprevistas, en
definitiva, con clases incompatibles.
Necesitas el acceso a múltiples subclases pero es más
práctico adaptar el interface de la clase base que crear
subclases de las mismas para poder utilizarlas.
Condiciones complejas vinculan el comportamiento de una
clase a su estado
La transición entre estados debe ser explícita
Diagrama
Tenemos un interface para manejar un conjunto de ítems, pero nuestra
clase no es compatible con él.
Por lo tanto, creamos una clase Adaptar para que el cliente pueda
relacionar ambas:
Código
[Link]
public interface Inventory {
public void addItem (Item item);
public void removeItem (int itemIndex);
public String showAll();
}
[Link]
public class Item {
private String name;
// getters/setters…
}
[Link]
public class Equipment extends ArrayList {
public void addElement (Object object) {
[Link](object);
}
public void delete (int index) {
[Link](index);
}
}
[Link]
public class EquipmentAdapter extends Equipment implements Inventory {
public void addItem(Item item) {
[Link](item);
}
public void removeItem(int itemIndex) {
[Link](itemIndex);
}
public String showAll() {
String result = "";
for (int i = 0; i < [Link](); i++) {
result += [Link](i).toString();
}
return result;
}
}
[Link]
public class AdapterClient {
public void useEquipment () {
EquipmentAdapter equipment = new EquipmentAdapter();
[Link](new Item());
[Link](new Item());
[Link](new Item());
}
}
2.2.2 Bridge
Desacopla una clase partiéndola en su abstracción y en su
implementación de tal manera que ambas pueden variar de forma
independiente.
Escenarios
Las abstracciones y las implementaciones no deben estar
vinculadas en el momento de compilar o ejecutar.
Las abstracciones y las implementaciones deberían ser
extensibles y, además, de manera independiente.
El cambio en la implementación de una abstracción no debe
afectar a los clientes.
Los detalles de implementación deben ocultarse al cliente.
Necesitas compartir una implementación entre muchos
objetos y quieres ocultarlo a los clientes.
Diagrama
Tenemos un interface para hacer una operación de asignación, y
creamos dos implementaciones distintas:
Por un lado, tenemos una clase abstracta llamada Character, a las que
aplicaremos distintas asignaciones:
Código
[Link]
public interface Assignation {
public static final int TOTAL_VALUES = 3;
public static final double MAX_VALUE = 10;
public int[] generateValues();
}
[Link]
public class RandomAssignation implements Assignation {
/**
* implements generateValues using random values
*/
public int[] generateValues() {
int [] values = new int[Assignation.TOTAL_VALUES];
for (int i = 0; i<[Link]; i++) {
values[i] = (int) ([Link]() * Assignation.MAX_VALUE);
}
return values;
}
}
[Link]
public class BalancedAssignation implements Assignation {
/**
* generate values in a compensated manner
*/
public int[] generateValues() {
int [] values = new int[Assignation.TOTAL_VALUES];
for (int i = 0; i<[Link]; i++) {
values[i] = (int) ([Link]() * (Assignation.MAX_VALUE/2) +
(Assignation.MAX_VALUE/2));
}
return values;
}
}
[Link]
public abstract class Character {
protected Assignation assignationMethod;
private int strength;
private int speed;
private int intelligence;
protected Character (Assignation assignationMethod) {
[Link] = assignationMethod;
}
public abstract void generateCharacter ();
// getters/setters…
}
[Link]
public class ComputerCharacter extends Character {
private String tribe;
protected ComputerCharacter(Assignation assignationMethod, String tribe) {
super(assignationMethod);
[Link] = tribe;
}
/**
* generate and assign character attributes.
* We add 1, just in case.
*/
@Override
public void generateCharacter() {
int[] values = [Link]();
setStrength(values[0] + 1);
setSpeed(values[1] + 1);
setIntelligence(values[2] + 1);
}
public String getTribe() {
return tribe;
}
public void setTribe(String tribe) {
[Link] = tribe;
}
}
[Link]
public class PlayerCharacter extends Character {
private String name;
protected PlayerCharacter(Assignation assignationMethod, String name) {
super(assignationMethod);
[Link] = name;
}
@Override
public void generateCharacter() {
int[] values = [Link]();
setStrength(values[0]);
setSpeed(values[1]);
setIntelligence(values[2]);
}
public String getName() {
return name;
}
public void setName(String name) {
[Link] = name;
}
}
[Link]
public class Main {
public static void main(String[] args) {
PlayerCharacter player
= new PlayerCharacter(new RandomAssignation(), "Eugene");
ComputerCharacter computer
= new ComputerCharacter(new BalancedAssignation(), "Bob");
[Link]();
[Link]();
[Link](player);
[Link](computer);
}
}
2.2.3 Composite
Compone los objetos en estructuras de árbol para representar jerarquías
de parto o del todo. Este patrón permite a los clientes tratar a objetos
individuales y sus composiciones de manera uniforme.
Escenarios
Se precisan representaciones de objetos jerárquicas.
Los objetos y las composiciones de objetos deben tratarse de
la misma manera. Los clientes deben poder ignorar las
diferencias entre los objetos de la jerarquía, sean
compuestos o individuales.
Diagrama
Tenemos un interface llamado WarUnitComponent que es
implementado por una clase llamada Squad, la cual es un conjunto de
componentes. A su vez, tenemos otras clases que implementan una instancia
de WarUnit.
Código
[Link]
public interface WarUnitComponent {
public void add (WarUnitComponent unit);
public void del (WarUnitComponent unit);
public int attack();
public int defend();
public ArrayList<WarUnitComponent> getChild();
}
[Link]
public class Squad implements WarUnitComponent {
private ArrayList<WarUnitComponent> units = new ArrayList<WarUnitComponent>();
public void add(WarUnitComponent unit) {
[Link](unit);
}
public void del(WarUnitComponent unit) {
[Link](unit);
}
public int attack() {
int total = 0;
for (WarUnitComponent unit: units) {
total = total + [Link]();
}
return total;
}
public int defend() {
int total = 0;
for (WarUnitComponent unit: units) {
total = total + [Link]();
}
return total;
}
/**
* if it's a composite, it holds more components.
*/
public ArrayList<WarUnitComponent> getChild() {
return units;
}
}
[Link]
public class Knight implements WarUnitComponent {
private static final int KNIGHT_MODIFIER = 4;
/**
* adds a new WarUnitComponent, but not applicable here
*/
public void add(WarUnitComponent unit) {
// there is a choice between transparency and safety
// In these case, as we area extending an interface
// we leave this blank.
}
public void del(WarUnitComponent unit) {
// same ass add method
}
public int attack() {
return [Link]().roll() + KNIGHT_MODIFIER;
}
public int defend() {
return [Link]().roll() + (KNIGHT_MODIFIER - 1);
}
/**
* if it's a composite, it holds more components.
*/
public ArrayList<WarUnitComponent> getChild() {
return null;
}
}
[Link]
public class Bowman implements WarUnitComponent {
private static final int BOWMAN_MODIFIER = 2;
public void add(WarUnitComponent unit) {
// there is a choice between transparency and safety
// In these case, as we area extending an interface
// we leave this blank.
}
[Link]
public class CompositeClient {
public void useComposite () {
Knight knight1 = new Knight();
Knight knight2 = new Knight();
Knight knight3 = new Knight();
Knight knight4 = new Knight();
Bowman bowman1 = new Bowman();
Bowman bowman2 = new Bowman();
Bowman bowman3 = new Bowman();
Squad squad = new Squad();
[Link](knight1);
[Link](bowman1);
[Link](bowman2);
[Link]("knight 4 attack: " + [Link]());
[Link]("Squad attack: " + [Link]());
}
}
2.2.4 Decorator
Otorgar responsabilidades adicionales a un objeto de manera dinámica.
Suponen una alternativa muy flexible a la creación de subclases para extender
las funcionalidades.
Escenarios
Poder añadir nuevas responsabilidades a los objetos de manera
dinámica y de forma transparente.
No se pueden crear subclases para conseguir modificaciones.
Las responsabilidades y comportamientos de los objetos deben
ser dinámicamente modificables.
Las implementaciones deben poder desacoplarse de las
responsabilidades y los comportamientos.
Una funcionalidad específica no puede residir en una clase base o
en la parte alta de la jerarquía de clases.
Para responsabilidades que puede ser retiradas.
Cuando se tiende a crear una jerarquía de clases demasiado
grande para cubrir todas las posiblees variantes de funcionalidad
a base de crear extensiones.
Diagrama
En principio tenemos un interface que representa un personaje que es
implementado de forma directa por una clase:
Pero podemos implementar el Character con una clase aplicando este patrón
y creando instancias de personaje a partir de esa extensión y no del interface.
Código
[Link]
public interface Character {
public int move ();
public int attack();
public int defend();
}
[Link]
public class SimpleCharacter implements Character {
private String name;
public SimpleCharacter (String name) {
[Link] = name;
}
public int move() {
return 3;
}
public int attack() {
return 3;
}
public int defend() {
return 3;
}
public String toString() {
return "Simplest character. Name: " + name;
}
}
[Link]
public class CharacterDecorator implements Character {
protected Character extendedCharacter;
public CharacterDecorator(Character extendedCharacter) {
[Link] = extendedCharacter;
}
// delegates move call
public int move() {
return [Link]();
}
// delegates attack call
public int attack() {
return [Link]();
}
// delegates defend call
public int defend() {
return [Link]();
}
public String toString() {
return [Link]();
}
}
[Link]
public class Orc extends CharacterDecorator {
private static final int ORCISH_MODIFIER = 3;
public Orc (Character extendedCharacter) {
super(extendedCharacter);
}
private int orcModifier () {
return (int) (([Link]() * ORCISH_MODIFIER) + ORCISH_MODIFIER);
}
@Override
public int move() {
return orcModifier() + [Link]();
}
@Override
public int attack() {
return orcModifier() + [Link]();
}
@Override
public int defend() {
return orcModifier() + [Link]();
}
@Override
public String toString() {
return [Link]() + " and I'm an orcish character";
}
}
[Link]
public class DecoratorClient {
public void useCharacters () {
Character hordeCharacter= new Orc(new SimpleCharacter("Thrall"));
[Link]([Link]());
}
}
2.2.5 Facade
Provee una interface única para un conjunto de interfaces de nuestro
sistema. Es una fachada que se pone por encima del sistema para que este sea
más fácil de usar.
Escenarios
Los sistemas y subsistemas se deben organizar en capas
dando acceso de una a otra través de fachadas.
Se precisa interface simple para dar acceso a un sistema
complejo.
Existen muchas dependencias entre la implementación del
sistema y los clientes y se precisa desacoplarlos.
Diagrama
Como se puede apreciar, la clase Facade se encarga de la generación de
todos los elementos y el cliente no debe preocuparse de los detalles.
Código
[Link]
public class Treasure {
private String name;
private int value;
public Treasure(String name, int value) {
[Link] = name;
[Link] = value;
}
// getters/setters
}
[Link]
public class Trap {
private static final int TRAP_DAMAGE = 6;
private String name;
private int damage;
public Trap(String name) {
[Link] = name;
damage = (int) ([Link]() * TRAP_DAMAGE);
}
// getters/setters
}
[Link]
public class DungeonFacade {
private ArrayList<Treasure> treasures;
private Hashtable<String, Character> characters;
private ArrayList<Trap> traps;
public DungeonFacade () {
treasures = new ArrayList<Treasure>();
characters = new Hashtable<String,Character>();
traps = new ArrayList<Trap>();
}
public void generate() {
int total = (int)([Link]() * 5);
for (int i = 0;i < total; i++) {
[Link](new Treasure("Gold",(int)([Link]() * 5)));
}
total = (int)([Link]() * 5);
for (int i = 0;i < total; i++) {
[Link]("Character" + i,
new Orc(new SimpleCharacter("Orc" + i)));
}
total = (int)([Link]() * 5);
for (int i = 0;i < total; i++) {
[Link](new Trap("Spikes"));
}
}
public String getProperties() {
String report = "";
report = "Dungeon properties";
report += "Treasures\n";
for (Treasure treasure: treasures) {
report += "\t" + [Link]() + "\n";
}
report += "Characters\n";
Enumeration<String> list = [Link]();
while ([Link]()) {
report += "\t" +
[Link]([Link]()).toString() + "\n";
}
report += "Traps\n";
for (Trap trap: traps) {
report += [Link]();
}
return report;
}
}
[Link]
public class FacadeClient {
public static void main (String args[]) {
DungeonFacade dungeon = new DungeonFacade();
[Link]();
[Link]([Link]());
}
}
2.2.6 Flyweight
Se trata de compartir con el propósito de tener soporte para un amplio
número de objetos concretos.
Escenarios
Se utilizan muchos objetos similares y su coste es elevado
La mayor parte del estado del objeto puede aislarse
En lugar de utilizar muchos objetos, usamos y reutilizamos
unos pocos modificando su estado.
La identidad de cada objeto no es relevante y la aplicación
no depende de ello.
Diagrama
Como se puede ver, la clase Creature no contiene sus atributos, sino que
los deja en manos de otra clase, llamada CreatureAttributes. Creamos una
clase Cache que es quien se encarga de instanciar esos atributos. Una vez
hecho, Creature puede acceder cómodamente a ese cache para obtener esas
propiedades.
Código
[Link]
public class CreatureAttributes {
private int speed;
private int damage;
private int defense;
/**
* @param name
* @param damage
* @param defense
*/
public CreatureAttributes(int speed, int damage, int defense) {
[Link] = speed;
[Link] = damage;
[Link] = defense;
}
// getters/setters...
}
[Link]
public class Creature {
private String name;
public Creature (String name) {
[Link] = name;
}
/**
* using the creature name, gets attributes
* @return
*/
public CreatureAttributes getAttributes () {
return [Link]().getCreatureAttributes(name);
}
}
[Link]
public class CreatureCache {
private static CreatureCache creatureCache;
private static Map<String,CreatureAttributes> creatures = new
Hashtable<String,CreatureAttributes> ();
private CreatureCache () {
init();
}
private void init () {
[Link]("bat", new CreatureAttributes(6,5,3));
[Link]("snake", new CreatureAttributes(4,3,2));
[Link]("rat", new CreatureAttributes(5,6,3));
}
public static CreatureCache getInstance () {
if (null == creatureCache) {
creatureCache = new CreatureCache();
}
return creatureCache;
}
public CreatureAttributes getCreatureAttributes (String name) {
return [Link](name);
}
}
[Link]
public class FlyweightClient {
private static final int HUGE_NUMBER_OF_CREATURES = 3000;
public static void main (String args) {
ArrayList<Creature> creatures = new ArrayList<Creature>();
for (int i = 0;i < HUGE_NUMBER_OF_CREATURES; i++) {
[Link](new Creature("bat"));
[Link](new Creature("snake"));
[Link](new Creature("rat"));
}
}
}
2.2.7 Proxy
Nos permite ofrecer un objeto por delante de otro para controlar el
acceso al mismo.
Escenarios
Se necesita un control de acceso al objeto original y deben
proveerse distintos derechos de acceso.
El objeto representado es externo al sistema.
Se precisa crear objetos costosos bajo demanda.
Se requiere funcionalidad añadida cuando se accede a un
objeto: contadores de acceso, registros de acceso, control de
bloqueo, etc.
Diagrama
Como podemos observar, el cliente utiliza una instancia de
ScoresProxy, que es quien está ocultando el uso de FileProxy:
Código
[Link]
public interface Scores {
public String show();
public boolean save(String scores);
public boolean eraseAll();
}
[Link]
public class FileScores implements Scores {
public String show() {
// ... stuff
return null;
}
public boolean save(String scores) {
// ... stuff
return false;
}
public boolean eraseAll() {
// ... stuff
return false;
}
}
[Link]
public class ScoresProxy implements Scores {
private static Scores realScores;
public ScoresProxy () {
}
private void checkInstance () {
if (null == realScores) {
realScores = new FileScores();
}
}
public String show() {
return null;
}
public boolean save(String scores) {
return false;
}
public boolean eraseAll() {
return false;
}
}
[Link]
public class ProxyClient {
public static void main (String args[]) {
Scores scores = new ScoresProxy();
[Link]([Link]());
}
}
2.3 Patrones de
comportamiento
Estos patrones, se centran en distintas formas de funcionamiento o de
interacción entre clases.
2.3.1 Chains of
Responsibility
Entre los objetos hay envío de mensajes que en definitiva son llamadas
a métodos. Este patrón está orientado a evitar el acoplamiento entre el emisor
y receptor dando a diferentes objetos la opción de ocuparse de las peticiones.
Situando un objeto en medio, los encadena y se ocupa de pasar la petición a
lo largo de ellos hasta llegar el objeto receptor final.
Escenarios
Muchos objetos pueden responder a una petición, no se sabe
cuál de ellos es a priori y no se precisa que esa petición sea
responsabilidad de un único objeto.
Un conjunto de objetos, deberían ser capaces de responder a
una petición en tiempo de ejecución, sin necesidad de
especificar uno en concreto.
Se puede asumir que una petición no sea respondida o
tratada.
Diagrama
Como podemos ver, tenemos una clase abstracta para hacer operaciones
de logging o registro, que se puede implementar de tres maneras distintas.
Pese a tener varias implementaciones, lo peculiar de este patrón es el
uso que se le da desde el cliente. Se instancia una implementación de Logger
y luego se encadena con las otras y se llama simplemente a la operación log.
Código
[Link]
public class LogMessage {
private int type;
private String msg;
public LogMessage (int type, String msg) {
[Link] = type;
[Link] = msg;
}
// getters/setters...
}
[Link]
public abstract class Logger {
protected Logger nextLogger;
protected int myLogType;
public Logger (int myLogType) {
[Link] = myLogType;
}
/**
* stablish next logger in chain
*/
public Logger setNext (Logger logger) {
nextLogger = logger;
return logger;
}
/**
* This is called to perform log, if this class
* is not able, passes this task to the next in chain
*/
public void log (LogMessage logMessage) {
if (myLogType == [Link]()) {
logMessage(logMessage);
} else if (null != nextLogger) {
[Link](logMessage);
}
}
protected abstract void logMessage (LogMessage logMessage);
}
[Link]
public class StringLogger extends Logger {
public StringLogger(int myLogType) {
super(myLogType);
}
@Override
protected void logMessage(LogMessage logMessage) {
[Link]([Link]());
}
}
[Link]
public class JsonLogger extends Logger {
public JsonLogger(int myLogType) {
super(myLogType);
}
@Override
protected void logMessage(LogMessage logMessage) {
// Do JSONparsing stuff...
[Link]([Link]());
}
}
[Link]
public class ChainOfResponsibilityClient {
public static final int STRING_LOGGER = 1;
public static final int JSON_LOGGER = 2;
public static final int XML_LOGGER = 3;
public static void main (String args[]) {
Logger logger = new StringLogger(STRING_LOGGER);
[Link](new JsonLogger(JSON_LOGGER)).setNext(new
XmlLogger(XML_LOGGER));
LogMessage message1 = new LogMessage(STRING_LOGGER,"hello");
LogMessage message2 = new LogMessage(XML_LOGGER,"<!xml>
<greet>Hello</greet>");
LogMessage message3 = new LogMessage(JSON_LOGGER,"{\"greet\":\"hello\"}");
[Link](message1);
[Link](message2);
[Link](message3);
}
}
2.3.2 Command
Encapsula una petición como un objeto, permitiendo parametrizar los
clientes con distintas peticiones, además de encolar o registrar peticiones e
incluso dar opción a revertir peticiones.
Escenarios
Se precisa funcionalidad de callback, es decir, poder pasar
como parámetro un método a ejecutar. En POO aplicamos
este patrón para poder tener callbacks.
Se precisa registrar un histórico de peticiones.
Las peticiones deben ser soportadas en tiempo u orden
variable.
Las peticiones pueden ser transferidas de un proceso a otro.
Se necesita desacoplar a quien lanza la petición de quien la
soporta.
Se precisa un sistema de operaciones de alto nivel
compuestas de pequeñas ordenes que se puedan englobar en
mecanismos como las transacciones.
Diagrama
Todo gira en torno al interface Command, que es implementado por los
comandos que precisemos. Necesitamos una clase llamada Invoker para
ejecutar y llevar la gestión de los comandos emitidos:
[Link]
public class Army {
private int totalLife = 10;
public void move () {
[Link]("Army on the move");
}
public int attack () {
[Link]("Army attacking");
return (int)[Link]() * 6;
}
public int defend () {
[Link]("Army defending");
return (int)[Link]() * 6;
}
// getters/setters...
}
[Link]
public class AttackCommand implements Command {
private Army army;
public AttackCommand(Army army) {
[Link] = army;
}
public void execute() {
[Link]("AttackCommand: attack");
[Link]();
}
public void undo() {
[Link]("AttackCommand: undo attack");
}
}
[Link]
public class MoveCommand implements Command {
private Army army;
public MoveCommand(Army army) {
[Link] = army;
}
public void execute() {
[Link]("MoveCommand: moving");
[Link]();
}
public void undo() {
[Link]("MoveCommand: undo moving");
}
}
[Link]
public class Invoker {
private ArrayList<Command> commandHistory = new ArrayList<Command>();
private int undoIndex = 0;
/**
* stores new command in commandHistory and executes it
* When a new command is executed, undoIndex will point to the
* last command.
*/
public void storeAndExecute (Command command) {
[Link](command);
undoIndex = [Link]() - 1;
[Link]();
}
public void undo () {
if (undoIndex >= 0 && undoIndex <= [Link]() -1 ) {
[Link](undoIndex).undo();
if (undoIndex > 0) { undoIndex--; }
}
}
}
[Link]
public class CommandClient {
public static void main (String args[]) {
Army army = new Army();
Command attackCommand = new AttackCommand(army);
Command moveCommand = new MoveCommand(army);
Invoker invoker = new Invoker();
String option = "";
Scanner reader = new Scanner([Link]);
do {
[Link]("Your command: ");
option = [Link]();
switch (option) {
case
"attack": [Link](attackCommand);
break;
case "move":
[Link](moveCommand);
break;
case "undo":
[Link]();
break;
case "exit":
[Link]("Hasta la vista");
break;
default:
[Link]("unknown command");
break;
}
} while (option != "exit");
}
}
2.3.3 Interpreter
Dado un lenguaje, define una representación de su gramática junto a un
intérprete que utiliza esa representación para interpretar sentencias de ese
lenguaje.
Escenarios
Se necesita interpretar una gramática que puede estar
representada a modo de árbol.
La gramática es simple. En el caso de gramáticas más complejas
conviene utilizar herramientas específicas, como parseadores.
La eficiencia no es relevante.
Se precisa desacoplar la gramática de las expresiones
subyacentes.
Diagrama
El diagrama representa la estructura de un diagrama muy simple en el
que tenemos expresiones formadas por Ordenes: las órdenes son comandos y
opciones, y las opciones pueden ser cosas o números.
Código
[Link]
public abstract class Expression {
public abstract String interpret();
}
[Link]
public class Order extends Expression {
private Command command;
private Option option;
public Order(Command command, Option option) {
[Link] = command;
[Link] = option;
}
@Override
public String interpret() {
return [Link]() + " " + [Link]();
}
}
[Link]
public class Command extends Expression {
private String command;
public Command(String command) {
[Link] = command;
}
@Override
public String interpret() {
return "[" + command + "]";
}
}
[Link]
public abstract class Option extends Expression {
}
[Link]
public class Item extends Option {
private String item;
public Item(String item) {
[Link] = item;
}
@Override
public String interpret() {
return item;
}
}
[Link]
public class Number extends Option {
private int number;
public Number(int number) {
[Link] = number;
}
@Override
public String interpret() {
return number + "#";
}
}
[Link]
public class InterpreterClient {
public static void main (String args[]) {
ArrayList<Order> orders = new ArrayList<Order>();
[Link](new Order(new Command("Move"),new Number(5)));
[Link](new Order(new Command("Attack"),new Item("enemy")));
[Link](new Order(new Command("Defend"),new Item("town")));
for (Order order : orders) {
[Link]([Link]());
}
}
}
Output
[Move] 5#
[Attack] enemy
[Defend] town
2.3.4 Iterator
Permite que un objeto agregado (que contiene un conjunto de
elementos), pueda ser accesible como una secuencia sin exponer su
representación interna.
Escenarios
Se requiere acceder a los elementos sin necesidad de
acceder a toda la estructura.
Se necesita un interface único para recorrer la estructura.
Se precisan poder recorrer la estructura de forma múltiple y
concurrente
Existen diferencias entre los detalles de implementación de
varios iteradores, es decir, se precisa soporte para la
iteración polimórfica.
Diagrama
Como se puede apreciar, tenemos un escenario más completo con dos
interfaces:
1- Iterator: para
presentar
métodos que
permitan
iterar
2- Aggregation:
para
presentar
métodos que
permitan
añadir y
eliminar
elementos.
Iterator es implementada por HashIterator y HashtableStructure
implementa el interfaz Aggregation y hace uso del Iterator y su
implementación.
Código
[Link]
public interface Iterator<T> {
public void first();
public void next();
public boolean isDone();
public T currentItem();
}
[Link]
public class HashIterator<S,T> implements Iterator<T> {
private Hashtable<S,T> items;
Enumeration keys;
Object current;
public HashIterator (Hashtable<S,T> items) {
[Link] = items;
keys = [Link]();
}
@Override
public void first() {
keys = [Link]();
current = [Link]();
}
@Override
public void next() {
current = [Link]();
}
@Override
public boolean isDone() {
return [Link]();
}
@Override
public T currentItem() {
return [Link](current);
}
}
[Link]
public interface Aggregation<T> {
public Iterator createIterator();
public int count();
public void append(T item);
public void remove(T item);
}
[Link]
public class HashtableStructure<S,T> implements Aggregation<T> {
private Hashtable<S,T> hashtable;
public HashtableStructure () {
hashtable = new Hashtable<S,T>();
}
@Override
public Iterator<T> createIterator() {
return new HashIterator<S,T>(hashtable);
}
@Override
public int count() {
return [Link]();
}
@Override
public void append(T item) {
}
public void append(S key, T item) {
[Link](key, item);
}
@Override
public void remove(T item) {
Enumeration<S> keys = [Link]();
S key = null;
while ([Link]()) {
key = [Link]();
if ([Link](key)==item) {
[Link](key);
break;
}
}
}
}
[Link]
public class IteratorClient {
public static void main (String args[]) {
Squad squad1 = new Squad();
Squad squad2 = new Squad();
Squad squad3 = new Squad();
HashtableStructure<String,Squad> squads =
new HashtableStructure<String,Squad>();
[Link]("Squad1",squad1);
[Link]("Squad2",squad2);
[Link]("Squad3",squad3);
Iterator<Squad> iterator = [Link]();
while ([Link]()) {
[Link]();
Squad squad = [Link]();
[Link]("Squad data: "+ [Link]());
}
}
}
2.3.5 Mediator
Define un objeto que encapsula la manera en la que otros objetos
interactúan. Este patrón otorga mayor desacoplamiento al hacer que los
objetos no interactúen entre si de forma directa.
Escenarios
La comunicación entre un conjunto de objetos está bien
definida y es compleja. La dependencia entre ellos no tiene
estructura concreta o es difícil de entender.
Es difícil reutilizar un objeto porque tiene referencias y
comunicación con otros objetos.
Existen demasiadas relaciones y se precisa un punto único
para el control o la comunicación.
Un comportamiento que se reparte entre muchas clases
necesita ser personalizado sin hacer subclases.
Diagrama
Por un lado, tenemos la estructura de un ejército, con unidades que
pueden ser General o Soldado. Uno dar órdenes y el otro las recibe.
Pero la clave está en que las unidades utilizan una instancia del
Mediator para llevar a cabo el intercambio de órdenes. Si ocultamos los
métodos de Unit, veremos de forma más clara la relación que se establece con
Mediator y su implementación:
Código
[Link]
public interface Mediator {
public void order (Command command, Unit unit);
public void move (int x, int y, Unit unit);
public int attack (int x, int y, Unit unit);
public void hold (Unit unit);
}
[Link]
public class Command {
private String msg;
public Command(String msg) {
[Link] = msg;
}
// getters/setters ...
}
[Link]
public abstract class Unit {
protected String name;
protected String range;
protected int x;
protected int y;
protected Mediator mediator;
public Unit(String name, String range, Mediator mediator) {
[Link] = name;
[Link] = range;
x = y = 0;
[Link] = mediator;
}
public void receiveOrder (Command command) {
[Link]("Order received> " + [Link]());
}
public void move (int x, int y) {
this.x = x;
this.y = y;
}
public int attack () {
return [Link]().roll();
}
public void hold () {
x = y = 0;
}
// getters/setters ...
}
[Link]
public class Soldier extends Unit {
public Soldier(String name, String range, Mediator mediator) {
super(name, range, mediator);
}
@Override
public void receiveOrder (Command command) {
[Link](name + ": Order received> " + [Link]());
}
public void move (int x, int y) {
this.x = x;
this.y = y;
[Link](name + ": Move Order received> " + x + ":" + y);
}
public int attack () {
[Link](name + ": Attack Order received> ");
return [Link]().roll();
}
public void hold () {
x = y = 0;
[Link](name + ": Hold Order received> " + x + ":" + y);
}
}
[Link]
public class General extends Unit {
public General(String name, String range, Mediator mediator) {
super(name, range, mediator);
}
public void giveOrder (Command command, Unit unit) {
[Link]("Giving order> " + [Link]());
[Link](command, unit);
}
public void attack (int x, int y, Unit unit) {
[Link]("Attack> " + x +":" + y + " >" + [Link]());
[Link](x, y, unit);
}
public void move (int x, int y, Unit unit) {
[Link]("Move> " + x +":" + y + " >" + [Link]());
[Link](x, y, unit);
}
public void hold (Unit unit) {
[Link]("Hold> " + [Link]());
[Link](unit);
}
}
[Link]
public class MediatorImpl implements Mediator {
@Override
public void order(Command command, Unit unit) {
[Link](command);
}
@Override
public void move(int x, int y, Unit unit) {
[Link](x, y);
}
@Override
public int attack(int x, int y, Unit unit) {
return [Link]();
}
@Override
public void hold(Unit unit) {
[Link]();
}
}
[Link]
public class MediatorClient {
public static void main (String args[]) {
Mediator mediator = new MediatorImpl();
Vector<Unit> army = new Vector<Unit>();
[Link](new Soldier("u1","soldier",mediator));
[Link](new Soldier("u2","soldier",mediator));
[Link](new Soldier("u3","soldier",mediator));
General general = new General("General", "general", mediator);
[Link](new Command("Attack"), [Link]());
[Link](5, 7, [Link]());
}
}
2.3.6 Memento
Permite sacar una instantánea del estado de un objeto para
externalizarlo, guardarlo o recuperarlo más adelante, todo ello sin
desencapsular el mismo.
Escenarios
Cuando el estado interno de un objeto debe poder ser
salvado para ser recuperado más adelante.
Los límites de la encapsulación deben preservarse.
Se precisa si al exponer el estado a través de interfaces se
acaba exponiendo la implementación
Diagrama
En este escenario disponemos de una clase que genera unidades de
guerra. Cada unidad tiene una instancia de Estate que representa su estado
actual y que maneja a través de la clase UnitMemento.
La clase CareTaker es la que utilizará el cliente para poder gestionar el
estado de la unidad.
Código
[Link]
public class State {
private int life;
private String range;
private Weapon weapon;
// getters/setters...
}
[Link]
public class UnitMemento {
private State state;
public void saveState(State state) {
[Link] = state;
}
public State getState () {
return state;
}
}
[Link]
public class CareTaker {
private UnitMemento memento;
public UnitMemento getMemento() {
return memento;
}
public void setMemento(UnitMemento memento) {
[Link] = memento;
}
}
[Link]
public class UnitOriginator {
private String name;
private int life;
private String range;
private Weapon weapon;
private State state;
public UnitOriginator(String name, int life, String range, Weapon weapon) {
[Link] = name;
[Link] = life;
[Link] = range;
[Link] = weapon;
}
public UnitMemento createMemento () {
State state = new State();
[Link]([Link]);
[Link]([Link]);
[Link]([Link]);
UnitMemento memento = new UnitMemento();
[Link](state);
return memento;
}
public void setUnitMemento (UnitMemento unitMemento) {
[Link] = [Link]();
life = [Link]();
range = [Link]();
weapon = [Link]();
}
// getters/setters...
}
[Link]
public class MementoClient {
public static void main (String args[]) {
UnitOriginator unit =
new UnitOriginator("Conan",100,"General", new HumanWeapon());
CareTaker careTaker = new CareTaker();
[Link]([Link]());
[Link]("Unit is: " + [Link]());
[Link](50);
[Link]("captain");
[Link](new HumanWeapon());
[Link]("Unit is: " + [Link]());
[Link]([Link]());
[Link]("Unit recovered status is: " + [Link]());
}
}
2.3.7 Observer
Define una dependencia uno a varios entre objetos de tal manera que
cuando el estado de un objeto cambia, todos los elementos dependientes son
notificados y cambian de manera automática.
Escenarios
Los cambios de estado en uno o más objetos debe afectar al
comportamiento de otros objetos.
Se precisan capacidad de broadcasting entre objetos.
Se requiere que los objetos no tengan que preocuparse de la
propagación de mensajes o notificaciones.
Cuando un objeto precisa cambiar otros, pero no sabemos
quién o no podemos asumirlo previamente. Y por supuesto,
no queremos acoplamiento.
Cuando una abstracción tiene dos aspectos dependientes. Si
encapsulamos esos aspectos en objetos separados podemos
variarlos y reutilizarlos de forma separada.
Diagrama
Observer es una clase que básicamente dispone de un método de
actualización con los que notificará de los cambios a los subject o sujetos en
cada cambio de estado.
Código
[Link]
public abstract class Observer {
protected String observerState;
protected Subject subject;
// This method will update view
public abstract void update ();
public Observer (Subject subject) {
[Link] = subject;
}
// getters/setters...
}
[Link]
public abstract class Subject {
private ArrayList<Observer> observers = new ArrayList<Observer>();
public abstract String getState();
// adds an observer to list
public void attach (Observer observer) {
[Link](observer);
}
// removes observer from list
public void dettach (Observer observer) {
[Link](observer);
}
public void notifyObservers () {
for (Observer observer : observers) {
[Link]();
[Link]([Link]);
}
}
}
[Link]
public class PlayerSubject extends Subject {
private String playerState;
@Override
public String getState() {
return playerState;
}
/*
* sets player data
* and consequently notifies and changes observers
*/
public void setState (String state) {
playerState = state;
notifyObservers();
}
}
[Link]
public class TextObserver extends Observer {
public TextObserver(Subject subject) {
super(subject);
}
public void update() {
observerState = [Link]();
}
}
[Link]
public class XMLObserver extends Observer {
public XMLObserver(Subject subject) {
super(subject);
}
public void update() {
observerState = "<!xml version=\"1.0\"><data>"
+ [Link]() + "</data>";
}
}
[Link]
public class JSONObserver extends Observer {
public JSONObserver(Subject subject) {
super(subject);
}
public void update() {
observerState = "{\"data\" :\""
+ [Link]() + "\" }";
}
}
[Link]
public class ObserverClient {
public static void main(String[] args) {
PlayerSubject player = new PlayerSubject();
Observer stringObserver = new TextObserver(player);
Observer xmlObserver = new XMLObserver(player);
Observer jsonObserver = new JSONObserver(player);
[Link](stringObserver);
[Link](xmlObserver);
[Link](jsonObserver);
[Link]("I am alive");
[Link]("I am dead");
[Link]("Hola");
}
}
2.3.8 State
Permite a un objeto alterar su comportamiento cuando su estado interno
cambia.
Escenarios
El comportamiento de un objeto debe ser regido por su
estado y debe poder hacerlo en tiempo de ejecución.
Condiciones complejas vinculan ese comportamiento a su
estado. Pueden existir estructuras condicionales cuyas ramas
se procurarán separar en diferentes clases.
La transición entre los estados de un objeto debe ser
explícita.
Diagrama
Definimos una interface con un método por cada uno de los estados, y a
continuación, implementamos ese interface por cada uno de los métodos, lo
cuales nos permiten cambiar el estado de la clase.
El cliente interactúa únicamente con la instancia de Dragón.
Código
[Link]
public interface Dragon State {
public void fly(Dragon dragon);
public void land(Dragon dragon);
public int attack(Dragon dragon);
}
[Link]
public class DragonFlyState implements DragonState {
public void fly(Dragon dragon) {
[Link]("Fly State> flying...");
}
public void land(Dragon dragon) {
[Link]("Fly State> Landing...");
}
public int attack(Dragon dragon) {
[Link]("Fly State> we attack while flying.");
return (int) ([Link]() * 10);
}
}
[Link]
public class DragonLandState implements DragonState {
public void fly(Dragon dragon) {
[Link]("Land State> we are landing not flying...");
}
public void land(Dragon dragon) {
[Link]("Land State> landing");
}
public int attack(Dragon dragon) {
[Link]("Land State> can't attack while landing.");
return 0;
}
}
[Link]
public class DragonAttackState implements DragonState {
public void fly(Dragon dragon) {
[Link]("Attack State> fly while attacking");
}
public void land(Dragon dragon) {
[Link]("Attack State> can't land while attacking.");
}
public int attack(Dragon dragon) {
[Link]("Attack State> attacking.");
return (int) ([Link]() * 100);
}
}
[Link]
public class Dragon {
private DragonState dragonState;
public Dragon () {
[Link] = new DragonLandState();
}
public void fly() {
dragonState = new DragonFlyState();
[Link]("Dragon> let's fly");
[Link](this);
}
public void land() {
dragonState = new DragonLandState();
[Link]("Dragon> let's land");
[Link](this);
}
public int attack() {
dragonState = new DragonAttackState();
[Link]("Dragon> attack of the dragon");
return [Link](this);
}
}
[Link]
public class StateClient {
public static void main(String[] args) {
Dragon smaug = new Dragon();
[Link]();
[Link]();
[Link]();
[Link]();
}
}
2.3.9 Strategy
Nos permite definir una familia de algoritmos, encapsulando cada uno
de ellos y haciéndolos intercambiables. Este patrón nos permite que un
algoritmo puede variar de forma dinámica en el uso que hacen los clientes.
Escenarios
Se requieren muchas versiones o variantes de un algoritmo.
Se aplica cuando la única diferencia entre muchas clases
relacionadas es su comportamiento.
El comportamiento de una clase debe definirse en tiempo de
ejecución.
Los algoritmos utilizan datos a los que el código no queda
expuesto.
Existen sentencias condicionales complejas y difíciles de
mantener.
Diagrama
Como se puede apreciar, el cliente puede aplicar distintas clases de
estrategia para instanciar objetos Unit.
Código
[Link]
public interface PointsGenerator {
public int [] generate();
}
[Link]
public class FixedPointsGenerator implements PointsGenerator {
@Override
public int[] generate() {
return new int[]{6,6,6};
}
}
[Link]
public class RandomPointsGenerator implements PointsGenerator {
@Override
public int[] generate() {
int [] values = new int[3];
for (int i = 0;i<3;i++) {
values[i] = (int)([Link]() * 8);
}
return values;
}
}
[Link]
public class Unit {
private int strength;
private int speed;
private int intelligence;
private PointsGenerator pointsGenerator;
private AttackStrategy attackStrategy;
public Unit (PointsGenerator pointsGenerator, AttackStrategy attackStrategy) {
[Link] = pointsGenerator;
[Link] = attackStrategy;
init();
}
// this method makes use of the strategy
private void init() {
int [] values = [Link]();
strength = values[0];
speed = values[1];
intelligence = values[2];
}
public int attack () {
return [Link](this);
}
public int getStrength() {
return strength;
}
public int getSpeed() {
return speed;
}
public int getIntelligence() {
return intelligence;
}
@Override
public String toString() {
return "Unit [strength=" + strength + ", speed=" + speed
+ ", intelligence=" + intelligence + "]";
}
}
[Link]
public class StrategyClient {
public static void main (String args[]) {
Unit randomUnit =
new Unit(new RandomPointsGenerator(), new FastAttackStrategy());
Unit fixedUnit =
new Unit(new FixedPointsGenerator(), new StrongAttackStrategy());
[Link](randomUnit);
[Link](fixedUnit);
}
}
2.3.10 Template
Method
Define el esqueleto del algortimo de una operación, delegando alguno
de los pasos a las subclases. Este patrón permite que las subclases modifiquen
ciertos pasos de un algoritmo sin modificar la estructura general del mismo.
Escenarios
Se precisa una única implementación abstracta de un
algoritmo y en todo caso dejar las particularidades las
subclases.
El comportamiento común entre subclases debe localizarse
en una clase común.
Cuando las todas o la mayoría de las subclases necesitan
implementar un comportamiento.
Controlar mejor la extensión en las subclases utilizando
mecanismos tipo hook en métodos que nos permitan
extender el comportamiento solo en ciertos puntos.
Las clases base debe ser capaces de invocar de forma
uniforme a las subclases.
Diagrama
Como se puede apreciar, la clase abstracta Assault lo único que tiene
implementado es el método assault, que hace referencia a varios métodos sin
implementar. Esas implemetaciones las deja en manos de las subclases que
quieran extenderla, como por ejemplo RandomAssault:
[Link]
public class RandomAssault extends Assault {
public RandomAssault(Army army1, Army army2) {
super(army1, army2);
}
@Override
public void init(Army army) {
[Link](20);
}
@Override
public int attack(Army army) {
return [Link]();
}
@Override
public void defend(Army army, int attack) {
[Link]([Link]() - (attack - [Link]()));
}
@Override
public boolean unitsAlive(Army army) {
return ([Link]() > 0);
}
@Override
public String outcome(Army army1, Army army2) {
return "1:" + [Link]() + " 2:" + [Link]();
}
}
[Link]
public class TemplateMethodClient {
public static void main (String args[]) {
Army armyOfGood = new Army();
Army armyOfEvil = new Army();
Assault assault = new RandomAssault(armyOfGood, armyOfEvil);
}
}
2.3.11 Visitor
Representa una operación que debe ser efectuada en los elementos de
una estructura de objeto. Nos permite definir un nuevo método u operación
sin necesidad de cambiar las clases de los elementos en los que actúa.
Escenarios
Se necesita cuando una estructura de objetos debe tener
múltiples operaciones no relacionadas.
Se precisa ejecutar operaciones sobre clases concretas de
una estructura de objetos
La estructura de objetos no puede cambiar, pero si las
operaciones que se hacen sobre ellos.
Cuando exponer el estado interno o las operaciones de una
estructura de objetos resulta aceptable.
Se necesita cuando las operaciones deben ser aplicables en
múltiples estructuras de objetos que implementen los
mismos conjuntos de interfaces.
Diagrama
Por un lado, tenemos el interface visitor y dos implementaciones, cada
una de las cuales añadirá funcionalidades distintas a cada subclase:
Por otro lado, tenemos la jerarquía ArmyUnit que implementan
distintas unidades de un ejército, incluyendo el propio ejército.
Código
[Link]
public interface ArmyUnit {
public void accept(ArmyUnitVisitor visitor);
}
[Link]
public interface ArmyUnitVisitor {
public void spell(Bowman bowman);
public void spell(Knight knight);
public void spell(Army army);
}
[Link]
public class Knight implements ArmyUnit {
private int horseSpeed = 1;
@Override
public void accept(ArmyUnitVisitor visitor) {
[Link](this);
}
public int getHorseSpeed() {
return horseSpeed;
}
public void setHorseSpeed(int horseSpeed) {
[Link] = horseSpeed;
}
@Override
public String toString() {
return "Knight [horseSpeed=" + horseSpeed + "]";
}
}
[Link]
public class Bowman implements ArmyUnit {
private int arrows = 10;
@Override
public void accept(ArmyUnitVisitor visitor) {
[Link](this);
}
@Override
public String toString() {
return "Bowman [arrows=" + arrows + "]";
}
public int getArrows() {
return arrows;
}
public void setArrows(int arrows) {
[Link] = arrows;
}
}
[Link]
public class Army implements ArmyUnit {
private ArrayList<ArmyUnit> armyUnits =
new ArrayList<ArmyUnit>();
public Army() {
[Link](new Bowman());
[Link](new Bowman());
[Link](new Bowman());
[Link](new Knight());
[Link](new Knight());
}
@Override
public void accept(ArmyUnitVisitor visitor) {
for (ArmyUnit unit : armyUnits) {
[Link](visitor);
}
[Link](this);
}
public ArrayList<ArmyUnit> getArmyUnits() {
return armyUnits;
}
@Override
public String toString() {
return "Army [armyElements=" + armyUnits + "]";
}
}
[Link]
public class ArmyUnitBlessingVisitor implements ArmyUnitVisitor {
@Override
public void spell(Bowman bowman) {
[Link]("Bowman was blessed with arrows");
[Link](1000);
}
@Override
public void spell(Knight knight) {
[Link]("Knight was blessed with a faster horse");
[Link](500);
}
@Override
public void spell(Army army) {
[Link]("Knight was blessed with more knights");
[Link]().add(new Knight());
[Link]().add(new Knight());
}
}
[Link]
public class ArmyUnitCurseVisitor implements ArmyUnitVisitor {
public void spell(Bowman bowman) {
[Link]("Bowman was cursed with arrow loss");
[Link](1000);
}
@Override
public void spell(Knight knight) {
[Link]("Knight was cursed with a slow horse");
[Link](500);
}
@Override
public void spell(Army army) {
[Link]("Army is cursed");
[Link]().clear();
}
}
[Link]
public class VisitorClient {
public static void main(final String[] args) {
Army army = new Army();
[Link](new ArmyUnitBlessingVisitor());
[Link]([Link]());
[Link](new ArmyUnitCurseVisitor());
[Link]([Link]());
}
}
3 BIBLIOGRAFÍA
Sin duda el libro de los patrones de software ppor antonomasia: