Qué es la programación orientada a objetos
Programar es construir un modelo. De un problema real —una escuela, una cuenta bancaria, un videojuego— se arma una representación en la computadora, y el programa trabaja sobre esa representación. Ahora bien: hay muchas formas de modelar la misma cosa, y a cada una de esas formas se la llama paradigma.
La programación orientada a objetos propone una en particular: representar todo lo que conocemos en términos de entidades —los objetos— que interactúan y se relacionan entre sí. Esas entidades pueden representar absolutamente cualquier cosa: algo físico y tangible como una persona, una factura o un auto, o algo intangible como una operación comercial, un proceso químico o un algoritmo matemático.
El paradigma empezó a tomar forma a principios de la década de 1980, y hoy es el más usado en el desarrollo de software. La razón hay que buscarla en lo que resuelve: permite representar modelos complejos sin perderse en los detalles, y eso hace que el programa sea más fácil de escribir, de comprender y de mantener.
Los paradigmas de programación
Un paradigma es, en palabras de los apuntes, «la manera de pensar el desarrollo de software: un conjunto de elementos, reglas y modelos que brindan un marco para el diseño y la construcción de programas en un determinado tiempo». Hay varios, y cada uno tiene ventajas y desventajas: cada enfoque es más apropiado para ciertas cosas.
| Paradigma | Lenguajes típicos | En qué consiste |
|---|---|---|
| Estructurado | C, Pascal, Basic | El programa es una secuencia de pasos, organizada en funciones. Los datos y las operaciones que trabajan con ellos están separados. |
| Funcional | CAML, LISP | El programa se describe como la composición de funciones matemáticas. Se evita modificar valores: en lugar de cambiar algo, se calcula un resultado nuevo. |
| Declarativo (lógico) | Prolog | No se describe cómo resolver el problema sino qué es verdad: se cargan hechos y reglas, y el motor del lenguaje deduce la respuesta. |
| Orientado a objetos | Java, C#, VB.NET, Smalltalk | El programa es un conjunto de objetos que colaboran entre sí, cada uno con sus propios datos y sus propias operaciones. |
| Híbridos | C++, Visual Basic | Mezclan dos o más paradigmas en el mismo lenguaje. Son la mayoría de los lenguajes actuales. |
El problema que vino a resolver
La diferencia se ve mejor con un ejemplo chico. Supongamos que hay que guardar las notas de un curso. Un programa escrito con el paradigma estructurado haría algo así:
/* Estructurado: los datos en un lado, las funciones en otro */
char nombres[40][30];
float notas[40];
int cantidad = 0;
float promedio(float notas[], int cantidad) {
float suma = 0;
for (int i = 0; i < cantidad; i++) {
suma = suma + notas[i];
}
return suma / cantidad;
}
Y el mismo problema, orientado a objetos:
// Orientado a objetos: los datos y lo que saben hacer, juntos
class Alumno {
private String nombre;
private float nota;
float notaFinal() {
return nota; // el objeto sabe cómo calcular lo suyo
}
}
Los dos programas funcionan, pero el primero tiene tres problemas que se agrandan con el tamaño del sistema:
- Nada mantiene los arreglos alineados. El nombre del alumno 7 está en
nombres[6]y su nota ennotas[6]. Agregar un alumno obliga a acordarse de escribir en los dos arreglos, y a borrar en los dos cuando alguien se va. El índice es la única relación entre los datos, y el índice no existe en ningún lado: es un acuerdo tácito que hay que respetar a mano. - Cualquier parte del programa puede tocar cualquier dato. Una línea como
notas[0] = 100;escrita en cualquier función, en cualquier archivo, deja al sistema en un estado imposible. No hay ningún lugar donde verificar que la nota esté entre 0 y 10. - La operación no está donde están los datos.
promedio()es una función suelta que necesita recibir el arreglo y la cantidad de alumnos desde afuera. La función no «sabe» de alumnos: sabe sumar un arreglo de números.
La orientación a objetos responde a los tres de una sola vez: los datos y las operaciones que los controlan van juntos, dentro del objeto; el estado interno queda oculto detrás de los métodos; y el objeto sabe hacer lo que le corresponde en lugar de recibir todo desde afuera.
De la realidad al modelo
Conviene ver cómo se hace el pasaje, porque es el trabajo concreto del analista. Imaginemos que hay que hacer la representación virtual de un paisaje real: una foto en la que hay edificios, casas, puentes, árboles, agua y también cosas que no se ven ni se tocan, como el aire.
¿Qué se hace primero? Se buscan las cosas. El observador mira la foto y encuentra objetos: esto es un edificio, esto es un puente, esto es un árbol. Y entre esos objetos descubre algo importante: los edificios, en esencia, parten de la misma idea —son todos construcciones— pero cada uno tiene diferencias: la altura, el color, la cantidad de pisos, la ubicación, el nombre.
Ahí aparecen las dos nociones que sostienen todo el paradigma. Se define un molde —una figura base, un rectángulo, lo que todos los edificios tienen en común— y después se le agregan los atributos que hacen a cada edificio distinto. De ese molde se sacan las copias: cada edificio concreto es una instancia del molde.
Dos consecuencias de esto que conviene retener desde el principio:
- Un modelo nunca es idéntico a la realidad. Es una representación, y su exactitud depende del nivel de detalle que se elija. Nadie modela los tornillos de un edificio si el problema es administrar alquileres.
- Elegir el nivel de detalle es una decisión del analista, no del problema. El mismo automóvil puede ser un «vehículo con motor de cuatro cilindros» para quien estudia mecánica, o un «medio de transporte con capacidad para cinco personas» para quien organiza viajes. Los dos modelos son correctos: dependen de para qué se los va a usar. De esto se ocupa la abstracción, más adelante.
Qué es un objeto
La definición informal es corta: un objeto es una entidad del mundo real, y es una instancia de una clase. Pero conviene precisarla, porque «entidad del mundo real» no quiere decir solamente «cosa que se toca».
Objetos palpables y objetos abstractos
- Objeto palpable: aquel que se puede identificar a través de los sentidos. Un edificio, un perro, un árbol.
- Objeto abstracto: aquel que requiere de un razonamiento para comprender su existencia. Un cliente, una operación comercial, una cuenta bancaria: nadie los ve ni los toca, pero existen como concepto con límites claros.
Y si se mira desde qué tipo de cosa representan, los objetos del software se agrupan en tres familias:
| Tipo de entidad | Ejemplos |
|---|---|
| Físicas | Vehículo, Casa, Producto |
| Conceptuales | Proceso químico, Transacción bancaria |
| De software | Lista enlazada, Interfaz gráfica |
La última fila es la que se olvida: los objetos de un programa también sirven para representar cosas que existen dentro del programa. Una lista enlazada o una ventana son tan objetos como un auto.
Las definiciones formales
Tres definiciones que se citan siempre, y que dicen lo mismo desde tres ángulos distintos:
- «Un objeto es cualquier cosa, real o abstracta, acerca de la cual almacenamos datos y las operaciones que controlan dichos datos.» Aparecen las dos mitades: los datos y las operaciones, juntos, sobre la misma cosa.
- (Rumbaugh) «Un objeto es un concepto, abstracción o cosa con un significado y límites claros en el problema en cuestión.» Lo importante son los límites: si no se puede decir dónde termina un objeto y dónde empieza otro, el objeto está mal elegido.
- (Booch) Todo objeto posee estado, comportamiento e identidad. Es la definición más útil en la práctica, y la que se desarrolla en la sección siguiente.
Una última idea que atraviesa toda la unidad: un objeto existe en el tiempo y en el espacio, y puede estar compuesto por otros objetos. Una escuela es un objeto, y adentro tiene aulas, que son objetos, y cada aula tiene una puerta, que también lo es.
Estado, comportamiento e identidad
Si hay que aprender de memoria una sola fórmula de esta lección, es esta. Todo objeto se puede describir respondiendo tres preguntas:
El estado: lo que el objeto sabe
El estado es una de las posibles condiciones en que el objeto puede existir, y normalmente cambia a lo largo del tiempo. Se implementa con un conjunto de propiedades o atributos, más las conexiones que el objeto tiene con otros objetos.
El estado de un objeto abarca todas sus propiedades —que suelen ser fijas— más los valores actuales de cada una, que son los que cambian. Un auto tiene un cierto número de puertas, un cierto número de ruedas, un volante, un motor, pedales: esas propiedades no cambian. Lo que cambia es la velocidad, el nivel de combustible, si está encendido o apagado. Las dos cosas juntas forman el estado.
El comportamiento: lo que el objeto puede hacer
El comportamiento determina cómo actúa y reacciona el objeto frente a las peticiones de otros objetos. Se modela como el conjunto de mensajes a los que el objeto puede responder, y se implementa mediante métodos.
Volviendo al auto, su comportamiento sería: arrancar, frenar, doblar, acelerar. Dicho de otro modo: el estado es lo que el objeto sabe, y el comportamiento es lo que el objeto puede hacer con eso que sabe.
La identidad: lo que lo distingue
Cada objeto tiene una identidad única, incluso si su estado es idéntico al de otro objeto. Es la propiedad que lo distingue de todos los demás.
Dos autos del mismo modelo, del mismo color, con el mismo motor, salidos de la misma línea de producción el mismo día no dejan de ser dos autos diferentes, por más que todos sus atributos y todos sus valores sean iguales. La única posibilidad de que dos objetos sean el mismo objeto es que sean el mismo objeto. Esto no es una sutileza filosófica: es lo que permite distinguir dos alumnos que se llaman igual y tienen la misma edad, o dos facturas del mismo importe.
Qué es una clase
Si el objeto es la cosa, la clase es la descripción de un grupo de objetos que comparten tres cosas:
- Propiedades en común (los atributos).
- Comportamiento similar (las operaciones o métodos).
- La misma forma de relacionarse con otros objetos.
Y es, además, una abstracción: enfatiza las características relevantes y suprime las demás. Una clase nunca describe «todo» de un objeto; describe lo que importa para el problema que se está resolviendo.
La clase como molde
La comparación más útil es la del plano o molde: una clase es la plantilla a partir de la cual se crean las instancias particulares. De un mismo plano de una casa se construyen muchas casas, y todas comparten la distribución, aunque una sea blanca y la otra verde. Del mismo modo, de una clase se crean muchos objetos.
De ahí sale una distinción que hay que tener firme:
| Clase | Objeto | |
|---|---|---|
| Qué es | La definición, el molde, el tipo. | Una instancia concreta de esa definición. |
| Existe… | En el diseño y en el código fuente. | En la memoria, mientras el programa se está ejecutando. |
| Cuántos hay | Una sola definición por cada tipo de cosa. | Tantos como haga falta crear. |
| Ejemplo | Alumno |
a1, a2, a3… |
Ojo con una frase que se lee seguido: «las clases no tienen vida en el mundo real, los objetos sí». La interacción ocurre siempre entre objetos, nunca entre clases. Para poder usar una clase hay que crear una instancia: a ese proceso se lo llama instanciación.
La jerarquía de los términos
En el vocabulario del paradigma los términos están ordenados, y conviene conocer el orden para leer bibliografía sin perderse:
Un ejemplo de clase, con todo lo que tiene
Tomemos la clase Curso. Su estado serían las propiedades: nombre, ubicación, días ofrecidos, horario de inicio y horario de término. Su comportamiento, los métodos: agregar un alumno, borrar un alumno, entregar un listado del curso, determinar si está completo.
Notar cómo quedó definido todo desde el punto de vista del curso: el curso sabe si está completo, en lugar de que exista una función suelta afuera que reciba la lista de alumnos y cuente. El objeto es el que sabe de sí mismo.
Y un último ejemplo, que muestra hasta dónde llega la idea de «es un tipo de»: un auto es un tipo particular de vehículo motorizado, y dentro de su comportamiento están «arrancar» y «frenar». Una motocicleta también es un vehículo motorizado, y también tiene «arrancar» y «frenar». Los atributos se comparten —los dos tienen «cantidad de ruedas»— aunque los valores no coincidan: el auto tiene 4 y la moto, 2. Esa relación es la base de la herencia.
Atributos y métodos
Dentro de una clase hay dos cosas: los atributos, que describen el estado, y los métodos, que describen el comportamiento.
Los atributos
Un atributo representa alguna propiedad del elemento que se está modelando, y los valores que se le dan conforman el estado del objeto. Dos objetos son distintos aunque todos sus atributos tengan iguales valores: eso es la identidad, que ya se vio.
El atributo es la propiedad; el valor es lo que tiene esa instancia. Todos los alumnos tienen apellido, nombre y edad; cada alumno tiene los suyos.
Alumno a1: Alumno a2:
apellido = Fernández apellido = Pérez
nombre = Lucía nombre = Ana
edad = 15 edad = 16
Los métodos
Los métodos especifican la forma en que se controlan los datos de un objeto. La regla es tajante: no se puede tener acceso ni control de la estructura de datos excepto mediante los métodos que forman parte del objeto. Es decir, los atributos no se tocan desde afuera.
Un método es la contraparte funcional del mensaje: expresa cómo se lleva a cabo lo que el mensaje pide. Un método puede hacer básicamente tres cosas:
- Modificar el estado interno del objeto.
- Colaborar con otros objetos, enviándoles mensajes a su vez.
- Retornar un valor y terminar.
Casi todos los métodos hacen una combinación de las tres. Un método notaFinal() calcula y retorna; un método inscribir(alumno) modifica el estado del curso y además le manda un mensaje al alumno para avisarle.
Cómo se dibuja una clase: la notación UML
Para dibujar una clase se usa un rectángulo dividido en tres partes: el nombre arriba, los atributos en el medio y los métodos abajo. Y delante de cada atributo y de cada método va un signo que indica quién puede usarlo:
-significa privado: solo el propio objeto.+significa público: cualquiera que tenga el objeto.#significa protegido: el objeto y sus clases derivadas.
La notación del lenguaje UML (que se ve en detalle en la lección 3) se lee así: - apellido: String es un atributo privado llamado apellido y de tipo texto, y + darNombre(String) es un método público que recibe un texto.
Mensajes y eventos
Los objetos no viven aislados: el sistema funciona porque colaboran entre sí. La forma en que colaboran son los mensajes.
Los mensajes
Un mensaje es una petición de un objeto a otro para que ejecute uno de sus métodos. De ahí sale una definición que se da vuelta respecto de lo que uno esperaría: el comportamiento de un objeto está definido por los mensajes que ese objeto entiende. No por lo que el objeto es, sino por lo que acepta que le pidan.
Tres reglas prácticas:
- Para mandarle un mensaje a un objeto hay que conocerlo. El mensaje necesita un destinatario concreto, no una idea general.
- Al recibir el mensaje, el objeto activa el método asociado y responde con el resultado. El que manda el mensaje no necesita saber cómo está implementado el método: eso es asunto del que lo recibe.
- Si el método no existe, el mensaje no se entiende y hay un error. El conjunto de métodos públicos de una clase es, literalmente, el conjunto de mensajes que sus objetos entienden.
Un ejemplo típico: se le manda el mensaje titular() a una caja de ahorro. La caja activa su método correspondiente, busca el dato y retorna el objeto persona Juan Pérez. El que pidió el titular no sabe —ni le importa— si la caja tiene el nombre guardado en un texto, en un número de cliente o en otro objeto.
Ese desacople es el que permite que el sistema se pueda cambiar por partes: mientras la caja siga entendiendo el mensaje titular(), se puede reescribir todo su interior sin tocar una sola línea del código que la usa.
Los eventos
Los eventos son los estímulos que un objeto ejerce sobre otro: el mecanismo por el que una clase interactúa con otras o con el propio usuario. Un evento avisa que algo ocurrió, y alguien lo maneja.
Los eventos aparecen todo el tiempo aunque no se los nombre: cada tecla que se aprieta, cada clic en un botón o en un enlace, cada cambio de tamaño de una ventana es un evento. La diferencia con un mensaje es de origen: el mensaje lo manda otro objeto del programa por una decisión del código, mientras que el evento lo dispara algo externo —el usuario, el sistema operativo, un temporizador— y el programa tiene que estar preparado para reaccionar.
Los cinco pilares
Todo lo anterior —objeto, clase, atributos, métodos, mensajes— es el vocabulario. Ahora vienen las herramientas con las que se construye un diseño: cinco propiedades que el paradigma pone a disposición y que, combinadas, son las que hacen que un sistema grande se pueda entender.
Un resumen en una frase por pilar, antes de desarrollarlos:
| Pilar | En una frase |
|---|---|
| Abstracción | Quedarse con las características esenciales del problema e ignorar a propósito todo lo demás. |
| Encapsulamiento | Que el estado interno de un objeto solo lo pueda modificar ese mismo objeto, a través de sus métodos. |
| Relaciones | Cómo se conectan las clases entre sí: por asociación o por agregación. |
| Herencia | Organizar las clases de lo general a lo específico, reutilizando lo que ya está definido. |
| Polimorfismo | Mandar el mismo mensaje a objetos distintos y que cada uno responda a su manera. |
Abstracción
La abstracción es la propiedad que permite seleccionar las características esenciales y relevantes de un objeto, e identificar comportamientos comunes, para definir nuevos tipos de entidades.
Es el primer paso de cualquier análisis, y el más determinante: mediante la abstracción se llega al conjunto de clases que van a modelar la realidad o el problema que se quiere atacar. Si las clases están mal elegidas, ningún código prolijo lo va a arreglar después.
Ignorancia selectiva
La abstracción también se explica por lo que deja afuera, y ahí el nombre es muy preciso: ignorancia selectiva. Sirve para trabajar con cosas complejas porque se enfoca en lo importante y descarta lo que no lo es. Eso es simplificar, y simplificar no es un defecto: es la única forma de que un modelo sea utilizable.
Dos consecuencias que quedaron anotadas:
- Una clase debe capturar una y solo una abstracción clave. Si una clase responde a dos ideas distintas, en realidad son dos clases.
- El proceso de abstracción es subjetivo y dependiente del contexto. Ante el mismo automóvil, un observador interesado en la mecánica lo va a caracterizar como un vehículo con cierto tipo de motor, mientras que otro, en un contexto distinto, lo va a caracterizar como un medio de transporte con capacidad para trasladar personas o cosas, sin importar qué motor tenga. Los dos modelos son válidos: sirven para problemas distintos.
Las dos formas de abstraer: «es un» y «tiene un»
Hay dos maneras de dividir un problema, y el paradigma las distingue con dos expresiones que se usan todo el tiempo:
- tiene-un (dividir en partes). Un auto tiene-un motor, tiene-una transmisión. Es la composición: el objeto está armado con otros objetos. En el diseño se resuelve con un atributo cuyo tipo es otra clase.
- es-un (dividir en especializaciones). Una bicicleta es-un vehículo rodado, que a su vez es-un medio de transporte. Es la generalización: un tipo de cosa que es un caso particular de otra. En el diseño se resuelve con la herencia.
Reconocer cuál de las dos corresponde en cada caso es una de las decisiones más importantes del diseño, y equivocarse tiene consecuencias. La relación «es un» se desarrolla en la sección de herencia, y es la que después explica los principios SOLID de la lección 5: la mayoría de los diseños torcidos empiezan por haber usado herencia donde iba composición.
Encapsulamiento
El encapsulamiento es la propiedad que permite asegurar que el contenido de la información de un objeto está oculto al mundo exterior. La formulación de los apuntes es la más clara: el único que puede manipular el estado interno de un objeto es el mismo objeto; un objeto no puede invadir el espacio privado de otro.
En la práctica se implementa así: los atributos propios de un objeto no deben ser visibles desde otros objetos, o sea que se declaran privados, y el estado se expone solamente a través del comportamiento definido mediante miembros públicos.
Para qué sirve, concretamente
Dos razones, y las dos se ven mejor con un ejemplo. Supongamos un atributo edad al que se puede acceder sin control:
// Sin encapsulamiento: el atributo está expuesto
a1.edad = -5; // nadie lo impide, y el objeto queda en un estado imposible
Con el atributo privado y un método público en el medio, ese acceso pasa por un punto de control, y ahí se puede verificar antes de aceptar el cambio:
// Con encapsulamiento: el método es la única puerta
public void setEdad(int edad) {
if (edad < 0 || edad > 120) {
throw new IllegalArgumentException("Edad inválida: " + edad);
}
this.edad = edad;
}
Las dos razones son, entonces:
- Punto de control y validación. El objeto puede garantizar que nunca va a estar en un estado inválido, porque todas las modificaciones pasan por métodos que verifican.
- Mejor respuesta ante los cambios. Como el interior está oculto, se puede reescribir sin que nadie se entere. Si mañana conviene guardar la fecha de nacimiento en lugar de la edad, se cambia el atributo y la manera de calcularlo; el método
getEdad()sigue existiendo y sigue retornando lo mismo. Toda la aplicación que lo usa sigue funcionando sin tocar una línea.
Y hay un tercer beneficio que aparece en los sistemas grandes: permite abstraer al resto del mundo de la complejidad de la implementación interna. Quien usa un objeto solo necesita conocer qué mensajes entiende, no cómo está hecho por dentro.
get y un set que no verifican nada, el resultado es el mismo que haber dejado el atributo público, con más líneas de código. La privacidad no está en la palabra private: está en qué se decide exponer. Un método set que solo copia el valor no protege nada.
Relaciones
Todo sistema abarca muchas clases y objetos, y los objetos contribuyen al comportamiento del sistema colaborando entre sí. Esa colaboración se logra a través de las relaciones, y hay dos tipos principales: asociación y agregación.
Asociación
Una asociación es una conexión entre dos clases que representa una comunicación. Tres cosas que hay que definir en una asociación:
- El nombre. La asociación puede tener nombre, y ese nombre describe qué une a las dos clases: «dueño», «inscripto», «pertenece a».
- La dirección. La comunicación puede ser unidireccional o bidireccional; por omisión, bidireccional.
- La multiplicidad. Es el número de instancias que participan en la relación: uno, muchos, de cero a muchos.
El ejemplo clásico: una Persona es dueña de un Vehículo, y un Vehículo pertenece a una Persona.
Agregación
La agregación es una forma especial de asociación donde un todo se relaciona con sus partes. También se la conoce como relación «una parte de» o de contención.
El ejemplo: una Puerta es una parte de un Vehículo. Lo que muestra que la relación es más fuerte que una asociación común son las consecuencias: si el vehículo es azul, la puerta es azul; mover el vehículo implica mover la puerta. No se puede pensar la parte sin el todo.
Vale la pena enlazar esto con lo anterior: la agregación es la forma que toma en el diseño la abstracción «tiene-un». Y la asociación también se implementa con atributos: si una Persona es dueña de un Vehiculo, la clase Persona tendrá un atributo de tipo Vehiculo.
Herencia
El propósito principal de la herencia es organizar mejor las clases que componen una realidad: agruparlas en función de atributos y comportamientos comunes, y dejar que cada una se especialice según sus particularidades.
En términos formales, es la relación entre clases que permite reutilizar los atributos y las operaciones definidas en otra clase más general, de la cual se hereda o deriva. Cada clase que hereda de otra posee:
- Los atributos de la clase base, además de los propios.
- Todos o algunos de los métodos de la clase base, que puede usar tal como están o redefinir.
Los nombres de los dos lados de la relación son: clase base o superclase (la general) y clase derivada o subclase (la especializada).
Transporte y agregan lo propio. Avanzar() está declarado una sola vez y sirve para los tres.Herencia «es un»
La herencia se justifica cuando la subclase es un tipo específico de la superclase. Es la relación «es un» de la abstracción, y los ejemplos sirven para reconocerla:
- Un cuadrado es un rectángulo.
- Un perro es un mamífero.
- Un automóvil es un vehículo a motor.
Si la frase «X es un Y» se puede decir sin forzarla, la herencia es el camino. Si no se puede, conviene revisar: casi siempre la relación correcta es de composición —«tiene un»— y forzar la herencia produce un diseño que después no se puede corregir sin romper todo.
Herencia simple y herencia múltiple
- Herencia simple: una clase derivada hereda de una sola clase base.
- Herencia múltiple: una clase derivada hereda de una o más clases base.
La herencia múltiple parece una buena idea y trae un problema conocido: si dos clases base definen un método con el mismo nombre, ¿cuál hereda la subclase? Por eso Java no la permite. El lenguaje ofrece otra herramienta para lo que la herencia múltiple intentaba resolver —las interfaces— y de eso se ocupa la lección 3.
Polimorfismo
El último pilar es el que le da sentido a los anteriores. El polimorfismo es la propiedad que tienen los objetos de permitir invocar genéricamente un comportamiento cuya implementación será delegada al objeto correspondiente recién en tiempo de ejecución.
Dicho de otra manera, que es como conviene recordarlo: permite desencadenar operaciones diferentes en respuesta a un mismo mensaje.
Los apuntes agregan una aclaración importante: el polimorfismo tiende a existir en las relaciones de herencia, pero no siempre es así. Es un concepto independiente, que en Java se implementa principalmente con herencia y con interfaces.
Tres frases que explican todo
- La definición del método reside en la clase base. Es la clase general la que declara que existe un mensaje
avanzar(), aunque no diga cómo se hace. - La implementación reside en la clase derivada. Cada subclase define qué significa avanzar para ella: las ruedas girando, las turbinas empujando.
- La invocación se resuelve al momento de ejecución. Cuando el programa manda el mensaje, todavía no se sabe qué implementación se va a ejecutar: eso lo decide el objeto que lo recibe, cuando lo recibe.
Cómo se ve en el código
Este es el ejemplo que hace clic, y que la lección 3 va a desarrollar con la sintaxis completa:
Transporte t = new Avion(); // la variable es de tipo Transporte...
t.avanzar(); // ...pero el objeto guardado es un Avion
La primera línea se lee como una contradicción: se declara una variable de un tipo y se le asigna un objeto de otro. No es una contradicción, es exactamente el mecanismo: un Avion es un Transporte, así que un objeto Avion puede guardarse donde se espera un Transporte.
Y ahí pasa lo interesante. Cuando el compilador lee t.avanzar() verifica una sola cosa: que la clase Transporte declare un método avanzar(). Que exista. Nada más. No puede saber —ni le interesa— qué objeto concreto va a estar guardado en t en el momento de ejecutar esa línea, porque eso puede depender de datos que llegan del usuario. Así que deja la decisión para después: en el momento en que se ejecuta la instrucción, la máquina virtual mira la clase real del objeto que hay en t y ejecuta esa implementación. A ese mecanismo se lo llama enlace dinámico.
La ganancia práctica es enorme y es la razón por la que existe el pilar. Si el código recorre una lista de transportes y le manda avanzar() a cada uno, ese código no hay que tocarlo cuando mañana aparezca un tercer tipo de transporte: el código nuevo se agrega aparte y el bucle sigue funcionando. Sin polimorfismo, ese bucle tendría que preguntar de qué tipo es cada objeto —if es auto… else if es avión…— y esa lista de condiciones habría que ampliarla cada vez.
Todo esto, en Java
Para cerrar, el mismo material escrito en Java. No hace falta entender la sintaxis todavía —eso es la lección 2 y sobre todo la 3—: la idea es reconocer los conceptos de esta lección dentro del código y ver que cada uno tiene su forma concreta en el lenguaje.
public class Alumno {
// Estado: los atributos, declarados privados (encapsulamiento)
private String nombre;
private String apellido;
private int edad;
// Constructor: inicializa el objeto en el momento en que se lo crea
public Alumno(String nombre, String apellido, int edad) {
this.nombre = nombre;
this.apellido = apellido;
setEdad(edad);
}
// Un punto de control: el objeto no acepta un estado imposible
public void setEdad(int edad) {
if (edad < 0 || edad > 120) {
throw new IllegalArgumentException("Edad inválida: " + edad);
}
this.edad = edad;
}
public int getEdad() {
return edad;
}
// Comportamiento: el objeto calcula lo suyo, no se lo piden desde afuera
public String nombreCompleto() {
return apellido + ", " + nombre;
}
}
Y ahora un programa que usa esa clase, con cada operación anotada:
public class Escuela {
public static void main(String[] args) {
// Instanciación: del molde salen los objetos
Alumno a1 = new Alumno("Lucía", "Fernández", 15);
Alumno a2 = new Alumno("Ana", "Pérez", 16);
// Mensajes: le pedimos al objeto que haga algo que sabe hacer
System.out.println(a1.nombreCompleto()); // Fernández, Lucía
// Encapsulamiento: el cambio pasa por el método, que lo rechaza
a2.setEdad(-3); // error: Edad inválida: -3
// Identidad: dos objetos con los mismos datos siguen siendo dos
Alumno a3 = new Alumno("Lucía", "Fernández", 15);
System.out.println(a1 == a3); // false
}
}
La correspondencia entre los conceptos de la lección y el lenguaje es directa:
| Concepto | Cómo se escribe en Java |
|---|---|
| Clase (el molde) | class Alumno { … } |
| Objeto (la instancia) | new Alumno("Lucía", "Fernández", 15) |
| Atributo | Una variable declarada adentro de la clase: private int edad; |
| Método | Una función declarada adentro de la clase: public int getEdad() { … } |
| Encapsulamiento | La palabra private en los atributos y los métodos públicos como única puerta |
| Instanciación | La palabra reservada new |
| Mensaje | a1.nombreCompleto() |
| Herencia («es un») | class Profesor extends Persona { … } |
| Composición («tiene un») | Un atributo cuyo tipo es otra clase: private Direccion domicilio; |
| Polimorfismo | Persona p = new Profesor(…); |
Si algo de esto resultó llamativo —por qué los atributos van privados, por qué se puede guardar un Profesor en una variable de tipo Persona, para qué sirve el constructor—, ese es el camino: cada una de esas preguntas se responde en la lección 3.
Del problema al programa
Todo lo anterior describe qué es el modelo. Falta lo otro: cómo se llega desde un enunciado escrito en castellano hasta un programa que funcione. Ese camino tiene cuatro etapas, y en un trabajo ordenado no se saltea ninguna, porque cada una se apoya en la anterior.
| Etapa | Qué se hace | Con qué se termina |
|---|---|---|
| 1. Análisis | Entender el problema y encontrar las cosas que participan en él. Los sustantivos del enunciado son los objetos candidatos; los verbos, las responsabilidades —o sea, los métodos—. | La lista de clases con sus atributos y sus métodos. |
| 2. Diseño | Dibujar el diagrama de clases con las relaciones entre ellas, y escribir la lógica de cada método en pseudocódigo, antes de tocar un lenguaje. | El diagrama UML y el pseudocódigo. |
| 3. Programación | Traducir el diseño al lenguaje elegido: escribir el código, compilarlo y corregir lo que el compilador marque. | Un programa que compila y se ejecuta. |
| 4. Implementación | Probarlo con datos reales —incluidos los casos límite, que son los que rompen todo— y ponerlo a funcionar en su ambiente definitivo. | El programa probado y en uso. |
El mismo camino, con un ejemplo corto
Supongamos que el enunciado es este: «la biblioteca necesita registrar los préstamos de libros a los socios y saber cuáles están vencidos». Recorriendo las cuatro etapas:
- Análisis. Los sustantivos son
Socio,LibroyPrestamo. Los verbos —registrar un préstamo, saber si venció— son las responsabilidades, y cada una va a ser un método. Y hay una trampa que conviene ver desde el principio: vencido no es un objeto ni una clase, es un dato calculado. No se guarda «vencido = sí» en un atributo, porque al día siguiente ese valor sería mentira: se guardan la fecha de retiro y el plazo, y el método compara con la fecha de hoy cada vez que se lo consulta. Distinguir qué se guarda de qué se calcula es la mitad del análisis. - Diseño. Se dibuja la relación: un
Prestamotiene unSocioy unLibro—son atributos cuyo tipo es otra clase, o sea composición, y en el diagrama va con el rombo— y guarda la fecha de retiro y la de vencimiento. Después, el pseudocódigo de cada método: si la fecha de hoy pasó la de vencimiento y no hay fecha de devolución, el préstamo está vencido. - Programación. Se escriben las tres clases en Java. Esto es el trabajo de la lección 3: declarar los atributos, el constructor que no deje nacer un objeto inválido, los
setyget, y cada método con su lógica. - Implementación. Se prueban los casos que importan: un socio sin préstamos, un préstamo devuelto a tiempo, uno devuelto tarde —que no está vencido, porque ya se devolvió—, uno que vence hoy y uno que vence mañana. Los de los extremos son los que descubren los errores de comparación.
Dos atajos que ayudan en serio, y que valen para toda la vida del programador: dibujar el diagrama antes de escribir —aunque sea en un papel, y aunque después cambie— y escribir en pseudocódigo la lógica de los métodos difíciles. Traducir a Java un procedimiento que ya está resuelto en castellano es fácil; resolverlo al mismo tiempo que se pelea con la sintaxis, no. El resto de las herramientas de diseño, y por qué se organiza el código en clases de cierta manera, ocupa la lección 5.
Ejercicios
Siete ejercicios, todos resolubles con lo que se vio en esta página. Conviene intentarlos antes de mirar las respuestas. Los tres primeros son de los apuntes originales; los cuatro últimos agregan lo que se desarrolló acá.
1. Clases y objetos
Decidir cuáles de estos son clases y cuáles son objetos: Auto, mi auto, Alumno, el alumno Lucía Fernández, Factura, la factura número 1204, Curso, el curso 4º B.
2. Descubrir los objetos
Elegir un lugar conocido —una biblioteca, un kiosco, un aula— y hacer el trabajo del analista: listar cinco objetos que aparezcan ahí, y para cada uno indicar si es palpable o abstracto.
3. Definir la estructura
Para tres de los objetos del ejercicio anterior, escribir en notación UML la clase correspondiente: los atributos con el signo -, los métodos con el signo +, y el tipo de dato de cada uno.
4. Estado, comportamiento e identidad
Para una cuenta bancaria: escribir tres atributos (estado), tres métodos (comportamiento), y explicar con un ejemplo por qué dos cuentas con el mismo saldo y el mismo titular siguen siendo dos objetos distintos.
5. «Es un» o «tiene un»
Indicar si la relación es de herencia (es un) o de composición (tiene un), y en cada caso decir cuál es la clase base y cuál la derivada, o cuál es el todo y cuál la parte:
- Un Auto y un Motor.
- Una Moto y un Vehículo.
- Una Factura y sus Ítems.
- Un Profesor y una Persona.
- Una Escuela y sus Aulas.
- Un Cuadrado y un Rectángulo.
6. Encapsulamiento
Se está modelando una clase Fecha con tres atributos: día, mes y año. Explicar por qué conviene que sean privados y escribir el método setMes con la validación correspondiente. Después responder: ¿qué se gana el día que haya que cambiar los tres atributos por un solo número de día juliano?
7. Del enunciado al modelo
Este es el enunciado: «Un taller mecánico necesita registrar las reparaciones que les hace a los autos de sus clientes y saber cuánto tiene que facturarle a cada uno». Hacer el trabajo de las dos primeras etapas:
- Separar los sustantivos de los verbos, y decidir qué es una clase, qué es un atributo y qué es un dato que se calcula.
- Escribir la lista de clases con sus atributos y sus métodos.
- Indicar qué relación hay entre ellas y de qué lado va cada rombo.
Respuestas sugeridas
1. Son clases: Auto, Alumno, Factura, Curso. Son objetos: mi auto, el alumno Lucía Fernández, la factura número 1204, el curso 4º B. La marca que los distingue es el artículo determinado y el dato concreto: «mi auto» es uno de los autos, «Auto» es la idea de todos.
2 y 3. No hay una respuesta única, y ese es justamente el punto: depende del contexto. Si el lugar elegido es una biblioteca, los objetos palpables pueden ser el libro, el estante y la mesa, y los abstractos el préstamo y el socio —que no se tocan—. Una estructura posible para el libro:
Libro
- titulo: String
- autor: String
- isbn: String
- disponible: boolean
---
+ prestar(socio)
+ devolver()
+ estaDisponible(): boolean
Vale la pena comparar lo que cada uno eligió: dos personas que modelan la misma biblioteca rara vez escriben el mismo conjunto de atributos, y las dos listas pueden estar bien. Lo que se evalúa es que la clase describa un solo concepto.
4. Atributos: titular, saldo, numeroCuenta. Métodos: depositar(importe), extraer(importe), consultarSaldo(). Sobre la identidad: dos cuentas de la misma persona con el mismo saldo son dos cuentas distintas; cada una tiene un número propio, y sacar plata de una no afecta a la otra. El estado puede ser idéntico y los objetos siguen siendo dos.
5.
- a) tiene un: el Auto es el todo, el Motor la parte. Un auto no es un motor.
- b) es un:
Vehículoes la clase base yMotola derivada. Una moto es un vehículo. - c) tiene un: la Factura es el todo y cada Ítem una parte. Una factura sin ítems no tiene sentido.
- d) es un:
Personabase,Profesorderivada. Un profesor es una persona. - e) tiene un: la Escuela es el todo, las Aulas las partes.
- f) es un:
Rectángulobase,Cuadradoderivada. Atención: este es el ejemplo más incómodo de todos —un cuadrado es un rectángulo, pero si el rectángulo permite cambiar el ancho y el alto por separado, el cuadrado no lo puede reemplazar sin romper algo—. Guardalo: es exactamente el problema que describe el principio de sustitución de Liskov en la lección 5.
6. Los atributos van privados porque hay combinaciones que no son fechas válidas —el 31 de febrero, el mes 13, el día 0—. Si el día o el mes se pudieran escribir desde afuera, cualquiera podría dejar al objeto en un estado imposible. El método con la validación:
public void setMes(int mes) {
if (mes < 1 || mes > 12) {
throw new IllegalArgumentException("Mes inválido: " + mes);
}
this.mes = mes;
}
Y la ganancia el día que haya que cambiar la representación interna: si los llamadores solo conocen getDia(), getMes() y getAno(), se puede reemplazar el atributo por un número de día juliano —o por la clase LocalDate de la biblioteca estándar— y ninguno de esos llamadores se entera. Sin encapsulamiento, ese cambio obliga a revisar todo el programa buscando quién tocaba los atributos. Es el «mejor respuesta ante los cambios» que se vio en la sección de encapsulamiento.
7. (a) Los sustantivos del enunciado son taller, reparación, auto, cliente y factura; los verbos, registrar y facturar. No todos los sustantivos son clases: taller es el sistema —el programa—, no una cosa que se modele adentro de él. Y factura es un dato calculado, no un objeto: se arma sumando las reparaciones del cliente. En cambio cliente, auto y reparación sí son cosas con estado propio que aparecen muchas veces, y esos son los tres candidatos a clase. Los verbos dan los métodos: registrar una reparación, calcular el total.
(b) Una lista posible:
| Clase | Atributos | Métodos |
|---|---|---|
Cliente |
nombre, telefono, autos (una colección de Auto) |
agregarAuto(auto), totalAFacturar() |
Auto |
patente, marca, modelo, ano, reparaciones (una colección de Reparacion) |
agregarReparacion(r), historial() |
Reparacion |
fecha, descripcion, horas, precioHora, importeRepuestos |
costoTotal(), estaTerminada() |
Ahí aparece otra decisión interesante: el costoTotal() se calcula multiplicando las horas por el precio, y sin embargo el precioHora se guarda. No es una contradicción: el total no se guarda porque sale de los otros datos, pero el precio de la hora sí, porque cambia con el tiempo —si sube la tarifa, el total de una reparación del año pasado tiene que seguir siendo el que se cobró—. El criterio general: se guarda lo que no se puede volver a saber, y se calcula lo que sale de lo guardado.
(c) Las relaciones y el lado del rombo:
ClienteyAuto: agregación, rombo blanco del lado del cliente. El auto existe aunque el cliente deje de venir al taller.AutoyReparacion: composición, rombo negro del lado del auto. Una reparación de este sistema no se entiende sin el auto al que se le hizo; si se borra el auto, su historial no tiene dónde vivir.- Y el «cuánto facturarle a cada uno» no agrega ninguna clase: es el método
totalAFacturar()del cliente, que recorre sus autos y suma. Fijate que las tres decisiones de este punto —qué es clase, qué se guarda y qué se calcula— no salen del lenguaje sino del problema. Eso es lo que se hace en las etapas 1 y 2, antes de escribir la primera línea de código.
Resumen
Lo que hay que poder decir de memoria al terminar esta lección:
- Un paradigma es una manera de modelar; la orientación a objetos modela con entidades que interactúan entre sí.
- Un objeto es una entidad con significado y límites claros dentro del problema, y siempre tiene estado, comportamiento e identidad.
- Una clase es la descripción de un grupo de objetos: es el molde. Los objetos son las instancias, y se crean por instanciación.
- Los atributos forman el estado; los métodos son el comportamiento. Los mensajes son las peticiones que un objeto le hace a otro, y definen qué entiende cada objeto.
- Los cinco pilares son abstracción, encapsulamiento, relaciones, herencia y polimorfismo:
| Pilar | Lo que hay que retener |
|---|---|
| Abstracción | Se eligen las características relevantes y se descarta el resto. Es subjetiva: depende del problema. Tiene dos formas, «es un» y «tiene un». |
| Encapsulamiento | El estado interno solo lo toca el objeto dueño. Atributos privados, métodos públicos. Da control y permite cambiar el interior sin romper a nadie. |
| Relaciones | Asociación: comunicación entre dos clases, con nombre, dirección y multiplicidad. Agregación: relación todo-partes; la parte no se explica sin el todo. |
| Herencia | Organiza las clases de lo general a lo específico y permite reutilizar. Se justifica solo cuando la subclase es un caso particular de la base. |
| Polimorfismo | El mismo mensaje produce respuestas distintas según el objeto. La definición está en la base, la implementación en la derivada, y la decisión se toma al ejecutar. |
Si queda una sola confusión, que no sea esta: una clase no es un objeto. La clase es el plano; el objeto es la casa construida con ese plano. Y el plano puede existir sin que se haya construido ninguna casa.
Licencia. Obra derivada de los apuntes de clase del Prof. Matías E. García —Introducción a la Programación Orientada a Objetos—, publicada en profmatiasgarcia.com.ar bajo CC BY-NC-SA 4.0 Internacional; el desarrollo del texto, los diagramas y los ejercicios son de Prof Mauro Bobyk.