====== 12 - POO V: Clases y Métodos abstractos. Interfaces ======
===== Métodos abstractos =====
Vamos a seguir con nuestro ejemplo de los vehículos con herencia. Tenemos la clase //Vehiculo//, a la que vamos a añadir una nueva propiedad: //matricula//.
public class Vehiculo {
private String marca, modelo;
private int velocidad;
private float[] presionRuedas;
private String matricula;
...
Supongamos que el proceso de matriculación es diferente para los distintos vehículos (las motos de menos de 250cc no pagan impuesto de matriculación, por ejemplo), pero queremos que todos los vehículos implementen ese método obligatoriamente. Si dejamos las clases como están, es el programador el que tiene que acordarse de implementar ese método. Podría darse el caso de crear un vehículo sin la opción de matricular.
Existe una forma de obligar a todas las clases descendientes a implementar un método: los **métodos abstractos**. Un método abstracto es aquel que tiene solo la definición de la cabecera, pero no su implementación. Por ejemplo, podemos crear el método abstracto //matricular()// en la clase //Vehiculo//:
public class Vehiculo {
private String marca, modelo;
private int velocidad;
private float[] presionRuedas;
private String matricula;
...
public abstract void matricular(String matricula);
Fíjate que el método //matricular// no tiene implementación. Tan solo hemos definido su cabecera (es un método público que no devuelve nada y recibe un parámetro de tipo String). Además, para indicar que es un método abstracto debemos usar la palabra **abstract**.
Lo primero que pasa al crear un método abstracto es que Java nos da un error. ¿Por qué? Simplemente, porque si una clase tiene uno o más métodos abstractos, debemos declarar esa clase también como abstracta. Esto indica que no podemos crear objetos de dicha clase, cosa lógica, ya que no tiene implementado el método recién creado (//matricular//). Para declarar la clase como abstracta, lo único que tenemos que hacer es añadir **abstract** a la definición de la clase:
public abstract class Vehiculo {
private String marca, modelo;
private int velocidad;
private float[] presionRuedas;
private String matricula;
...
public abstract void matricular(String matricula);
...
Vamos a aprovechar y a crear otro método que nos será útil más adelante. Será de tipo //protected// para que lo puedan usar solo las clases descendientes (aunque acuérdate que en Java los método protegidos también lo pueden usar las clases definidas en el mismo paquete, con lo que para hacerlo bien deberíamos cambiar el //package// de la clase //Vehiculo// y sus descendientes. Por simplificar, vamos a dejarlo como está) y será el setter del atributo //matricula//:
public abstract class Vehiculo {
private String marca, modelo;
private int velocidad;
private float[] presionRuedas;
private String matricula;
...
public abstract void matricular(String matricula);
protected void setMatricula(String matricula){
this.matricula = matricula;
}
...
El método //setMatricula()// lo necesitaremos más adelante, ya que el atributo //matricula// es privado y no se puede acceder a él desde las clases descendientes. Además, lo hacemos //protected// para que no se pueda cambiar la matrícula de un vehículo desde fuera de las clases descendientes de //Vehiculo//.
Al definir la clase como abstracta, vemos que nos desaparece el error anterior, pero ahora nos muestra errores en las clases //Coche// y //Moto//. Esos errores nos están indicando que ambas clases tienen que implementar **obligatoriamente** los métodos abstractos de sus clases antecesoras. Si creamos el método en la clase //Coche// vemos como desaparece el error:
public class Coche extends Vehiculo{
private boolean ruedaRepuesto;
...
@Override
public void matricular(String matricula) {
this.setMatricula(matricula);
System.out.println("Pagar impuesto de matriculación");
}
Lo único que hace el método es mostrar la frase "Pagar impuesto de matriculación". Para implementar el método en la clase //Moto//, primero debemos crear el atributo //cilindrada// y pasarlo al constructor. El método //matricular()// comprobará la cilindrada de la moto y mostrará por pantalla si se debe pagar matrícula o no:
public class Moto extends Vehiculo{
private int cilindrada;
public Moto(String marca, String modelo, int velocidad, float[] presionRuedas, int cilindrada) {
super(marca, modelo, velocidad, presionRuedas);
this.cilindrada = cilindrada;
}
@Override
public void matricular(String matricula) {
if(this.cilindrada<250) {
System.out.println("No hace falta pagar impuesto matriculación");
} else {
System.out.println("Pagar impuesto matriculación");
}
}
}
Vamos a crear un coche y un par de motos en nuestra clase principal para comprobar que todo funciona:
public class Main {
public static void main(String[] args) {
Coche coche1 = new Coche(
"Kia",
"Niro",
0,
new float[] {2.4f, 2.4f, 2.4f, 2.4f},
true
);
Moto moto1 = new Moto(
"Honda",
"NC 750",
0,
new float[] {2.5f, 2.5f},
750
);
Moto moto2 = new Moto(
"Yamaha",
"XMAX",
0,
new float[] {2.2f, 2.2f},
125
);
coche1.matricular("1111AAA");
moto1.matricular("2222BBB");
moto2.matricular("3333CCCC");
}
}
Pagar impuesto de matriculación
Pagar impuesto matriculación
No hace falta pagar impuesto matriculación
==== Propiedades de los métodos abstractos ====
* Podemos declarar clases abstractas sin necesidad de tener algún método abstracto. Aunque no es lo habitual, puede ser útil si queremos definir métodos comunes de las clases descendientes pero no queremos que se puedan crear objetos de dicha clase.
* Un método abstracto solo se puede definir como //protected// o //public//. Esto es lógico, ya que si es //private// no puede ser accesible desde las clases descendientes y no podrían implementarlo.
* No podemos reducir la visibilidad de un método abstracto en las clases hijas. Es decir, si en la clase padre hemos declarado el método abstracto como //public//, en las clases hijas que implementen ese método no podemos definirlas con una accesibilidad más restrictiva (//private// o //protected//). Sin embargo, si declaramos el método abstracto como //protected// si que podemos declararlo en las clases hijas como //public//.
===== Interfaces =====
Podríamos definir las **interfaces** como clases donde todos sus métodos son abstractos (no todos, como veremos a continuación). En realidad, tienen algunas características más:
* Las //interfaces// pueden ser públicas o sin modificador de acceso (accesible desde el paquete donde está definida).
* Una //interface// pueden definir métodos abstractos o métodos estáticos y ambos serán públicos (aunque no se indique).
* Podemos definir atributos en la //interface//, pero tienen que ser públicos, estáticos y finales (constantes), con lo que debemos darle un valor. No hace falta añadirles los modificadores (//public//, //static// y //final//), pero sí darles un valor.
* Una clase puede heredar (implementar) de una o varias //interfaces//.
Para declarar una //interface// en Java, debemos usar la palabra reservada **interface**:
public interface nombreInterface {
...
}
Si queremos que una clase //implemente// una //interface//, debemos usar la palabra reservada **implements**, en lugar de //extends// como en la herencia:
public class miClase implements nombreInterface {
...
}
==== Utilidad de las interfaces ====
Aunque en un principio pueda parecer que las //interfaces// son poco útiles, son uno de los mecanismos más importantes para crear una buena arquitectura de nuestra aplicación.
Por ejemplo, supongamos que estamos haciendo un programa que permite leer documentos //pdf// donde tenemos almacenados nuestros clientes. Para eso, creamos una clase //Cliente// y una clase //DocumentoPdf// un método para encontrar un cliente en el //pdf// con el //id// que le pasemos:
public class Cliente {
int id;
String nombre, CIF;
...
}
public class DocumentoPdf {
String path;
...
public Cliente getClienteFromPdf(int id){
Cliente cliente = new Cliente();
...
return cliente;
}
...
}
En nuestra clase principal, usamos esas clases para buscar el cliente con //id// = 2:
public class Main {
public static void main(String[] args) {
DocumentoPdf documento = new DocumentoPdf();
Cliente cliente = documento.getClienteFromPdf(2);
}
}
La aplicación funciona como toca y nos busca clientes en los //pdf// sin ningún problema. Al cabo del tiempo, sustituimos los documentos //pdf// por //json//. Tenemos que crear una nueva clase (//DocumentoJson//) con el método //getClienteFromJson()//:
public class DocumentoJson {
String path;
public Cliente getClienteFromJson(int id){
Cliente cliente = new Cliente();
...
return cliente;
}
}
Además, tenemos que sustituir la llamada al método desde todas las clases donde lo hemos utilizado (en nuestro caso, solo está en nuestra clase //Main//):
public class Main {
public static void main(String[] args) {
DocumentoJson documento = new DocumentoJson();
Cliente cliente = documento.getClienteFromJson(2);
}
}
Imagínate si tuviésemos varias referencias al método. Nos tocaría buscar donde lo hemos utilizado y sustituirlos. Si después cambiamos a otro tipo de documentos o base de datos o cualquier otro tipo de persistencia, nos tocaría volver a hacer lo mismo, con el costo que conlleva.
Vamos a hacer lo mismo, pero usando //interfaces//. Para eso, creamos una //interface// //Documento// con el método //getCliente()//:
public interface Documento {
public Cliente getCliente(int id);
}
En principio (igual que antes) leemos los clientes de documentos //pdf//. Para eso, creamos la clase //DocumentoPdf// que implementará la //interface// //Documento// (y su método //getCliente()//:
public class DocumentoPdf implements Documento{
String path;
public Cliente getCliente(int id){
Cliente cliente = new Cliente();
//...
return cliente;
}
}
En nuestra clase principal, usamos esas clases y métodos:
public class Main {
public static void main(String[] args) {
Documento documento = new DocumentoPdf();
Cliente cliente = documento.getCliente(2);
}
}
Date cuenta que estamos usando //polimorfismo// para definir el tipo de la variable //documento//. Lo definimos del tipo //Documento// (//interface//), no como una clase concreta.
Si cambiamos y queremos usar documentos Json, creamos la nueva clase implementando de nuevo el //interface//:
public class DocumentoJson implements Documento{
String path;
public Cliente getCliente(int id){
Cliente cliente = new Cliente();
//...
return cliente;
}
}
Ahora, lo único que tenemos que cambiar es el tipo de documento en nuestra clase principal, pero el método no hace falta cambiarlo:
public class Main {
public static void main(String[] args) {
Documento documento = new DocumentoJson();
Cliente cliente = documento.getCliente(2);
}
}
Con este enfoque, desacoplamos las clases entre sí, permitiendo hacer cambios en unas sin que afecten a las otras.
En nuestro ejemplo, seguimos teniendo que cambiar cosas en la clase principal (el tipo de documento). Existen mecanismos para reducir todavía más el acoplamiento entre clases, sin tener que definir el tipo de documento donde queremos buscar. Uno de estos mecanismos es el de **inyección de dependencias**.
Nuestro ejemplo es muy sencillo, pero imagínate una aplicación grande con miles de líneas de código. Como hemos dicho, el uso de los //interfaces// es básico para desacoplar las diferentes capas de una aplicación entre sí. Aunque hasta segundo no veremos las diferentes arquitecturas (en capas, hexagonal...), deberías ir acostumbrándote a usar //interfaces// siempre que puedas, ya que te pueden solucionar muchos problemas en un futuro.
===== Ejercicios =====
** Ejercicio 1 **
Crea una estructura de paquetes similar a la del tema anterior en el //back//:
/controller //Capa de presentación de nuestra aplicación (controladores)
BookController //Controlador de libros
/business //Capa de negocio de nuestra aplicación (entidades y servicios)
/entity //Entidades de nuestra aplicación
Book //Modelo de negocio //Book//
/service //Servicios de nuestra aplicación
<> //Interfaz del caso de uso "Listar todos los libros"
/impl //Implementaciones de nuestros servicios
BooksServiceImpl //Implementación de la interface //BooksService//
/persistence //Capa de persistencia de nuestra aplicación (repositorios)
<> //Interfaz de acceso a datos de los libros
/impl //Implementaciones de nuestros repositorios
StaticBookRepositoryImpl //Implementación estática de la //interface// //BookRespository//
App //Clase principal de nuestra aplicación
** Ejercicio 2 **
Crea la clase //Book// (será nuestro modelo de negocio) con los atributos //id//, //title// y //author//. La clase tendrá el constructor donde le pasaremos las propiedades del objeto, los //getters// correspondientes y el método //toString()// para mostrar el libro por pantalla.
** Ejercicio 3 **
Crea un método en </BookRespository//>> (ten en cuenta que es una interface, con lo que no tendrás que implementarlo) llamado //all()// que devuelva una lista de libros (clase //Book//). Haz que la clase //StaticBookRepositoryImpl// implemente la interface </BookRepository//>> y devuelva un listado de libros que estarán escritos en el código. La clase podría quedar más o menos así:
public class StaticBookRepositoryImpl implements BookRepository{
List books = List.of(
new Book(1, "El nombre de la rosa", "Umberto Eco"),
new Book(2, "La insoportable levedad del ser", "Milan Kundera"),
new Book(3, "1Q84", "Haruki Murakami")
);
public List all() {
return this.books;
}
}
** Ejercicio 4 **
Crea la interface </BookService//>> con un sólo método llamado //getAll()//, que devolverá un listado de libros y su implementación en la clase //BooksServiceImpl//, que creará un objeto de tipo **BookRepository** (fíjate que el tipo del objeto es la interface </BookRepository//>>) y devolverá lo retornado por el método //all()//. La clase //BooksServiceImpl// tendrá una propiedad privada que instanciará un objeto de tipo //BookRepository// con la implementación correspondiente (//StaticBookRepositoryImpl//).
public class BooksServiceImpl implements BookService{
private BookRepository repository = new StaticBookRepositoryImpl();
public List getAll() {
return this.repository.all();
}
}
** Ejercicio 5 **
Crea en la clase //BookController// el método //getAll()//. Este método tendrá una propiedad de tipo //BookService// y devolverá lo retornado por su método //getAll()//:
public class BookController {
private BookService service = new BooksServiceImpl();
public List getAll() {
return this.service.getAll();
}
}
** Ejercicio 6 **
Por último, crea en la clase principal (//App//) el método //main()//, donde se creará el controlador //BookController// y se mostrará por pantalla el listado de libros:
public class App {
private static BookController controller = new BookController();
public static void main(String[] args) {
List books = App.controller.getAll();
System.out.println(books);
}
}
** Ejercicio 7 **
Crea un //enum// con 3 libros. Tendrá las propiedades //id//, //title// y //author//, con su constructor correspondiente.
Crea además una clase //EnumBookRepositoryImpl// que implementará la interface //BookRepository// y recogerá los datos del //enum// recién creado. Cambia en el servicio el //Repository// que utilizamos para leer los datos de los libros y comprueba que funciona todo correctamente.