miércoles, 24 de septiembre de 2014

Programación y Personalización SIG. Programación Orientada a Objetos

A lo largo de la historia de la informática, han ido apareciendo diferentes paradigmas de programación. En primer lugar, apareció la programación secuencial, que consistía en secuencias de sentencias que se ejecutaban una tras otra. El lenguaje ensamblador o el lenguaje COBOL son lenguajes secuenciales. Entonces no existía el concepto de función, que apareció más adelante en los lenguajes procedimentales, como el BASIC o el C. La evolución no terminó aquí, y continuó hasta el paradigma más extendido en la actualidad: la programación orientada a objetos. Smalltalk, C++ o Java pertenecen a esta nueva generación. 

Cada nuevo paradigma ha extendido el anterior, de manera que podemos encontrar las características de un lenguaje secuencial en uno procedimental, y las de uno procedimental en uno orientado a objetos. De hecho, hasta ahora sólo hemos visto la vertiente procedimental del lenguaje Java. 

Estudiaremos qué es una clase y qué es un objeto, cómo se definen y cómo se utilizan. Además, conoceremos los aspectos más relevantes de la programación orientada a objetos y las principales diferencias respecto a la programación procedimental.

2.1. Clases y objetos

En la programación orientada a objetos, aparecen por primera vez los conceptos de clase y objeto. Una clase es como una especie de patrón conceptual, mientras que un objeto es la materialización de dicho patrón. Imaginemos la clase “motocicleta”. Todos estamos de acuerdo en que todas las motocicletas tienen características comunes que nos permiten distinguirlas como tales. Una motocicleta, entre otras cosas, tiene dos ruedas, carece de techo y tiene un manillar y un motor de una determinada cilindrada. Además, será de alguna marca y tendrá algún nombre de modelo. A este esquema mental, a este patrón, lo llamaremos “clase”. Sin embargo, motocicletas hay muchas: la de mi hermano, la del vecino, la del concesionario de enfrente..., todas de diferentes marcas, cilindradas y colores. A esta materialización del patrón le daremos el nombre de “objeto”. Clase sólo hay una, pero objetos puede haber muchos. 

Las líneas siguientes declaran la clase “Producto” y sus atributos:
class Producto {
 int código; 
String descripción; 
double precio;
}

Como se puede observar, el nombre de la clase va precedido por la palabra reservada class (clase). Los atributos se declaran del mismo modo que las variables, aunque encerrados entre llaves.

Cabe recordar que una clase es siempre una simplificación de la realidad. Por esta razón, nunca declararemos atributos para todas y cada una de las características del ente que queramos representar; sólo crearemos aquellos que necesitemos para resolver el problema que se nos plantea. Por otro lado, podemos tener una o más funciones que lean y escriban esos atributos, por ejemplo, una función que se encargue de leer y otra que se encargue de cambiar (escribir) el precio del producto. Estas funciones, que están estrechamente ligadas a los atributos y que no tienen razón de ser sin ellos, se declaran en el ámbito de la clase. El código siguiente:

public class Producto {
int código;
String descripción;
double precio;
// fija el precio del producto
void fijarPrecio(double precioNuevo) {
precio = precioNuevo;
}
// devuelve el precio del producto
double obtenerPrecio() {
return precio;
}
}

Declara la clase “Producto” con dos funciones miembro: “fijarPrecio” y “obtenerPrecio”. Es importante observar que ambas funciones tienen visibilidad sobre los atributos de la clase, es decir, pueden acceder a ellos de la misma forma que acceden a sus propios parámetros y variables.

2.2. Instanciación de objetos
Como ya hemos comentado, una clase es sólo un patrón de objetos. Los atributos de una clase no existen en la memoria del ordenador hasta que no se materializan en un objeto. A este proceso de materialización se le llama instanciación. Un objeto se instancia mediante el operador new (nuevo). El código siguiente:  

Producto sal; sal = 
new Producto(); 

instancia el objeto “sal” de la clase “Producto”. Como se puede observar, “sal” debe declararse como una variable del tipo “Producto”. Los paréntesis después del nombre de la clase son obligatorios. A partir de la instanciación, ya tenemos en memoria un objeto con todos sus atributos. Cada nueva instanciación crea un objeto nuevo, independiente de los demás. Si tenemos, por ejemplo, dos objetos, “sal” y “azúcar”:

Producto sal, azúcar; 
sal = new Producto(); 
azúcar = new Producto(); 

en la memoria tendremos dos pares de atributos “código”, “descripción” y “precio”, uno por cada objeto:
Para acceder a estos atributos, utilizaremos el operador “.” (punto). Las líneas siguientes:

Producto sal, azúcar;
sal = new Producto();
azúcar = new Producto();
// fija el precio del paquete de sal
sal.precio = 0.60;
// fija el precio del paquete de azúcar
azúcar.precio = 0.81

inicializan el atributo “precio” de los objetos “sal” y “azúcar” a 0,60 y 0,81 respectivamente. Observad la independencia entre ambos objetos. El operador “.” nos permite especificar de forma unívoca a qué atributo de qué objeto hacemos referencia. De la misma forma (usando el operador “.”) accederemos a las funciones miembro de la clase. El código siguiente hace exactamente lo mismo que el anterior, pero usando la función “fijarPrecio” que anteriormente habíamos definido:

Producto sal, azúcar;
sal = new Producto();
azúcar = new Producto();
// fija el precio del paquete de sal
sal.fijarPrecio(0.60);
// fija el precio del paquete de azúcar
azúcar.fijarPrecio(0.81);


public class Producto {
int código;
String descripción;
double precio;
// fija el precio del producto
void fijarPrecio(double precioNuevo) {
precio = precioNuevo;
}
// devuelve el precio del producto
double obtenerPrecio() {
return precio;
}
public static void main(String[] args) {
Producto sal, azúcar;
sal = new Producto();
azúcar = new Producto();
// fija el precio del paquete de sal
sal.fijarPrecio(0.60);
// fija el precio del paquete de azúcar
azúcar.fijarPrecio(0.81);
}
}

Las funciones miembro de una clase, como “fijarPrecio” u “obtenerPrecio”, acceden directamente a los atributos de un objeto sin necesidad de especificar a qué objeto pertenecen. Esto es así porque las funciones miembro siempre se ejecutan en el contexto de un objeto. Es decir, cuando nos encontramos con la llamada:

sal.fijarPrecio(0.60);

ejecutamos la función para el objeto “sal”. Entonces, dentro de la función, cada vez que se hace referencia a un atributo, a éste, implícitamente, se le supone de la clase “sal”.

2.3. El objeto this
Como ya hemos explicado, cuando una función miembro accede a un atributo lo hace en el contexto de un objeto. Por eso no es necesario que especifique a qué objeto hace referencia. Sin embargo, hay casos en los que puede ser interesante disponer de dicho objeto, por ejemplo para pasarlo por parámetro a otra función o para distinguirlo de una variable o un parámetro homónimo. 

Las funciones miembro pueden acceder al objeto actual mediante el objeto predefinido this (éste). A continuación, se muestra una implementación alternativa (y equivalente) de la función “fijarPrecio”:

void fijarPrecio(double precio) {
this.precio = precio;
}
En este caso, se utiliza this para distinguir entre el atributo de la clase y el parámetro de la función. El “this.precio” hace referencia al atributo “precio” del objeto actual (this), mientras que “precio”, sin el this, hace referencia al pará- metro de la función. 

En este caso, mediante this explicitamos a qué objeto hacemos referencia. Entonces, si tenemos la llamada: en el contexto de la función “fijarPrecio”, this será el mismo objeto que “sal”.  

2.4. El constructor 
Cuando se instancia un objeto, puede ser necesario inicializar sus atributos. Volviendo al ejemplo anterior, establecíamos el precio del producto después de haberlo instanciado, ya fuese accediendo directamente al atributo “precio” o mediante la función “fijarPrecio”. 

Sin embargo, las clases nos ofrecen un mecanismo mucho más elegante de inicializar un objeto: el constructor. El constructor es una función como cualquier otra, salvo por un par de particularidades: se llama como la clase y no tiene tipo de retorno. 

Las líneas siguientes muestran un posible constructor de la clase “Producto”:

Producto(int código, String descripción, double precio) {
this.código = código;
this.descripción = descripción;
this.precio = precio;
}

El constructor espera tres parámetros: el código, la descripción y el precio del producto. En este caso, la tarea que lleva a cabo el constructor es relativamente sencilla: tan sólo copiar el código, la descripción y el precio proporcionados a los atributos homónimos. El ejemplo siguiente instancia otra vez los productos “sal” y “azúcar”. Sin embargo, esta vez se hace uso del constructor que acabamos de definir:

Producto sal, azúcar;
sal = new Producto(80005355, "Sal", 0.60);
azúcar = new Producto(800053588, "Azúcar", 0.81);

Agrupándolo todo, el código quedaría así:  

public class Producto {
int código;
String descripción;
double precio;
// el constructor: inicializa el objeto Producto
Producto(int código, String descripción, double precio) {
this.código = código;
this.descripción = descripción;
this.precio = precio;
}
// fija el precio del producto
void fijarPrecio(double precioNuevo) {
precio = precioNuevo;
}
// devuelve el precio del producto
double obtenerPrecio() {
return precio;
}
public static void main(String[] args) {
Producto sal, azúcar;
sal = new Producto(80005355, "Sal", 0.60);
azúcar = new Producto(80005388, "Azúcar", 0.81);
System.out.println("Precio de 1 paquete de sal: " +
sal.obtenerPrecio() + " EUR");
System.out.println("Precio de 1 paquete de azúcar: " +
azúcar.obtenerPrecio() + " EUR");
}
}

Lo que sigue al operador new no es otra cosa que la llamada al constructor de la clase. Después de la instanciación, los atributos de los objetos “sal” y “azúcar” ya estarán inicializados con los valores que hayamos pasado al constructor. El programa generará la salida siguiente:

Precio de 1 paquete de sal: 0.60 EUR
Precio de 1 paquete de azúcar: 0.81 EUR

Una clase puede tener más de un constructor, del mismo modo que puede tener dos o más funciones con el mismo nombre. Sin embargo, como en el caso de las funciones, los parámetros deben ser distintos. Si no se define ningún constructor para una clase, ésta tendrá un construc- a tor implícito: el constructor por defecto. El constructor por defecto, que no espera ningún parámetro, es el que usábamos en las primeras implementaciones de la clase “Producto”, cuando aún no habíamos definido ningún constructor:

azúcar = new Producto(); // llamada al constructor por defecto 

2.5. Extensión y herencia
La programación orientada a objetos (POO de ahora en adelante) establece un mecanismo fundamental que nos permite definir una clase en términos de otra: la extensión. La idea subyacente es partir de una clase general para generar una clase más específica que considere alguna característica o funcionalidad no cubierta por la clase más general.  

Supongamos ahora que en nuestro colmado empezamos a vender productos a granel. La aproximación anterior, en la que cada producto tenía un precio, deja de ser válida para todos los productos. Ahora hay productos que tienen un precio por kilogramo y un peso, además del código y la descripción. Está claro que, en gran medida, los productos que se venden a granel no difieren demasiado de los que se venden por unidades. 

De hecho, sólo cambia un poco el significado del concepto de precio, que ahora no es por unidad sino por kilo. Además, tenemos un nuevo atributo, el peso, y una nueva forma de computar el precio, el precio (por kilo) multiplicado por el peso. Jugando con este símil, el código siguiente define la clase “ProductoGranel” como una extensión de la clase “Producto”: 

class ProductoGranel extends Producto {
// añade el atributo peso (del producto)
double peso;
// el constructor también añade el parámetro peso
ProductoGranel(int código, String descripción, double
precio, double peso) {
super(código, descripción, precio);
this.peso = peso;
}
// para los productos vendidos a granel, el precio es
// igual al resultado de multiplicar el peso (en Kg) por
// el precio por Kg
double obtenerPrecio() {
return precio * peso;
}
}

En primer lugar, observad el uso de la palabra clave extends (extiende):

class ProductoGranel extends Producto {

 Mediante esta construcción, definimos una clase en términos de otra. Esto implica que la clase “ProductoGranel” tendrá los mismos atributos y funciones que la clase “Producto”, más aquellos que defina de forma expresa. A este mecanismo de transmisión de atributos y funciones se le llama herencia. Observad también la llamada al constructor de la clase “Producto” desde el constructor de la clase “ProductoGranel” mediante la palabra reservada super (superclase):

super(código, descripción, precio);

Como ya hemos comentado, la clase “ProductoGranel” hereda los atributos y las funciones de la clase “Producto”. Esto le permite llamar al constructor de la clase “Producto” para inicializar aquellos atributos que ha heredado. La llamada al constructor de la clase madre, si la hay, debe ser la primera sentencia del constructor de la clase hija. Finalmente, podemos observar que “ProductoGranel” define una función “obtenerPrecio”, que es exactamente la misma que ya existía en la clase “Producto”. Sin embargo, su código cambia sustancialmente: ahora el precio se calcula como el producto de los atributos “precio” (por kilo) y “peso”:

double obtenerPrecio() {
 return precio * peso; 
}


La nueva función sustituirá a la heredada de “Producto”. Este mecanismo nos permite redefinir una función para que se ajuste a las características de la nueva clase. En el caso de la clase “ProductoGranel”, este cambio era necesario, pues la función “obtenerPrecio” que había heredado simplemente devolvía el valor del atributo “precio”.

El mecanismo de extensión nos permite utilizar objetos de la clase “ProductoGranel” como si fueran de la clase “Producto”. El código siguiente imprime el nombre y el precio de varios productos independientemente de su tipo: 
class Producto {
int código;
CC BY • PID_00174495 39 Introducción a la programación orientada a objetos
String descripción;
double precio;
// el constructor: inicializa el objeto Producto
Producto(int código, String descripción, double precio) {
this.código = código;
this.descripción = descripción;
this.precio = precio;
}
// fija el precio del producto
void fijarPrecio(double precioNuevo) {
precio = precioNuevo;
}
// devuelve el precio del producto
double obtenerPrecio() {
return precio;
}
// devuelve la descripción del producto
String obtenerDescripción() {
return descripción;
}
}
class ProductoGranel extends Producto {
// añade el atributo peso (del producto)
double peso;
// el constructor también añade el parámetro peso
ProductoGranel(int código, String descripción,
double precio, double peso) {
super(código, descripción, precio);
this.peso = peso;
}
// para los productos vendidos a granel, el precio es
// igual al resultado de multiplicar el peso (en Kg) por
// el precio por Kg
double obtenerPrecio() {
return precio * peso;
}
}

public class Caja {
// muestra por pantalla el precio de un producto
public static void escribirPrecio(Producto p) {
System.out.println(p.obtenerDescripción() + " " +
p.obtenerPrecio() + " EUR");
}
public static void main(String[] args) {
Producto sal;
ProductoGranel mango, salmón;
// crea los productos sal, salmón y mango
sal = new Producto(80005355, "Sal", 0.60);
salmón = new ProductoGranel(80005373, "Salmón",
9.55, 0.720);
mango = new ProductoGranel(80005312, "Mango", 2.99,
0.820);
// escribe el precio de los tres productos
escribirPrecio(sal);
escribirPrecio((Producto)salmón);
escribirPrecio((Producto)mango);
}
}

Como podemos observar, en la llamada a la función “escribirPrecio” para los objetos de la clase “ProductoGranel” hay una peculiaridad: el uso del operador de conversión:

escribirPrecio((Producto)mango) 

El operador de conversión permite cambiar el tipo de una variable a otro tipo compatible (casting, en inglés). En el ejemplo, forzamos a que “salmón” y “mango” se traten como si fuesen de la clase “Producto”. El operador de conversión toma la forma del tipo destino encerrado entre paréntesis justamente delante de la variable que deseamos convertir. El resultado será el siguiente:

Sal 0.60 EUR
Salmón 6.876 EUR
Mango 2.4518 EUR

2.6. Los paquetes y la directiva import
 
Para estructurar el código y evitar conflictos con los nombres de las clases, el lenguaje Java nos permite agrupar las clases en paquetes (packages). Normalmente, los paquetes agrupan clases que están relacionadas por alguna funcionalidad o característica. Por ejemplo, se suelen crear paquetes para agrupar las clases que implementan la interfaz de usuario de una aplicación o aquellas que proporcionan algún tipo de cálculo matemático o probabilístico.
La agrupación de clases en paquetes nos permite: • Poner de manifiesto la relación entre un conjunto de clases. 
Identificar un conjunto de clases con una funcionalidad. 
Evitar los conflictos con los nombres de las clases: puede haber clases homónimas en paquetes distintos.

Para crear un paquete, basta con escribir en la parte superior de un fichero fuente la palabra package seguida del nombre del paquete. Por ejemplo, las lí- neas siguientes: 

package postgradosig;
class Asignatura {
// (...)

definen una clase llamada “Asignatura” perteneciente al paquete “postgra- a dosig”.

Los paquetes se pueden dividir en subpaquetes de manera arbitraria, formando una jerarquía de paquetes. Las líneas siguientes:  

package edu.uoc.postgradosig;
class Asignatura {
// (...)
}

Cuando desde una clase se desea acceder a otra clase situada en otro paquete, es necesario indicarlo de forma explícita mediante la directiva import (importar). De hecho, esta directiva no se hace otra cosa que importar la definición de la clase. 

Supongamos la clase “Alumno” perteneciente al paquete “edu.uoc.postgradosig.alumno” y la clase “Asignatura” perteneciente al paquete “edu.uoc.postgradosig.asignatura”. Supongamos también que la clase “Asignatura” tiene una función “matricular” que permite matricular a un alumno a la asignatura. Entonces, el código de la clase “Asignatura” sería parecido al siguiente:

package edu.uoc.postgradosig.asignatura;
// importa la clase “Alumno”
import edu.uoc.postgradosig.alumno.Alumno;
class Asignatura {
// (...)
public void matricular(Alumno alumno) {
// (...)
}
}

2.7. Visibilidad y encapsulación
Hasta ahora hemos accedido a los atributos y las funciones de un objeto libremente, sin ninguna restricción. Sin embargo, como los atributos contienen el estado de un objeto, no suele ser deseable que código ajeno a la clase pueda acceder a ellos y modificarlos, porque, al desconocer el papel que desempeñan cada uno de los atributos en la implementación de la clase, esto podría dejar el objeto en un estado erróneo.

Se llama encapsulación a la técnica consistente en ocultar el estado de un objeto, es decir, sus atributos, de manera que sólo pueda cambiarse mediante un conjunto de funciones definidas a tal efecto. 

Los atributos y las funciones de una clase pueden incorporar en su definición un modificador que especifique la visibilidad de dicho elemento. Hay tres modificadores explícitos en Java: 

public: el elemento es público y puede accederse a él desde cualquier clase, 
protected: el elemento es semiprivado y sólo pueden acceder a él la clase que lo define y las clases que lo extienden, y 
private: el elemento es privado y sólo puede acceder a él la clase que lo define.

Si no se especifica ningún nivel de visibilidad, se aplica la regla de visibilidad implícita (conocida como package): el atributo o función será público para todas las clases del mismo paquete, y privado para las demás. A modo de ejemplo, veamos una nueva versión de la clase “Producto”: 

class Producto {
private int código;
private String descripción;
private double precio;
// el constructor: inicializa el objeto Producto
public Producto(int código, String descripción,
double precio) {
this.código = código;
this.descripción = descripción;
this.precio = precio;
}
// fija el precio del producto
public void fijarPrecio(double precioNuevo) {
precio = precioNuevo;
}
// devuelve el precio del producto
public double obtenerPrecio() {
return precio;
}
// devuelve la descripción del producto
public String obtenerDescripción() {
return descripción;
}
}

Como se puede apreciar, se han declarado todos los atributos privados y las funciones públicas. Esto implica que la sentencia

sal.precio = 0.60;

en la que “sal” es una instancia de la clase “Producto”, será inválida. En su lugar, debemos usar una llamada a la función “cambiarPrecio” creada a tal efecto:

sal.cambiarPrecio(0.60);

Es importante remarcar que al menos un constructor de la clase debe ser pú- blico. En caso contrario, no se va a poder instanciar ningún objeto de la clase.


Bibliografía
Oracle, "The Java(tm) Tutorials", http://download.oracle.com/javase/tutorial/. 
Oracle, "Java(tm) 2 Platform Standard Edition 5.0 API Specification", http:// download.oracle.com/javase/1.5.0/docs/api/.
Personalización SIG, Albert Gavarró Rodríguez , Universidad Oberta  de Catalunya, España 2011



No hay comentarios:

Publicar un comentario