/* */

18 de agosto de 2007

Capítulo 9

.
Encapsulamiento e Introducción a Clases y Objetos

Hasta aquí hemos trabajado con procedimientos y funciones. Es decir los procesos por un lado y los datos por el otro.

Sin embargo, sería más natural pensar directamente en los objetos que intervienen e interactúan en el contexto del problema que tenemos que resolver.

Para explicar este tema tomaremos como ejemplo el problema 3.2 (analizado en el capítulo 3).


Problema 3.2
Leer los datos correspondientes a dos circunferencias y luego los datos que corresponden a 10 puntos. Indicar cuantos puntos están ubicados dentro del área de cada una de las circunferencias, cuantos están en la intersección, cuantos no están en ninguna y cuantos coinciden con el centro de cada una.


Recordemos la solución que planteamos para resolver este problema:


Como analizamos en su momento, en el programa principal definimos todos los contadores necesarios, los inicializamos y luego leemos los datos de las dos circunferencias (radio, x,y), los datos de los 10 puntos identificados por sus coordenadas, y por cada punto analizamos si está dentro de alguna, ninguna o de la dos circunferencias. Incrementamos los contadores que correspondan y (luego del ingreso de los 10 puntos) mostramos los resultados.

Definimos también una función procesaCirc que recibe los datos de una circunferencia y los datos de un punto, evalúa ese punto respecto de esa cirfunferencia e incrementa los contadores en caso de ser necesario.



Si hacemos un análisis de los objetos que intervienen e interactúan en el contexto de este problema podremos identificar los siguiente objetos:
  • c1, c2 – dos circunferencias, cada una identificada por su radio y por su centro
  • plano – el plano que contiene a las dos circunferencias
  • pi – 10 puntos identificados por sus coordenadas
Los objetos tienen atributos. Por ejemplo: c1 puede tener radio=10 y centro en (4, 2) mientras que c2 puede tener radio=6 y centro en (3,7).

c1 y c2 son dos objetos del mismo tipo (son circunferencias) y por lo tanto tienen los mismos atributos: un radio y un centro identificado por un par (x,y), pero con diferentes valores. Decimos entonces que c1 y c2 son objetos de la misma clase o bien son instancias de la misma clase: la clase Circunferencia.


Clases y Objetos

Una clase es un tipo de datos (definido por el programador). Por lo tanto un objeto es una variable cuyo tipo de datos es una clase.

El siguiente código Pascal define dos objetos c1 y c2 cuyo tipo de datos es la clase Circunferencia.

var c1,c2: Circunferencia;


En una clase podemos definir datos y operaciones. Los atributos de un objeto son datos y (siguiendo con el problema) una operación para la clase Circunferencia podría ser procesarPunto. Es decir: un objeto circunferencia debería ser capáz de recordar su radio y su centro identificado por las coordenadas (x,y), y de evaluar y procesar un punto en relación a sí mismo.

uCircunferencia.pas (versión 1, interface)
   1:
2:unit uCircunferencia;
3:
4:interface
5:
6:uses math;
7:
8:type
9: Circunferencia = class
10: private
11: // atributos
12: radio: real;
13: x,y: real;
14:
15: public
16: // metodos
17: constructor create(r,cx,cy: real);
18: function procesarPunto(px,py: real):real;
19: end;
20:
21:// continua mas abajo con la seccion implementation
22:


En el código anterior definimos la clase Circunferencia. Las clases tienen una parte privada (identificada por la palabra private) y una parte pública (identificada por la palabra public). Generalmente los atributos se definen en la parte privada y los métodos (operaciones asociadas a los datos) se definen en la parte pública.

Los métodos pueden definirse como procedimientos o funciones. Además existe un método especial: el contructor.


El Constructor

El constructor es un método especial cuyo objetivo es permitir inicializar al objeto. Es decir: en el contructor (generalmente) se reciben (como parámetro) los valores con los que se dará valor a los atributos del objeto.

En otros lenguajes de programación como C++ o Java el constructor obligatoriamente debe tener el mismo nombre que la clase. Sin embargo, en Pascal puede tener cualquier nombre. Simplemente se diferencia de los otros métodos porque no se define como procedure ni como function: se define como constructor. En todos los casos nosotros lo llamaremos create.


Por convensión cada clase será definida en una unit de Pascal que tendrá el mismo nombre de la clase pero anteponiendo el prefijo u. Por eso en este caso la unit es: uCircunferencia.pas

Veamos la continuación de uCircunferencia.pas donde analizaremos la implementación de los métodos definidos en la clase.

uCircunferencia.pas (versión 1, implementation)
   1:
2:implementation
3:
4:// implementacion del contructor
5:constructor Circunferencia.create(r,cx,cy: real);
6:begin
7: radio:=r; // dentro de los metodos tenemos acceso
8: x:=cx; // a los atributos (variables de
9: y:=cy; // de instancia de la clase
)
10:end;
11:
12:// implementacion del metodo procesarPunto
13:function Circunferencia.procesarPunto(px,py:real):real;
14:var d:real;
15:begin
16: // si es centro
17: if( (px=x) AND (py=y) ) then begin
18: procesarPunto:=0;
19: exit;
20: end;
21:
22: // calculo la distancia al centro
23: d:=sqrt( power(px-x,2) + power(py-y,2) );
24:
25: // si esta dentro del area de la circunferencia...
26: if( d<=radio ) then begin
27: procesarPunto:=-1;
28: exit;
29: end;
30:
31: // el punto cae fuera del area
32: procesarPunto:=1;
33:end;
34:
35:end.
36:

Para escribir la implementación de cada uno de los métodos hay que hacer referencia a la clase en donde el método está definido. Por eso el prototipo se define así (como función):

function NombreClase.nombreMetodo(...): tipoRetorno;

o bien (si lo definimos como procedimiento):

procedure NombreClase.nombreMetodo(...);


Ahora podemos analizar el código de un programa que utilice lo anterior.

problema3.2oop.pas (versión 1)
   1:
2:uses uCircunferencia;
3:
4:var
5: c1,c2: Circunferencia; // definimos dos objetos
6: radio,x,y: real;
7: i:integer;
8: ret1, ret2: real;
9: contInterseccion,contFueraAmbas: integer;
10: contDentroC1, contDentroC2: integer;
11: contCentroC1, contCentroC2: integer;
12: contFueraC1, contFueraC2: integer;
13:
14:begin
15: // inicializamos los contadores
16: contInterseccion:=0;
17: contFueraAmbas:=0;
18: // ... (asi con todos los que faltan...)
19:
20: write('Ingrese Circunferencia 1 (radio,x,y): ');
21: readln(radio,x,y);
22:
23: // instanciamos la primer circunferencia
24: c1:=Circunferencia.create(radio,x,y);
25:
26: write('Ingrese Circunferencia 2 (radio,x,y): ');
27: readln(radio,x,y);
28:
29: // instanciamos la segunda circunferencia
30: c2:=Circunferencia.create(radio,x,y);
31:
32: for i:=1 to 10 do begin
33: write('Ingrese punto',i,' (x,y): ');
34: readln(x,y);
35:
36: ret1:= c1.procesarPunto(x,y);
37: ret2:= c2.procesarPunto(x,y);
38:
39: // analizamos los resultados...
40: if( (ret1<0) AND (ret2<0) ) then begin
41: contInterseccion:=contInterseccion+1;
42: continue;
43: end;
44:
45: if( (ret1>0) AND (ret2>0) ) then begin
46: contFueraAmbas:=contFueraAmbas+1;
47: continue;
48: end;
49:
50: // :
51: // asi para todas las posibilidades
52: // :
53:
54: end;
55:end.
56:

En el programa principal que acabamos de ver instanciamos dos objetos circunferencia (c1 y c2 en la líneas 24 y 30), luego leemos los 10 puntos e incovamos (por cada punto) el método procesarPunto en cada una de las dos circunferencias (líneas 36 y 37) e incrementamos los contadores que corresponda según los resultados ret1 y ret2.


Variables de Instancia

Los objetos, una vez creados decimos que están instanciados. Llamamos instancia a un objeto que há sido creado y (por lo tanto) sus atributos tienen valor. Por este motivo, decimos que los datos definidos en la clase (atributos y todas las variables que podamos definir) son variables de instancia. Cada objeto de la clase mantiene valores particulares para cada una de esas variables.


Desde el punto de vista de una circunferencia, si procesamos un punto podemos indicar si el punto está dentro o fuera del área y (si está adentro) si fué centro. Pero no nos incumbe la relación que exista entre ese punto y la otra circunferencia.

Podemos modificar la clase Circunferencia para que lleve la información estadística (contadores) de los puntos que procesó y así liberar de esta responsabilidad al programa principal.

uCircunferencia.pas (versión 2, interface)
   1:
2:unit uCircunferencia;
3:
4:interface
5:
6:uses math;
7:
8:type
9: Circunferencia = class
10: private
11: radio: real;
12: x,y: real;
13:
14: contCentro: integer; // contador de centros
15: contDentro: integer; // cont. dentro del area
16: contFuera: integer; // cont. fuera del area
17:
18: public
19: constructor create(r,cx,cy: real);
20: function procesarPunto(px,py: real):real;
21:
22: // metodos de acceso
23: function getContCentro(): integer;
24: function getContDentro(): integer;
25: function getContFuera(): integer;
26: end;
27:
28:// continua mas abajo con la seccion implementation
29:

Como vemos agregamos tres variables de instancia: contCentro, contDentro y contFuera. Estas variables (son contadores) deben inicializarse en cero antes de que la circunferencia comience a procesar puntos. Esto lo haremos en el constructor.


Métodos de Acceso

Las variables de instancia que definimos en la clase (en la sección private) están encapsuladas. No pueden ser accedidas por fuera de la clase. Por lo tanto, la única forma de tener acceso al valor de las variables es a través de métodos de acceso que provea la misma clase.

Por convensión estos métodos se deben llamar "get" seguido del nombre de la variable a la permiten acceder.

En la clase Circunferencia definimos (hasta ahora) tres métodos de acceso:
  • getContDentro // retorna el valor de la variable contDentro
  • getContFuera // retorna el valor de la variable contFuera
  • getContCentro // retorna el valor de la variable contCentro

uCircunferencia.pas (versión 2, implementation)
   1:
2:implementation
3:
4:constructor Circunferencia.create(r,cx,cy: real);
5:begin
6: radio:=r;
7: x:=cx;
8: y:=cy;
9:
10: // los contadores deben inicializarse en cero
11: // el constructor es un buen lugar para hacerlo...
12: contCentro:=0;
13: contDentro:=0;
14: contFuera:=0;
15:end;
16:
17:
function Circunferencia.procesarPunto(px
18: ,py:real):real;
19:var d:real;
20:begin
21: // si es centro
22: if( (px=x) AND (py=y) ) then begin
23: contCentro:=contCentro+1;
24: procesarPunto:=0;
25: exit;
26: end;
27:
28: // calculo la distancia al centro
29: d:=sqrt( power(px-x,2) + power(py-y,2) );
30:
31: // si esta dentro del area de la circunferencia
32: if( d<=radio ) then begin
33: contDentro:=contDentro+1;
34: procesarPunto:=-1;
35: exit;
36: end;
37:
38: // el punto cae fuera del area
39: contFuera:=contFuera+1;
40: procesarPunto:=1;
41:end;
42:
43:function Circunferencia.getContCentro(): integer;
44:begin
45: // simplemente retornamos el valor del contador
46: getContCentro:=contCentro;
47:end;
48:
49:function Circunferencia.getContDentro(): integer;
50:begin
51: // simplemente retornamos el valor del contador
52: getContDentro:=contDentro;
53:end;
54:
55:function Circunferencia.getContFuera(): integer;
56:begin
57: // simplemente retornamos el valor del contador
58: getContFuera:=contFuera;
59:end;
60:
61:end.
62:

Ahora el programa principal queda liberado de la responsabilidad de manejar los contadores de cada una de las circunferencias. Solo maneja los contadores que exceden el contexto de una circunferencia (contInterseccion y contFueraAmbas)

problema3.2oop.pas (versión 2)
   1:
2:uses uCircunferencia;
3:
4:var
5: c1,c2: Circunferencia;
6: radio,x,y: real;
7: i:integer;
8: ret1, ret2: real;
9:
10: // contInterseccion y contFueraAmbas
11: // los manejamos "a mano".
12: contInterseccion, contFueraAmbas: integer;
13:
14:begin
15: // inicializamos los contadores
16: contInterseccion:=0;
17: contFueraAmbas:=0;
18:
19: write('Ingrese Circunferencia 1 (radio,x,y): ');
20: readln(radio,x,y);
21: c1:=Circunferencia.create(radio,x,y);
22:
23: write('Ingrese Circunferencia 2 (radio,x,y): ');
24: readln(radio,x,y);
25: c2:=Circunferencia.create(radio,x,y);
26:
27: for i:=1 to 10 do begin
28: write('Ingrese punto',i,' (x,y): ');
29: readln(x,y);
30:
31: ret1:= c1.procesarPunto(x,y);
32: ret2:= c2.procesarPunto(x,y);
33:
34: // dentro de las dos => esta en la interseccion
35: if( (ret1<0) AND (ret2<0) ) then begin
36: contInterseccion:=contInterseccion+1;
37: continue;
38: end;
39:
40: // fuera de las dos
41: if( (ret1>0) AND (ret2>0) ) then begin
42: contFueraAmbas:=contFueraAmbas+1;
43: continue;
44: end;
45: end;
46:
47: // pedimos los datos a la circunferencia c1
48: writeln('Circunferencia 1: ');
49: writeln('Dentro del Area: ',c1.getContDentro() );
50: writeln('Fuera del Area: ',c1.getContFuera() );
51: writeln('Centros: ',c1.getContCentro() );
52:
53: // pedimos los datos a la circunferencia c2
54: writeln('Circunferencia 2: ');
55: writeln('Dentro del Area: ',c2.getContDentro() );
56: writeln('Fuera del Area: ',c2.getContFuera() );
57: writeln('Centros: ',c2.getContCentro() );
58:
61: // mostramos los contadores que "llevamos a mano"
62: writeln('Interseccion: ',contInterseccion );
63: writeln('Fuera de Ambas: ',contFueraAmbas );
64:end.
65:

Avancemos un poco más. Vamos a darle a la clase Circunferencia la responsabilidad de leer sus propios datos. Para esto agregaremos un nuevo constructor (sin argumentos) y crearemos un nuevo método: ingresarDatos.

uCircunferencia.pas (versión 3, interface)
   1:
2:unit uCircunferencia;
3:
4:interface
5:
6:uses math;
7:
8:type
9: Circunferencia = class
10: private
11: radio: real;
12: x,y: real;
13:
14: contCentro: integer;
15: contDentro: integer;
16: contFuera: integer;
17:
18: public
19: constructor create(r,cx,cy: real);
20: constructor create(); // sobrecarga
21: procedure ingresarDatos();
22: function procesarPunto(px,py: real):real;
23: function getContCentro(): integer;
24: function getContDentro(): integer;
25: function getContFuera(): integer;
26: end;
27:
28:// continua mas abajo con la seccion implementation
29:


Sobrecarga

Como podemos ver, ahora tenemos definidos dos constructores. Se llaman exactamente igual (create) pero reciben diferentes argumentos. El primero recibe 3 parámetros de tipo real, el segundo no recibe nada. Decimos entonces que sobrecargamos (en este caso) el constructor.

Podemos sobrecargar cualquier método. Esto implica escribirlo más de una vez pero con diferente cantidad y/o tipos de parámetros.

uCircunferencia.pas (versión 3, implementation)
   1:
2:implementation
3:
4:constructor Circunferencia.create();
5:begin
6: // contructor nulo... no hacemos nada
7:end;
8:
9:constructor Circunferencia.create(r,cx,cy: real);
10:begin
11:// :
12:// todo lo que estaba...
13:// :
14:end;
15:
16:procedure Circunferencia.ingresarDatos();
17:begin
18: write('Ingrese Circunferencia (radio,x,y): ');
19: readln(radio,x,y);
20:end;
21:
22:// :
23:// aca siguie estando todo lo que estaba...
24:// :
25:
26:end.
27:

Ahora veamos como queda el programa principal:

problema3.2oop.pas (versión 3)
   1:
2:uses uCircunferencia;
3:
4:var
5: c1,c2: Circunferencia;
6: radio,x,y: real;
7: i:integer;
8: ret1, ret2: real;
9: contInterseccion,contFueraAmbas: integer;
10:
11:begin
12: // inicializamos los contadores
13: contInterseccion:=0;
14: contFueraAmbas:=0;
15:
16: // instanciamos y leemos
17: c1:=Circunferencia.create();
18: c1.ingresarDatos();
19:
20: // instanciamos y leemos
21: c2:=Circunferencia.create();
22: c2.ingresarDatos();
23:
24: // :
25: // aca siguie estando todo lo que estaba...
26: // :
27:end.
28:

El próximo paso será desligar al programa principal de la responsabilidad de leer los puntos “a mano”. Para esto vamos a crear una clase Punto con dos atributos: x,y (sus cordenadas) y con un método ingresarDatos. También tendremos que sobrecargar el método procesarPunto de la clase Circunferencia (que recibía dos valores tipo real) para que ahora reciba una instancia de la clase Punto.

uPunto.pas
   1:
2:unit uPunto;
3:
4:interface
5:
6:type
7: Punto = class
8: private
9: x,y: real;
10:
11: public
12: constructor create();
13: procedure ingresarDatos();
14:
15: // metodos de acceso a los atributos
16: function getX(): real;
17: function getY(): real;
18: end;
19:
20:implementation
21:
22:// constructor nulo... no hace nada...
23:constructor Punto.create();
24:begin
25:end;
26:
27:procedure Punto.ingresarDatos();
28:begin
29: write('Ingrese punto (x,y): ');
30: readln(x,y);
31:end;
32:
33:function Punto.getX():real;
34:begin
35: getX:=x;
36:end;
37:
38:function Punto.getY():real;
39:begin
40: getY:=y;
41:end;
42:end.
43:

Ahora sobrecargamos el método procesarPunto en la clase Circunferencia.

uCircunferencia.pas (versión 4)
   1:
2:unit uCircunferencia;
3:
4:interface
5:
6:uses math,uPunto;
7:
8:type
9: Circunferencia = class
10: private
11: radio: real;
12: x,y: real;
13:
14: contCentro: integer;
15: contDentro: integer;
16: contFuera: integer;
17:
18: public
19: // dos constructures
20: constructor create(r,cx,cy: real);
21: constructor create();
22:
23: procedure ingresarDatos();
24:
25: // dos procesarPunto...
26: function procesarPunto(px,py: real):real;
27: function procesarPunto(p: Punto):real;
28:
29: function getContCentro(): integer;
30: function getContDentro(): integer;
31: function getContFuera(): integer;
32: end;
33:
34:implementation
35:
36:// :
37:// aca siguie estando todo lo que estaba...
38:// :
39:
40:function Circunferencia.procesarPunto(p: Punto):real;
41:begin
42: // invocamos la otra version de procesarPunto
43: // pasandole los valores x e y del punto p
44: procesarPunto:=procesarPunto(p.getX(),p.getY());
45:end;
46:
47:end.
48:

El programa principal ahora será:

problema3.2oop.pas (versión 4)
   1:
2:uses uCircunferencia, uPunto;
3:
4:var
5: c1,c2: Circunferencia;
6: p: Punto; // definimos un objeto de la clase Punto
7: i:integer;
8: ret1, ret2: real;
9: contInterseccion,contFueraAmbas: integer;
10:
11:begin
12: // inicializamos los contadores
13: contInterseccion:=0;
14: contFueraAmbas:=0;
15:
16: // instanciamos c1
17: c1:=Circunferencia.create();
18: c1.ingresarDatos();
19:
20: // instanciamos c2
21: c2:=Circunferencia.create();
22: c2.ingresarDatos();
23:
24: // instanciamos p
25: p:=Punto.create();
26:
27: for i:=1 to 10 do begin
28: p.ingresarDatos();
29:
30: ret1:= c1.procesarPunto(p); // recibe un punto
31: ret2:= c2.procesarPunto(p); // :O)
32:
33: // :
34: // todo lo que estaba sigue estando...
35: // :
36: end;
37:end.
38:

Ahora (para finalizar) intentaremos evitar que el programa principal se ocupe del contador de puntos que pertenecen al área intersección y del contador de puntos que caen fuera de ambas circunferencias.

La solución será crear una nueva clase: la clase Plano. El plano tendrá dos circunferencias y la responsabilidad de mantener los contadores que excedan las atribuciones de las circunferencias que contiene.

uPlano.pas
  1:
2:unit uPlano;
3:
4:interface
5:uses uCircunferencia, uPunto;
6:
7:type
8: Plano = class
9: private
10: c1,c2: Circunferencia;
11: contInterseccion, contFueraAmbas:integer;
12: public
13: constructor create(circ1,circ2:Circunferencia);
14: procedure procesarPunto(p: Punto);
15:
16: // metodos de acceso
17: function getContInterseccion(): integer;
18: function getContFueraAmbas(): integer;
19: end;
20:
21:implementation
22:
23:constructor Plano.create(circ1,circ2: Circunferencia);
24:begin
25: // asignamos circ1 y circ2 a las vbles. de instancia
26: c1:=circ1;
27: c2:=circ2;
28:
29: // inicializamos los contadores
30: contInterseccion:=0;
31: contFueraAmbas:=0;
32:end;
33:
34:procedure Plano.procesarPunto(p: Punto);
35:var ret1,ret2: real;
36:begin
37: // pasamos el punto a cada circunferencia
38: ret1:=c1.procesarPunto(p);
39: ret2:=c2.procesarPunto(p);
40:
41: // incrementamos los contadores que corresponda
42: if( (ret1<0) AND (ret2<0) ) then begin
43: contInterseccion:=contInterseccion+1;
44: exit;
45: end;
46:
47: if( (ret1>0) AND (ret2>0) ) then begin
48: contFueraAmbas:=contFueraAmbas+1;
49: end;
50:end;
51:
52:function Plano.getContInterseccion():integer;
53:begin
54: getContInterseccion:=contInterseccion;
55:end;
56:end.
57:function Plano.getContFueraAmbas():integer;
58:begin
59: getContFueraAmbas:=contFueraAmbas;
60:end;
61:end.
62:

Ahora veamos la versión final del programa principal.

problema3.2oop.pas (versión 5, final)
   1:
2:uses uCircunferencia, uPunto, uPlano;
3:var
4: c1,c2: Circunferencia;
5: p: Punto;
6: pla: Plano;
7: i: integer;
8:
9:begin
10: // instanciamos c1
11: c1:=Circunferencia.create();
12: c1.ingresarDatos();
13:
14: // instanciamos c2
15: c2:=Circunferencia.create();
16: c2.ingresarDatos();
17:
18: // instanciamos el plano
19: pla:=Plano.create(c1,c2);
20:
21: // instanciamos un punto
22: p:=Punto.create();
23:
24: for i:=1 to 10 do begin
25: p.ingresarDatos();
26: pla.procesarPunto(p);
27: end;
28:
29: writeln('Circunferencia 1:');
30: writeln('Dentro: ',c1.getContDentro());
31: writeln('Fuera: ',c1.getContFuera());
32: writeln('Centro: ',c1.getContCentro());
33:
34: writeln('Circunferencia 2:');
35: writeln('Dentro: ',c2.getContDentro());
36: writeln('Fuera: ',c2.getContFuera());
37: writeln('Centro: ',c2.getContCentro());
38:
39: writeln('Fuera de Ambas:',pla.getContFueraAmbas());
40: writeln('Interseccion:',pla.getContInterseccion());
41:end.
42:





Algoritmos y Estructuras de Datos UTN - UBA.

No hay comentarios: