Programación Avanzada

Programación Orientada a Objetos

Herencia

Introducción

Muchas veces nos encontramos conque, para resolver un problema, disponemos de clases que nos ofrecen una funcionalidad muy cercana a la que necesitamos, pero carecen de cierta otra funcionalidad que nos interesa.

Una posible solución es escribir una nueva clase desde cero con toda la funcionalidad de la primera más la nueva funcionalidad que necesitamos.

Otra posibilidad es añadir a la clase existente sólo la nueva funcionalidad, tomando provecho de todo el trabajo ya existente.

Esta sencilla, pero potente idea, es la base de la Herencia, una de las piedras angulares de la POO.

Bibliografía

Contenidos

  1. Ejemplo introductorio.
  2. Extensión de una clase.
  3. Compatibilidad de referencias.
  4. Clases abstractas.
  5. Polimorfismo con herencia.
  6. De nuevo la vinculación dinámica.
  7. El principio de substitución de Liskov.
  8. Clases que no se pueden extender.
  9. Extensión de interface.
  10. Paquetes y modificadores de acceso.
  11. Principios SOLID en el diseño de clases.
  12. Resumen.

Ejemplo introductorio

Supongamos que tenemos la siguiente definición de un punto en dos dimensiones.

public class Punto2D {
    private double x;
    private double y;
    public Punto2D() {
        x = y = 0;
    }
    public Punto2D(float x, float y) {
        this.x = x;
        this.y = y;
    }
    public double getX() {
        return x;
    }
    public double getY() {
        return y;
    }
}

Ejemplo introductorio

Ahora necesitamos un punto en tres dimensiones.

Nos podemos plantear reescribir la clase desde cero:

public class Punto3D {
    private double x;
    private double y;
    private double z;
    ...
}

O bien podemos intentar aprovechar la clase Punto2D y añadir sobre ella la nueva funcionalidad.

En este segundo caso estamos haciendo uso de la herencia.

Extensión de una clase

En Java decimos que una clase extiende a otra cuando añade funcionalidad sobre una clase ya definida:

public class Punto3D extends Punto2D {
    private double z;

    public double getZ() {
        return z;
    }
    ...
}

En este caso concreto, decimos que Punto2D es la clase madre o clase base y que Punto3D es la clase hija.

Fíjate que Java nos proporciona la palabra reservada extends para usar el mecanismo de herencia.

Extensión de una clase

Podemos expresar la relación entre las clases Punto2D y Punto3D utilizando el siguiente diagrama UML:

Extensión de una clase

Lo primero que debes saber es que en Java sólo existe la Herencia simple, una clase no puede extender a más de una clase madre.

¿Cuantos métodos tiene la clase hija?

Los que en ella se definen más los que son accesibles en la clase madre.

¿Qué métodos de la clase madre son accesibles desde la clase hija?

Los public y protected de la madre, y si madre e hija están definidas en el mismo paquete también los del paquete.

Los miembros private sólo son accesibles en la clase que los define.

Ya veremos más sobre los modificadores de acceso.

Extensión de una clase

Sobrescritura de métodos

Supon que en la clase Punto2D tienes definido el siguiente método.

public String getDescripcion() {
    return x + ", " + y;
}

Que devuelve una cadena con información sobre el Punto2D.

Cuando tenemos puntos en 3D, este método no nos sirve, ya que debemos tener en cuenta también la coordenada z.

Extensión de una clase

Sobrescritura de métodos

En la clase Punto3D debemos definir el método del siguiente modo:

@Override
public String getDescripcion() {
    return getX() + ", " + getY() + ", " + z;
}

Detalles:

  • Fíjate que como los atributos x e y son private en la clase madre, no podemos acceder a ellos directamente, tenemos que utilizar los getters.
  • Utilizamos la anotación @Override para asegurarnos que estamos sobrescribiendo correctamente el método.

Extensión de una clase

Sobrescritura de métodos

Juguemos un poco con estas clases:

Punto2D p2d = new Punto2D(1, 2);
System.out.println(p2d.getDescripcion());
// Muestra 1.0, 2.0
Punto3D p3d = new Punto3D(1, 2, 3);
System.out.println(p3d.getDescripcion());
// Muestra 1.0, 2.0, 3.0

El mismo método, definido con distinta implementación en una clase madre y su hija.

Extensión de una clase

Sobrescritura de métodos

Añadamos a la clase Punto2D un nuevo método:

public Punto2D invierte() {
    return new Punto2D(-x, -y);
}

Y ahora sobrescribamos este método en la clase Punto3D

@Override
public Punto3D invierte() {
    return new Punto3D(-getX(), -getY(), -z);
}

Fíjate que hemos podido cambiar el tipo de retorno al sobrescribir el método. Esto es posible siempre que ampliemos el tipo de retorno.

Extensión de una clase

Sobrescritura de métodos

¿Y que pasa con los atributos? ¿Los podemos sobrescribir?

La respuesta es si podemos... pero mejor no lo hagas.

public class Punto3D extends Punto2D {
    private double x;
    private double y;
    private double z;
    ...
}

El código anterior no contiene ningún error. Pero, repito no lo hagas.

Extensión de una clase

La palabra reservada super

Revisemos de nuevo el método getDescripcion() en las clases Punto2D y Punto3D:

// En la clase Punto2D
public String getDescripcion() {
    return x + ", " + y;
}
// En la clase Punto3D
@Override
public String getDescripcion() {
    return getX() + ", " + getY() + ", " + z;
}

La herencia me promete que puedo aprovechar al máximo los métodos ya definidos en la clase madre. Debe haber un mejor modo de escribir este código.

Extensión de una clase

La palabra reservada super

Que una clase hija sobrescriba un método de la clase madre no quiere decir que borre el método de la clase madre, simplemente lo oculta.

Desde una clase hija podemos acceder a los métodos de la clase madre a través de la palabra reservada super.

// En la clase Punto2D
public String getDescripcion() {
    return x + ", " + y;
}
// En la clase Punto3D
@Override
public String getDescripcion() {
    return super.getDescripcion() + ", " + z;
}

Extensión de una clase

La palabra reservada super

La palabra reservada super es una referencia a la clase madre de igual modo que this es una referencia a la propia clase.

Hay veces en las que me interesa reescribir un método por completo en la clase hija.

Pero hay otras veces que lo que nos interesa es aprovechar la implementación que ya nos da la clase madre.

Extensión de una clase

Uso de super en los constructores

Los constructores los utilizamos para definir los valores iniciales de los atributos.

¿Cómo los podemos utilizar desde las clases hijas? También a través de super

// En la clase Punto2D
public Punto2D(double x, double y) {
    this.x = x;
    this.y = y;
}
// En la clase Punto3D
public Punto3D(double x, double y, double z) {
    super(x, y);
    this.z = z;
}

Extensión de una clase

El constructor sin argumentos

Cuando se crea un objeto de una clase hija, se debe incluir la funcionalidad de la clase madre.

public Punto2D() {
    System.out.println("Constructor sin argumentos Punto2D");
}
...
public Punto3D() {
    //debe iniciar la funcionalidad de la clase madre, ¿cómo?
    System.out.println("Constructor sin argumentos clase Punto3D");
}
...
// En algún lugar de nuestro código
Punto3D p = new Punto3D();
// Muestra: Constructor sin argumentos Punto2D
//          Constructor sin argumentos clase Punto3D

Extensión de una clase

El constructor sin argumentos

Aunque no escribamos explícitamente, cuando se crea un objeto de la clase Punto3D se llama al constructor sin argumentos de la clase madre Punto2D.

Un detalle importante es que, si cuando definimos una clase no definimos ningún construtor, Java definirá por nosotros un constructor sin argumentos por defecto (al que se llama constructor por defecto). Pero, si definimos un constructor con argumentos, Java no definirá el constructor por defecto.

Este mecanismo es un modo de asegurar que podemos crear instancias de una clase aunque no definamos ningún constructor en la clase.

Extensión de una clase

El constructor sin argumentos

Peligros de olvidar qué significa el construtor sin argumentos.

Observa este sencillo ejemplo:

public class Madre {
    private int valor;

    public Madre(int valor) {
        this.valor = valor;
    }
}

public class Hija extends Madre {
}

Tan poco código y esconde un error.

Extensión de una clase

El constructor sin argumentos

Buena práctica: Define siempre el constructor sin argumentos en tus clases.

Buena práctica: Si desde un constructor de una clase hija no tienes otro constructor mejor al que llamar, llama al menos al constructor sin argumentos de la clase madre.

Extensión de una clase

Yo no extiendo a nadie

public class Punto2D {
    // Aquí la definición de la clase
}

La clase Punto2D ¿a quien extiende? ¿tiene alguna madre?

Sí, ninguna clase en Java es huérfana. Si explícitamente no se extiende a ninguna clase, implícitamente se extiende a la clase Object, que es la raíz del árbol de jerarquía en Java.

Extensión de una clase

Yo no extiendo a nadie

La clase Object tiene métodos ya definidos:

boolean equals(Object o); // Compara si dos objetos son iguales.
int hashCode(); // Devuelve un código hash del objeto
String toString(); // Devuelve una representación del objeto

El último método es el que llama la familia de métodos print[ln](...) para obtener la cadena que se enviará a consola.

public class Punto2D {...
    @Override
    public String toString() {
        return x + ", " + y;
    }...
}

Extensión de una clase

Yo no extiendo a nadie

Podemos mejorar la clase Punto3D

public class Punto3D extends Punto2D {...
    @Override
    public String toString() {
        return super.toString() + ", " + z;
    }...
}

Finalmente podemos hacer:

Punto2D p2d = new Punto2D(1, 2);
System.out.println(p2d);
// Mostrará 1.0, 2.0
Punto3D p3d = new Punto3D(1, 2, 3);
System.out.println(p3d);
// Mostrará 1.0, 2.0, 3.0

Extensión de una clase

Métodos que no se pueden extender

Existen ocasiones donde no queremos que una clase hija sobrescriba un método de su clase madre.

Piensa por ejemplo en un método que recibe un valor real entre cero y diez y devuelve una nota como: APROBADO(5-7), NOTABLE(7-9), etcétera.

Queremos prohibir que una hija cambien los rangos y haga APROBADO(4-6). La manera de hacerlo es utilizar la palabra reservada final en definición del método:

public class MetodoFinal {
    public final void metodoFinal() {
        // Definición del método
    }
}

Extensión de una clase

Métodos que no se pueden extender

Si intentamos crear un clase hija que sobrescriba el método final:

public class HijaMetodoFinal extends MetodoFinal {
    @Override
    public void metodoFinal() {
        // Nueva definición del método
    }
}

Obtendremos el error: Cannot override the final method from MetodoFinal.

Extensión de una clase

Métodos estáticos

¿Funcionan igual los métodos estáticos?

La respuesta es no. Recuerda que un método estático pertenece a la clase.

Los métodos estáticos no se pueden sobreescribir, sólo ocultar.

public class Madre {
    public static void estatico() {
        System.out.println("Estático en el madre.");
    }
}

Extensión de una clase

Métodos estáticos

Esta clase Hija intenta sobreescribir el método usando la anotación Override.

public class Hija extends Madre {
    //@Override // Esto genera un error
    public static void estatico() {
        System.out.println("Estático en la Hija.");
    }
}

El resultado es un error en tiempo de compilacion.

Description Resource Path Location Type The method estatico() of type Hija must override or implement a supertype method Hija.java /Herencia/src/herencia line 7 Java Problem

Extensión de una clase

Métodos estáticos

¿Cuál será el resultado de la ejecución de este fragmento de código?

Hija.estatico();
Madre.estatico();

Estático en la Hija.

Estático en el madre.

Extensión de una clase

Métodos estáticos

¿Y el resultado de este otro ejemplo?

Hija hija = new Hija();
hija.estatico(); // Aquí tenemos un warning.
Madre madre = hija;
madre.estatico(); // Aquí tenemos otro warning.

Estático en la Hija.

Estático en el madre.

¿Pero el objeto no es de tipo Hija? Sí, pero en el caso de los métodos static lo que manda es el tipo de la referencia.

De hecho el warning significa que no debes acceder a los métodos static de una clase utilizando un referencia, sino el nombre de una clase.

Compatibilidad de referencias

Hasta ahora hemos sido muy conservadores con el tipo de las referencias. Si creábamos un objeto de tipo Punto3D se lo asignábamos a una referencia de este mismo tipo. Si creábamos un objeto de tipo Punto2D se lo asignábamos a una referencia de este tipo.

¿Podemos asignar a una referencia de tipo Punto2D un objeto de tipo Punto3D?

Sí: A una referencia de un tipo le podemos asignar un objeto de cualquiera de sus clases hijas.

¿Y al revés, a una referencia de tipo hijo le puedo asignar un objeto de clase madre?

No: A una referencia de tipo hijo nunca le podemos asignar un objeto de su clase madre.

Compatibilidad de referencias

Recuerda que las referencias son la puerta de entrada a los objetos, y no podemos tener puertas de entrada más amplias que la funcionalidad que nos brindan los objetos.

Si pudiese hacer los siguiente:

Punto3D punto = new Punto2D(1, 2); // Sin coordenada Z

Después también podríamos hacer:

punto.getZ();

Cosa que no tiene sentido porque el Punto2D que hemos creado no tiene un atributo Z.

Clases abstractas

Hasta ahora, al definir una clase hemos dado la implementación de todos sus métodos.

Por otro lado, hemos visto las interface que nos permiten declarar métodos sin definirlos, justo al contrario que las clases.

¿Existe algún camino intermedio? ¿Puedo definir una clase implementando sólo unos métodos y otros no?

La respuesta es Sí, veámoslo con un ejemplo.

Clases abstractas

Supón que desarrollas una aplicación para un veterinario, el veterinario atiende a perros, gatos, pájaros, etcétera.

A todos estos animales sus amos les han puesto un nombre, y cada uno de ellos hace un sonido diferente, así que podíamos empezar a codificarlos como:

public class Perro {
    private String nombre;
    public Perro(String nombre) {
        this.nombre = nombre;
    }
    public String getNombre() {
        return nombre;
    }
    public String hazSonido() {
        return "Soy un perro y hago: Guau";
    }
}

Clases abstractas

Esta es la implementación para el Gato:

public class Gato {
    private String nombre;
    public Gato(String nombre) {
        this.nombre = nombre;
    }
    public String getNombre() {
        return nombre;
    }
    public String hazSonido() {
        return "Soy un gato y hago: Miau";
    }
}

Clases abstractas

Y finalmente esta es la implementación para el Pájaro:

public class Pajaro {
    private String nombre;
    public Pajaro(String nombre) {
        this.nombre = nombre;
    }
    public String getNombre() {
        return nombre;
    }
    public String hazSonido() {
        return "Soy un pájaro y hago: Pío-pío";
    }
}

Clases abstractas

Jueguemos a ser veterinarios:

Perro perro = new Perro("Bobby");
System.out.println(perro.hazSonido());
Gato gato = new Gato("Gargamel");
System.out.println(gato.hazSonido());
Pajaro pajaro = new Pajaro("Twitee");
System.out.println(pajaro.hazSonido());

La salida que obtenemos es:

Soy un perro y hago: Guau
Soy un gato y hago: Miau
Soy un pájaro y hago: Pío-pío

Clases abstractas

Buff!!!, cuanto código repetido!!!

Todos los animales tienen un nombre como un atributo y un método que devuelve el valor de ese atributo.

Vaya, pero cada uno de ellos tiene una implementación distinta par el sonido que hacen.

Recuerda la técnica: Extrae hacia arriba el comportamiento común, deja en las clases sólo el comportamiento específico.

Clases abstractas

Todos los animales tienen el mismo atributo y el mismo método para devolver su valor, podemos llevar los dos a una clase madre Animal.

public class Animal {
    private String nombre;
    public Animal(String nombre) {
        this.nombre = nombre;
    }
    public String getNombre() {
        return nombre;
    }
}

Clases abstractas

Por ejemplo la clase Perro quedaría como:

public class Perro extends Animal {
    public Perro(String nombre) {
        super(nombre);
    }
    public String hazSonido() {
        return "Soy un perro y hago: Guau";
    }
}

Clases abstractas

Vale, pero esto es lo que ya sabíamos, hemos utilizado la herencia de nuevo, ¿cómo podemos indicar que todos los animales deben tener un método que devuelva el sonido que hacen, y que cada animal lo implemente como quiera?

public abstract class Animal {
    private String nombre;

    public Animal(String nombre) {
        this.nombre = nombre;
    }

    public String getNombre() {
        return nombre;
    }

    public abstract String hazSonido();
}

Clases abstractas

Ahora cada una de las clases hija está obligada a implementar el método abstract, de lo contrario obtendríamos un error.

public class Perro extends Animal {
    public Perro(String nombre) {
        super(nombre);
    }

    @Override
    public String hazSonido() {
        return "Soy un perro y hago: Guau";
    }
}

Clases abstractas

Animal animal = new Perro("Bobby");
System.out.println(animal.hazSonido());
animal = new Gato("Gargamel");
System.out.println(animal.hazSonido());
animal = new Pajaro("Twitee");
System.out.println(animal.hazSonido());
Soy un perro y hago: Guau
Soy un gato y hago: Miau
Soy un pájaro y hago: Pio-pio

Fantástico!!!, la referencia Animal animal es compatible con cualquier clase hija.

Extraordinario!!!, la vinculación dinámica sabe a qué metodo llamar aunque siempre escribamos lo mismo System.out.println(animal.hazSonido()).

Clases abstractas

Me alegro que te guste porque hay que pagar un precio.

Las clases abstractas no se pueden instanciar.

No podemos crear un objeto de una clase abstracta.

Bueno, no es mucho precio, al fin y al cabo ¿quien metería un Animal en casa sin saber de que animal se trata?

Clases abstractas

Tratemos de combinar clases abstractas con interfaces. Definamos primero la interfaz:

public interface HazSonido {
    public String hazSonido();
}
            

Hemos definido en ella el método que era abstracto en la clase. Recuerda que todos los métodos de una interfaz son public abstract

Clases abstractas

Ahora hagamos que la clase implemente la interfaz, pero sin llegar a definir el método hazSonido().

public abstract class Animal implements HazSonido {
    private String nombre;

    public Animal(String nombre) {
        super();
        this.nombre = nombre;
    }
    public String getNombre() {
        return nombre;
    }
}

Fíjate que la clase debe seguir siendo abstract porque el método no está definido en esta clase abstracta.

Clases abstractas

La implementación de las clases concretas es la misma que antes, pero ahora también podemos utilizar con ellas una referencia de tipo interfaz:

HazSonido animalRuidoso = new Perro("Bobby");
System.out.println(animalRuidoso.hazSonido());
                

Polimorfismo con herencia

El polimorfismo con Herencia significa lo mismo que con interfaces un mismo método puede tener implementaciones distintas en su clase madre y alguna de sus clases hijas.

Además, al utilizar Herencia el polimorfismo significa que donde se puede utilizar una clases madre también se puede utilizar una clase hija.

Si un método tiene un argumento de tipo una determinada clase, ese método también lo podemos invocar con instancias de una de sus clases hijas.

De nuevo la vinculación dinámica

Como ya hemos visto, el tipo de la referencia no es suficiente para que la máquina virtual de Java encuentre el método que se debe invocar.

En tiempo de ejecución, y basándose en el tipo del objeto al que se accede a través de una referencia, la máquina virtual de Java encuentra el método correcto al que debe llamar.

Punto2D p2d = new Punto2D(1, 2);
System.out.println(p2d);
// Muestra 1.0, 2.0
p2d = new Punto3D(1, 2, 3);
System.out.println(p2d);
// Muestra 1.0, 2.0, 3.0

El principio de substitución de Liskov

El principio de substitución de Liskov nos «obliga» a diseñar nuestras clases de modo que el comportamiento de nuestro software no se modifique, si invocamos un método que admite referencias de una clase madre, o si lo invocamos con referencias de alguna de sus clases hijas.

Clases que no se pueden extender

Existen ocasiones en que no nos interesa que nuestras clases se puedan extender.

Piensa, por ejemplo, en una clase cuyo cometido sea recibir el usuario y clave de una persona para validarlo contra una base de datos.

Si pudiésemos extender esta clase, y gracias al principio de substitución de Liskov, podríamos inyectar una hija de esta clase en el sistema y leer los datos de acceso.

En el paquete estándar de Java tienes muchas clases que no se pueden extender como String, System, Math y todas las clases recubridoras como Integer, Float, etc.

Clases que no se pueden extender

Para prohibir que una clase se pueda extender, simplemente añadimos a su definición la palabra reservada final, como en el siguiente ejemplo:

public final class SinHijos {
    // Aquí la definición de la clase
}

Si intentamos extender esta clase:

public class Hija extends SinHijos {

}

obtendremos el error: The type Hija cannot subclass the final class SinHijos

Extensión de interfaces

Los interface son tipos de datos y como tales se pueden extender. Recordemos el interface Pagador del capítulo anterior:

public interface Pagador {
    String getNombre();
    String getDireccion();
    String getNif();
}

Podemos extenderlo para incluir un método que me devuelva los ingresos brutos del Pagador.

public interface PagadorIngresosBrutos extends Pagador {
    float getIngresosBrutos();
}

Extensión de interfaces

Cualquier clase que implemente el interface PagadorIngresosBrutos debe implementar los cuatro métodos anteriores.

public class PersonaIngresosBrutos implements PagadorIngresosBrutos {
    @Override
    public String getNombre() {....
    }
    @Override
    public String getDireccion() {....
    }
    @Override
    public String getNif() {....
    }
    @Override
    public float getIngresosBrutos() {....
    }
}

Extensión de interfaces

Esto te va a sorprender: los interface pueden extender a más de un interface madre.

Fíjate que no hay problema, puesto que los interface sólo declaran métodos pero no los definen.

public interface EstudianteYPagador extends Pagador, Estudiante {
    // Aquí la definición de sus propios métodos
}

¿Qué ocurre si tenemos métodos con la misma signatura en los interface madre?

Nada, porque están declarados simplemente. Es como si finalmente se fusionasen en uno solo.

Recomendación: si te llegase a ocurrir, revisa el nombre de tus métodos, y donde están declarados.

Paquetes y modificadores de acceso

Hasta este momento, siempre hemos definido nuestras clases dentro de algún paquete. Está desaconsejado que las clases no estén definidas en algún paquete.

Los paquetes son espacios de nombres para la máquina virtual de Java, igual que el espacio de nombres en Internet

Además, los paquetes crean una jerarquía de subdirectorios que puedes comprobar en la vista Navigator de Eclipse.

Paquetes y modificadores de acceso

En Java contamos con cuatro modificadores de acceso: private, «no escribir modificador», protected y public.

Los modificadores de acceso restringen la visibilidad o el ámbito de los miembros: atributos y métodos, de una clase.

Cuidado!!, no escribir modificador de acceso tiene un significado bien definido en Java: modificador de acceso de ámbito el package.

El package debería ser el lugar, por defecto, donde creemos nuestras clases.

Paquetes y modificadores de acceso

Dos clases dentro del mismo paquete:

Paquetes y modificadores de acceso

Dos clases en diferentes paquetes pero con relación madre/hija:

Paquetes y modificadores de acceso

Dos clases en paquetes distintos:

Paquetes y modificadores de acceso

La siguiente tabla muestra los cuatro modificadores de acceso y su significado.

privateprotectedpublic
La claseSISISISI
El paqueteNOSISISI
Hijas (otro paquete)NONOSISI
Otras clasesNONONOSI

El ámbito natural de los miembros de una clase, atributos y métodos, es el paquete.

Principios SOLID en el diseño de clases

Los principios SOLID fueron introducidos por Robert C. Martin y guían el diseño de software orientado a objetos.

Single Responsability.

Open Close.

Liskov Substitution.

Interface Segregation.

Dependency Inversion.

Principios SOLID en el diseño de clases

Single Responsability Principle

Una clase debe tener un solo motivo para cambiar.

public class Estudiante {
    private String nombre;
    private String calle;
    private int numero;
    private String puerta;
    private float notas[];

    public String getDirecion() {
        return calle + "," + numero + "," + puerta;
    }

    public float califica() {
        // Algoritmo para calcular la nota final
    }
}

Principios SOLID en el diseño de clases

Single Responsability Principle

La clase Estudiante puede cambiar por varios motivos:

  • Si añadimos más información a la dirección, por ejemplo, el código postal.
  • Si cambiamos el modo de calcular la nota final a partir de las notas individuales.

Una solución posible es introducir nuevas clases con una responsabilidad bien delimitada:

  • Una clase que abstraiga la idea de dirección postal.
  • Una clase que almacene las notas y sepa dar una calificación final.

Principios SOLID en el diseño de clases

Single Responsability Principle

public class EstudianteSolid {
    private String nombre;
    private Direccion direccion;
    private Calificador calificador;

    public float calificacion() {
        return calificador.califica();
    }
    public String getDireccion() {
        return direccion.getDireccion();
    }
}

Los métodos calificacion() y getDireccion() se llaman métodos delegados, ya que «delegan» su trabajo en otros métodos, los de las clases Direccion y Calificador.

Principios SOLID en el diseño de clases

Single Responsability Principle

Principios SOLID en el diseño de clases

Open Close Principle

Una clase debe estar cerrada para la modificación pero abierta para la extensión.

Este principio quizás sea el más difícil de explicar, así que te dejo un vídeo donde se intenta aclarar la idea.

Vídeo con ejemplo.

Principios SOLID en el diseño de clases

Open Close Principle

Principios SOLID en el diseño de clases

Liskov Substitution Principle

Las clases hijas pueden ocupar el lugar de las clases madre sin que el comportamiento del software se vea modificado.

Este principio lo que nos dice es que cuando una clase extiende a otra debe ampliar el comportamiento de la clase que extiende, pero no modificarlo.

public class ClaseConNombre {
    private String nombre;
    public ClaseConNombre(String nombre) {
        this.nombre = nombre;
    }
    public String getNombre() {
        return nombre;
    }
}

Principios SOLID en el diseño de clases

Liskov Substitution Principle

Esta sería una clase hija que no cumple el principio:

public class ClaseConNombreYApellidos extends ClaseConNombre {
    private String apellidos;

    public ClaseConNombreYApellidos(String nombre, String apellidos) {
        super(nombre);
        this.apellidos = apellidos;
    }
    public String getNombre() {
        return "";
    }
}

getNombre() no devuelve nada, por lo tanto no podríamos comparar este nombre con otro.

Principios SOLID en el diseño de clases

Liskov Substitution Principle

Y esta clase sí que cumple el principio:

public class ClaseConNombreYApellidos extends ClaseConNombre {
    private String apellidos;

    public ClaseConNombreYApellidos(String nombre, String apellidos) {
        super(nombre);
        this.apellidos = apellidos;
    }
    public String getNombre() {
        return super.getNombre() + " " + apellidos;
    }
}

Principios SOLID en el diseño de clases

Liskov Substitution Principle

Principios SOLID en el diseño de clases

Interface Segregation Principle

El interfaz de una clase debe estar orientado a un único cliente.

Dicho de otro modo quizás se entienda mejor. Si una clase necesita definir un método que está declarado en un interface y este interface declara a su vez otros métodos que la clase no necesita... la clase no debería implementar el interface.

Principios SOLID en el diseño de clases

Interface Segregation Principle

Recuerda el interface Pagador

public interface Pagador {
    String getNombre();
    String getDireccion();
    String getNif();
}

Y una de las últimas clases que hemos visto:

public class ClaseConNombre {
    private String nombre;
    ....
    public String getNombre() {
        return nombre;
    }
}

Principios SOLID en el diseño de clases

Interface Segregation Principle

Pues bien, sólo porque ClaseConNombre tiene el método getNombre() a nadie se le ocurriría hacer:

public class ClaseConNombre implements Pagador{
    private String nombre;
    ....
    @Override
    public String getNombre() {
        return nombre;
    }
    @Override
    public String getDireccion() {
        return "";
    }
    ...
}

Principios SOLID en el diseño de clases

Interface Segregation Principle

A veces, se intenta soslayar este problema con el uso de clases adaptadoras, clases que implementan los métodos de un interface dejándolos todos vacíos.

La ventaja es que una clase que sólo necesita algunos de los métodos del interface, en vez de implementar el interface extiende a la clase adaptadora.

Lo veremos muy claro en el capítulo de creación de interfaces gráficas de usuario.

Principios SOLID en el diseño de clases

Interface Segregation Principle

Principios SOLID en el diseño de clases

Dependency Inversion Principle

No hagas tu código dependiente de la implementación, únicamente del interfaz.

Una clase no se debe basar en la definición de los métodos de otra para definir los propios.

Una clase debe ser completamente ignorante del código que implementa los métodos de otra clase que ella usa.

La clase Estudiante hace uso de la clase Calificador quien implementa un método para calcular la nota media; la implementación de ese cálculo no debe ser algo en lo que se base la implementación de la clase Estudiante.

Principios SOLID en el diseño de clases

Dependency Inversion Principle

Resumen

En este capítulo hemos visto extensamente los mecanismo que nos proporciona Java para utilizar la herencia.

  • Extensión de clases.
  • Sobreescritura de métodos.
  • Clases abstractas.

Has conocido nuevos modificadores:

  • final Impide que un método sea sobreescrito o una clase extendida.
  • static El miembro así modificado pertenece a la clase, no a sus instancias.

Resumen

También has conocido los modificadores de acceso y cómo restringen la visibilidad de los miembros de una clase.

Además hemos visto los principios SOLID que guían la escritura de buen código orientado a objetos.

Dominar estas técnicas es una tarea laboriosa donde nunca se acaba de aprender. No desesperes, el objetivo es recorrer el camino.