La orientación a objetos
Qué es una clase y qué es un objeto, cómo se comunican por mensajes, los cuatro pilares (abstracción, encapsulación, herencia y polimorfismo) y la diferencia entre sobrecarga y sobrescritura.
Clase y objeto
Una clase es una estructura de programación que define, para un tipo de entidad, qué información guarda y qué sabe hacer. La información son sus atributos y lo que sabe hacer son sus métodos. La clase no es la cosa: es el molde que describe cómo son todas las cosas de esa familia.
Un objeto es un ejemplar concreto de una clase, lo que se llama una instancia. De la clase Alumno hay un solo molde, pero puede haber diez mil objetos Alumno vivos a la vez, cada uno con sus propios valores.
Todo objeto tiene dos cosas. El estado es el valor que tienen sus atributos en un momento dado, y cambia con el tiempo: un alumno que hoy lleva 12 temas cerrados mañana llevará 13. La identidad es el hecho de ser ese objeto y no otro, es decir, el sitio de memoria donde vive su información. La identidad no cambia nunca y es lo que permite distinguir dos objetos.
Dos objetos con exactamente los mismos valores en todos sus atributos siguen siendo dos objetos distintos: los separa la identidad, no el estado. Por eso comparar objetos con el operador de igualdad de referencias y compararlos por contenido no es lo mismo.
| Clase | Objeto | |
|---|---|---|
| Qué es | Una definición, un molde | Un ejemplar concreto |
| Cuántos hay | Una por tipo de entidad | Tantos como haga falta |
| Cuándo existe | Se escribe al programar | Se crea al ejecutar |
| Qué contiene | Atributos y métodos declarados | Valores concretos en esos atributos |
Para el examen
Clase: el molde
Objeto: la instancia
Estado: los valores de sus atributos; cambia
Identidad: ser ese objeto; no cambia, aunque dos objetos tengan iguales valores
Atributos, métodos y mensajes
Un atributo es un dato de negocio del objeto, como el nombre de un alumno o su fecha de alta, o bien una referencia a otros objetos, como la lista de intentos que ha hecho. Esa segunda forma es la que teje la red de objetos: los atributos que apuntan a otros objetos son lo que en el diagrama de clases se dibuja como una relación.
Un método es la implementación de un comportamiento: el código que hace algo con el estado del objeto. Corregir un intento, calcular el porcentaje de aciertos o marcar un tema como repasado son métodos.
Un mensaje es la llamada a un método sobre un objeto concreto. Se habla de mensajes y no de simples llamadas a función porque siempre hay un destinatario: el objeto que recibe la petición decide qué código ejecuta. Esa distinción es la puerta del polimorfismo.
public class Intento {
private Alumno alumno; // atributo de referencia
private Tema tema; // atributo de referencia
private int aciertos; // atributo de negocio
private int total; // atributo de negocio
public double porcentajeDeAciertos() { // método
if (total == 0) return 0;
return (aciertos * 100.0) / total;
}
}
// En otra clase, esto es un mensaje: se le pide a ESE intento
// que calcule su porcentaje.
double nota = intentoDeAyer.porcentajeDeAciertos();Para el examen
Atributos: datos del objeto o referencias a otros objetos
Método: el comportamiento
Mensaje: la llamada a un método sobre un objeto concreto
Por qué importa el mensaje: es la puerta del polimorfismo
Abstracción y encapsulación
La abstracción consiste en capturar del negocio solo los detalles que hay que modelar y dejar fuera todo lo demás. Se hace en dos pasos: primero se identifican los objetos que existen en el problema real y después se agrupan en clases. Un alumno de una plataforma de oposiciones tiene un nombre y un plan contratado, pero su color favorito no se modela porque no sirve para nada.
La encapsulación es la ocultación de los detalles internos del objeto. Desde fuera se ve qué operaciones ofrece, no cómo las consigue ni cómo guarda sus datos. Su efecto práctico es reducir el acoplamiento: si nadie depende de cómo está hecho por dentro, cambiar lo de dentro no rompe nada.
Tocar un atributo desde fuera, del estilo intento.aciertos = 100, es exactamente lo que la encapsulación prohíbe. La regla práctica es atributos privados y comportamiento público: quien quiera cambiar el estado, que pida al objeto que lo cambie él.
| Visibilidad | Quién puede acceder | Símbolo en UML |
|---|---|---|
| Privada | Solo la propia clase | signo menos |
| De paquete | Las clases del mismo paquete | virgulilla |
| Protegida | La propia clase, sus subclases y el paquete | almohadilla |
| Pública | Cualquiera | signo más |
Para el examen
Abstracción: modelar solo lo relevante del negocio
Encapsulación: ocultar el interior: atributos privados y comportamiento público
Para qué: reducir el acoplamiento
Símbolos UML: menos privado, almohadilla protegido y más público
Herencia, herencia múltiple y composición
La herencia es la capacidad de definir unas clases en términos de otras. Una subclase extiende a su superclase: hereda sus atributos y sus métodos, puede añadir los suyos y puede cambiar el comportamiento de los que hereda. Se usa cuando hay una similitud semántica real, no cuando dos clases se parecen por casualidad. Su efecto principal es la reutilización.
La prueba para saber si toca herencia es la frase «es un». Un TestDeTema es un Test, así que hereda. Un Intento no es un Alumno aunque tenga uno dentro, así que ahí no hay herencia sino composición.
La herencia simple permite una sola superclase directa; la herencia múltiple permite varias. La herencia múltiple entre clases genera el llamado problema del diamante: si dos superclases distintas traen una implementación del mismo método y una tercera clase hereda de las dos, no está definido cuál se ejecuta. C++ la admite y obliga al programador a resolver la ambigüedad; Java y C# no la admiten entre clases precisamente para evitarla.
En Java una clase extiende como mucho a una clase, pero puede implementar tantas interfaces como quiera. Así se consigue el efecto útil de la herencia múltiple, que es tener varios tipos a la vez, sin heredar dos implementaciones en conflicto.
La alternativa a heredar es la composición: en lugar de extender una clase para aprovechar su código, se guarda una instancia de ella como atributo y se le delega el trabajo. Se reutiliza igual, sin quedar atado a una jerarquía y sin arrastrar métodos que no vienen a cuento. Por eso la recomendación clásica de diseño es favorecer la composición frente a la herencia.
Para el examen
Herencia: expresa «es un»
Composición: expresa «tiene un»
Herencia múltiple: Java y C# no la admiten entre clases, por el problema del diamante
Lo que sí admiten: implementar varias interfaces
Regla clásica: favorecer la composición frente a la herencia
Clases abstractas e interfaces
Una clase abstracta es una clase de la que no se pueden crear objetos: existe para ser heredada. Sirve cuando varias subclases comparten estado y parte del comportamiento, y solo se diferencian en unos pocos métodos, que se declaran abstractos para que cada subclase los rellene.
Una interfaz es un contrato puro: la lista de operaciones que una clase se compromete a ofrecer, sin decir nada de cómo. No tiene estado propio y no impone jerarquía, así que clases que no se parecen en nada pueden implementar la misma interfaz.
| Clase abstracta | Interfaz | |
|---|---|---|
| Relación que expresa | «Es un», parentesco | «Sabe hacer», capacidad |
| Atributos de estado | Sí | No, solo constantes |
| Constructor | Sí, lo usan las subclases | No |
| Cuántas se pueden tener | Una sola superclase | Todas las que hagan falta |
| Código heredado | Métodos ya implementados | Solo la firma, salvo métodos por defecto |
// Clase abstracta: comparte estado y deja un hueco.
public abstract class Test {
protected List<Pregunta> preguntas;
public int numeroDePreguntas() { return preguntas.size(); }
public abstract List<Pregunta> seleccionar(); // cada test lo hace a su modo
}
// Interfaz: un contrato, sin estado ni jerarquía.
public interface Corregible {
Resultado corregir(Respuesta respuesta);
}
public class TestDeFalladas extends Test implements Corregible { /* ... */ }Si lo que comparten las clases es código y datos, clase abstracta. Si lo que comparten es solo la forma de ser usadas, interfaz. En la duda, interfaz: no consume la única herencia disponible.
Para el examen
Clase abstracta: no se instancia, aporta estado y código, y solo se hereda una
Interfaz: contrato sin estado; se pueden implementar tantas como haga falta
Polimorfismo y ligadura dinámica
El polimorfismo es la capacidad de que, ante el mismo mensaje, la respuesta dependa de cuál sea la clase real del objeto que lo recibe. Visto desde el otro lado: objetos de clases distintas pueden hacerse pasar por el mismo tipo, de modo que quien los usa no necesita saber con cuál está tratando.
Lo que persigue es código más genérico y, con ello, un mantenimiento más barato. Conviene precisar el matiz: genérico quiere decir que la codificación es genérica, es decir, que está escrita contra un tipo abstracto, no que el uso sea genérico. El programa sigue haciendo cosas concretas; lo que es general es la forma de escribirlo.
- Teoría de tipos: el polimorfismo consiste en jugar con los tipos. Los tipos están relacionados entre sí y, cuando lo están, resultan intercambiables.
- En toda situación polimórfica hay dos actores: un cliente, que envía el mensaje contra el tipo abstracto, y un proveedor, que es el objeto concreto que responde.
El mecanismo que lo hace posible es la ligadura dinámica, también llamada enlace tardío o late binding: hasta el último momento no se sabe a qué método de qué clase se va a llamar. En tiempo de ejecución, la máquina virtual mira de qué tipo es realmente el objeto que recibió el mensaje y solo entonces decide qué método invoca.
public abstract class Test {
public abstract List<Pregunta> seleccionar();
}
public class TestDeTema extends Test { /* seleccionar() de un tema */ }
public class TestDeFalladas extends Test { /* seleccionar() de lo fallado */ }
// El cliente no sabe ni le importa qué test le han pasado.
public void lanzar(Test test) {
List<Pregunta> preguntas = test.seleccionar(); // ligadura dinámica
// ...
}El polimorfismo no está en la clase que ofrece varios comportamientos, sino en el código que puede ignorarlos. Si al añadir un tipo nuevo hay que tocar el cliente, no había polimorfismo.
Para el examen
Polimorfismo: ante el mismo mensaje, la respuesta depende de la clase real del objeto
Su mecanismo: la ligadura dinámica o late binding
Cuándo se elige el método: en tiempo de ejecución
Sobrecarga frente a sobrescritura
La sobrecarga es tener varios métodos con el mismo nombre dentro de la misma clase, diferenciados por el número y el tipo de sus parámetros, es decir, por su firma. Sirve para ofrecer la misma operación con distintas comodidades de llamada. Como el compilador ya sabe qué argumentos le están pasando, elige el método en tiempo de compilación: es ligadura estática.
La sobrescritura, u override, es otra cosa: una subclase vuelve a escribir un método que ya existía en su superclase, con exactamente la misma firma, para cambiarle el comportamiento. Aquí la decisión no la puede tomar el compilador, porque depende de cuál sea la clase real del objeto en ese momento, así que se resuelve en tiempo de ejecución por ligadura dinámica.
| Sobrecarga | Sobrescritura | |
|---|---|---|
| Dónde ocurre | En la misma clase | Entre superclase y subclase |
| La firma | Cambia | Es idéntica |
| Cuándo se decide | En compilación | En ejecución |
| Tipo de ligadura | Estática | Dinámica |
| Con qué se relaciona | Comodidad de la interfaz | Polimorfismo |
public class Buscador {
// Sobrecarga: mismo nombre, firmas distintas.
public List<Pregunta> buscar(Tema tema) { /* ... */ }
public List<Pregunta> buscar(Tema tema, int nivel) { /* ... */ }
public List<Pregunta> buscar(String texto) { /* ... */ }
}
public class Test {
public int duracionEnMinutos() { return 60; }
}
public class Simulacro extends Test {
@Override
public int duracionEnMinutos() { return 90; } // sobrescritura
}En Java, cambiar solo el tipo devuelto no crea una sobrecarga: dos métodos con el mismo nombre y los mismos parámetros que se diferencien únicamente en lo que devuelven no compilan. Lo que distingue una sobrecarga es la lista de parámetros.
Para el examen
Sobrecarga: misma clase, firmas distintas; se resuelve en compilación
Sobrescritura (override): subclase con firma idéntica; se resuelve en ejecución
Ligadura: estática en la sobrecarga, dinámica en la sobrescritura