lunes, 27 de abril de 2020

Reutilización de código aplicando herencia en POO

En java podemos implementar herencia simple de la siguiente manera para optimizar la reutilización de código:

Diagrama UML

Supongamos que tenemos 3 clases que comparten tanto atributos como métodos, si construimos la clase para cada uno de ellos tendríamos código repetido.



Podemos representar lo mismo abstrayendo una clase base que concentre los atributos y métodos que se repetirán en las demás clases derivadas.


Codificación

Clase base:
package dto;

public class Personas {
 protected Integer id;
 protected String nombre;
 protected String apellidos;
 protected String correo;
 protected String telefono;

 public Personas() {
 }

 public Personas(Integer id, String nombre, 
   String apellidos, String correo, String telefono) {
  this.id = id;
  this.nombre = nombre;
  this.apellidos = apellidos;
  this.correo = correo;
  this.telefono = telefono;
 } 
 
 public Integer getId() {
  return id;
 }

 public void setId(Integer id) {
  this.id = id;
 }

 public String getNombre() {
  return nombre;
 }

 public void setNombre(String nombre) {
  this.nombre = nombre;
 }

 public String getApellidos() {
  return apellidos;
 }

 public void setApellidos(String apellidos) {
  this.apellidos = apellidos;
 }

 public String getCorreo() {
  return correo;
 }

 public void setCorreo(String correo) {
  this.correo = correo;
 }

 public String getTelefono() {
  return telefono;
 }

 public void setTelefono(String telefono) {
  this.telefono = telefono;
 }

 public void logIn() {

 }

 public void logOut() {

 }
}

Clases derivadas:
package dto;

public class Clientes extends Personas{

 private String pais;
 
 public Clientes() {
  super();
 }

 public Clientes(Integer id, 
   String nombre, 
   String apellidos, 
   String correo, 
   String telefono, 
   String pais) {
  super(id, nombre, apellidos, correo, telefono);
  this.pais = pais;
 }

 public String getPais() {
  return pais;
 }

 public void setPais(String pais) {
  this.pais = pais;
 }
 
 public void comprar() {
  
 }

}








Creando una clase en Java

En el paradigma de la programación orientada a objetos (POO) existen diversos principios como la abstracción, polimorfismo, persistencia, etc. que requieren de objetos para llevar a cabo sus cometidos. Estos objetos deben ser creados a partir de una plantilla que son las clases. Por lo tanto, para crear un objeto es necesario haber definido una clase.

  • Para ver información sobre la programación orientada a objetos has clic aquí.
  • Para ver información sobre la definición y conceptos de clases y objetos has clic aquí.

Vamos a definir una situación donde debemos crear un programa para una veterinaria, en la cuál se atenderá múltiples animales, entre ellos a los perros y nos centraremos en este tipo de animales (obviaremos conceptos de herencia por ahora).

Definimos que cada perro debe tener un identificador único que podemos llamar "idPerros" el DNI de su propietario, el nombre del perro y un campo con toda su descripción (ahí podemos colocar color, raza, tamaño, peso, etc). Por motivos de simplificación de código vamos a juntar todas esas características en un solo atributo. Además, debemos definir el nombre de la clase que puede ser "Perro" o "Perros".

Teniendo así el siguiente código:
package dto; //Ubicación en el proyecto

/* Nombre de la clase*/
public class Perros {   

   /* Atributos */
   private Integer idPerro;
   private String dniPropietario;
   private String nombre;
   private String descripcion; 

   /* Método constructor */
   public Perros() {
   }

   /* Métodos Setter y Getter */
   public Integer getIdPerro() {
      return idPerro;
   }
   public void setIdPerro(Integer idPerro) {
      this. idPerro = idPerro;
   }
   public String getDniPropietario() {
      return dniPropietario;
   }
   public void setDniPropietario(String dniPropietario) {
      this. dniPropietario = dniPropietario;
   }
   public String getNombre() {
      return nombres;
   }
   public void setNombre(String nombre) {
      this.nombre = nombre;
   }
   public String getDescripcion() {
      return descripcion;
   }
   public void setDescripcion(String descripcion) {
      this. descripcion = descripcion;
   }
}

Se aprecia que tenemos un método constructor sin parámetros, por lo que al crear un objeto (instanciar) este tendrá su contenido vacío y mediante los métodos "set" le asignaremos valores a sus atributos para luego llamarlos mediante los métodos "get". Aquí observamos, además, el principio de encapsulamiento donde tenemos niveles de acceso "privados" y "públicos".

Herencia

La herencia es una de las características de la POO (programación orientada a objetos), pero no es exclusiva de los lenguajes de programación orientados a objetos. Consiste en que una clase recibe los atributos y métodos de otra clase en este sentido se tiene:
  • Clase base, clase padre o superclase que "hereda" sus atributos y métodos a otra clase.
  • Clase derivada, clase hija o subclase que "hereda" sus atributos y métodos de otra clase.

Los objetivos de emplear la herencia en la programación es la reutilización de código (clic aquí para ver un ejemplo de reutilización de código en un caso práctico), especialización de clases (principio abierto/cerrado) y aprovechar las ventajas el polimorfismo (característica más importante en los lenguajes OO), de esta manera se aumenta la productividad en la etapa de desarrollo produciendo programas con alta fidelidad y eficiencia. Además, se reduce el esfuerzo necesario para el mantenimiento del programa luego de su implementación.

Herencia simple

La herencia simple es aceptada por Java y se produce cuando una o más clases heredan métodos y atributos de otra única clase. Es decir existe una sola clase base y una o más clases derivadas. La palabra reservada en el lenguaje java para indicar herencia es "extends".

Consideraciones

  • En el momento de programar, nos daremos con la sorpresa que los constructores no se heredan y para hacer uso de ellos se debe emplear el método "super" en el constructor de la clase derivada.
  • Si aplicamos herencia, es una buena práctica hacer un llamado al constructor de la clase base (método "super")
  • La clase derivada hereda todos los métodos de la clase base, pero no puede acceder directamente a los atributos, pues tienen un nivel acceso "privado".
  • Para que los métodos de una superclase sean accesibles por una subclase se deben modificar sus niveles de acceso a "protegido" o "público".
  • Se pueden refinar los métodos en las clases hijas si el método tiene el mismo nombre en la clase hija y la clase padre.
  • Se puede llamar dentro de un método de la clase derivada el de la clase base mediante "super"
  • El modificados "final" en la clase base tiene diversos comportamientos según donde sea aplicado
    • En una variable, la convierte en una constante
      • Ejemplo: protected final String NOMBRE = "Jhon";
    • En un método, evita que pueda ser redefinido en la clase derivada.
    • En la clase (Ejemplo: public final class Autor {/* ... */}) evita que la clase pueda ser una clase padre, sería como la última clase hija en la jerarquía de clases.

Ejemplo

Supongamos una clase "Vehiculos" y una clase "Automoviles", donde "Automoviles" va heredar de la clase "Vehiculos":

Código de "Vehiculos"
package dto;

public class Vehiculos {

 private String nombre;
 private Double costo;
 private String marca;

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

 public String getNombre() {
  return nombre;
 }

 public void setNombre(String nombre) {
  this.nombre = nombre;
 }

 public Double getCosto() {
  return costo;
 }

 public void setCosto(Double costo) {
  this.costo = costo;
 }

 public String getMarca() {
  return marca;
 }

 public void setMarca(String marca) {
  this.marca = marca;
 } 
}

Código de "Automoviles"
package dto;

public class Automoviles extends Vehiculos {

 private String modelo;
 private Float cilindrada;

 public Automoviles() {
    super("Automovil");
 }

 public String getModelo() {
    return modelo;
 }

 public void setModelo(String modelo) {
    this.modelo = modelo;
 }

 public Float getCilindrada() {
    return cilindrada;
 }

 public void setCilindrada(Float cilindrada) {
    this.cilindrada = cilindrada;
 }

 @Override
 public String toString() {
    return String.format(
     "Tipo de vehículo: %s\nMarca: %s\nModelo: %s\nCilindrada: %.2f\nCosto: $%.2f",
     getNombre(),getMarca(), modelo, cilindrada, getCosto());
 }
}

Ejecución
package test;

import dto.Automoviles;

public class Ejecutor {

 public static void main(String[] args) {
  Automoviles a = new Automoviles();
  a.setModelo("Elantra");
  a.setMarca("Hyundai");
  a.setCilindrada(16.4f);
  a.setCosto(25000d);
  System.out.println(a.toString());
 }
}

Estructura de los archivos:

Salida:

Tipo de vehículo: Automovil
Marca: Hyundai
Modelo: Elantra
Cilindrada: 16.40
Costo: $25000.00


Herencia múltiple

La herencia múltiple se da cuando una clase hija puede tener más de una clase padre, esto no es soportado de manera nativa por Java debido al problema del diamante y consideraciones que los desarrolladores del lenguaje, pues causaba más problemas que soluciones su uso. Por ello, para poder aplicar una herencia múltiple se considera dos tipos: a) herencia múltiple de implementación de clases (usando extends) y b) la herencia múltiple con implementación de tipos (empleando interfaces). La primera no es soportada por Java, pero la segunda sí mediante el uso de interfaces y la palabra reservada "implements", así ingresamos a la vez al mundo del polimorfismo (otra de las características de la POO y principal ventaja de los lenguajes de este tipo frente a otros por su facilidad de implementar).

Consideremos el siguiente caso donde la clase "Automoviles" debe heredar tanto de "Vehiculos" como de la clase "Activos". Podemos considerar crear una interfaz llamada "Activos" donde definimos los atributos que serían simplemente constantes y los métodos simplemente declarados, pero no implementados.