Saltar al contenido

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.

ClaseObjeto
Qué esUna definición, un moldeUn ejemplar concreto
Cuántos hayUna por tipo de entidadTantos como haga falta
Cuándo existeSe escribe al programarSe crea al ejecutar
Qué contieneAtributos y métodos declaradosValores 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.

java
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.

VisibilidadQuién puede accederSímbolo en UML
PrivadaSolo la propia clasesigno menos
De paqueteLas clases del mismo paquetevirgulilla
ProtegidaLa propia clase, sus subclases y el paquetealmohadilla
PúblicaCualquierasigno 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 abstractaInterfaz
Relación que expresa«Es un», parentesco«Sabe hacer», capacidad
Atributos de estadoNo, solo constantes
ConstructorSí, lo usan las subclasesNo
Cuántas se pueden tenerUna sola superclaseTodas las que hagan falta
Código heredadoMétodos ya implementadosSolo la firma, salvo métodos por defecto
java
// 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.

java
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.

SobrecargaSobrescritura
Dónde ocurreEn la misma claseEntre superclase y subclase
La firmaCambiaEs idéntica
Cuándo se decideEn compilaciónEn ejecución
Tipo de ligaduraEstáticaDinámica
Con qué se relacionaComodidad de la interfazPolimorfismo
java
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