Programación Orientada a Objetos: Clases y Objetos

16 minuto(s) de lectura

Este Post es la primera parte de la serie “Programación Orientada a Objetos”. Esta serie tiene como objetivo establecer los fundamentos de la POO, utilizando Java como ejemplo. Los temas de esta serie son los siguientes:

  1. Definición de POO
  2. Clases y Objetos
  3. Herencia
  4. Abstracción
  5. Encapsulamiento
  6. Polimorfismo
  7. Composición
  8. Principios utilizados en la POO

Todo el código de la serie está disponible en GitHub.

El código correspondiente a esta primera parte lo pueden encontrar aquí.

Todos los ejemplos están hechos en Netbeans.

Introducción

La programación cada día está tomando más importancia en el mundo, ya que se utiliza para prácticamente cualquier cosa que se puedan imaginar. Desde aplicaciones móviles como Facebook, Twitter y Tinder, pasando por plataformas educativas como Coursera o Urmynd, hasta para controlar y llevar naves espaciales como la New Horizons hasta los límites del Sistema Solar.

A lo largo de los años, se han desarrollado diferentes técnicas de programación, cada una con sus respectivas ventajas y desventajas. Una de las téncicas de programación más utilizadas y que los mayores lenguajes de programación utilizan es la Programación Orientada a Objetos.

La Programación Orientada a Objetos (POO) es un paradigma (técnica o estilo) de programación el cual se basa en el concepto de objetos. Un objeto puede representar cualquier elemento del sistema o programa. Cada objeto contiene datos o información (en forma de atributos o variables) y código (en forma de métodos o funciones). Los diferentes objetos que conforman el programa se pueden comunicar entre sí mediante un intercambio de mensajes, el cual se lleva a cabo utilizando métodos que el objeto posee y expone a otros objetos.

Utilizar la POO en el diseño y desarrollo de sistemas de programación tiene diversas ventajas. Se puede tener un sistema bien diseñado, en el cual cada objeto tenga una función específica, haciendo que el desarrollo, detección de errores y expansión del sistema resulten más sencillos. Así mismo, la POO es la base para utilizar Patrones de Diseño, con el cual el código se hace más eficiente, mantenible y fácil para agregar nuevas funcionalidades en el futuro. También al utilizar la POO se puede llevar a cabo la reutilización de código, centralizando el código y evitando repetir código que se utiliza en varios objetos.

Existen varios lenguajes de programación que están diseñados para utilizar la Programación Orientada a Objetos. Algunos de estos lenguajes son los siguientes: Java, C#, Ruby, Python, PHP, Perl. Si bien algunos lenguajes han soportado POO desde el principio, otros como PHP han ido agregando soporte para POO a lo largo de los años.

La POO consiste de los siguientes conceptos:

  • Abstracción
  • Herencia
  • Polimorfismo
  • Encapsulamiento

También cuenta con los siguientes principios:

  • DRY - No te repitas
  • Separación de intereses
  • Principio abierto-cerrado
  • Composición sobre herencia

De estos conceptos y principios hablaremos en este post.

I. Clases y Objetos

Imaginemos que queremos desarrollar un sistema de streaming de música parecido (pero mucho mejor!) que Spotify. Este sistema va a contar con canciones, artistas, álbumes, listas de reproducción y muchas cosas más. Usando la POO podemos traducir estos elementos del sistema a objetos: vamos a crear objetos de tipo canción, artista, álbum y lista de reproducción. (Se pueden crear objetos más abstractos, también como Mensaje, TareaAsíncrona, Banner).

En este momento sabemos cuáles van a ser nuestros objetos, pero hace falta una manera de crearlos. Las Clases son la manera de crear estos objetos. Cada Clase es la plantilla con las instrucciones para cada objeto y contiene el comportamiento, los datos y las operaciones que el objeto puede realizar.

En su forma más simple una clase se define de la siguiente manera

public class Usuario {

}

Con esta definición de clase podemos construir objetos de tipo “Usuario”.

Para crear un objeto del tipo Usuario se utiliza la palabra reservada new, y se asigna a una variable del tipo Usuario:

Usuario miObjeto = new Usuario();

Al hacer esto, se dice que se crea una instancia de la clase Usuario. Esto quiere decir que el objeto “miObjeto” se comportará como un Usuario y tendrá los datos que caracterizan a un Usuario, pero con los valores propios de “miObjeto”.

Estructura de una Clase

Una clase vacía no tiene ninguna utilidad, por lo que se necesitan agregar contenedores de datos, que se llaman variables, con las cuales se pueden realizar operaciones y darle comportamiento al objeto, esto se hace utilizando métodos.

Para demostrar cómo se estructura una clase, vamos a utilizar nuestra clase Usuario.

public class Usuario {

}

Variables de instancia

Dentro de una clase se crean contenedores de datos, llamados variables, que será dónde el objeto pueda almacenar sus datos. Existen diferentes tipos de variables dependiendo en dónde se puedan utilizar, pero su estructura básica es la siguiente

tipoDeAcceso tipoDeDato nombreDeLaVariable;
private String nombre;

Al crear un objeto (cuando se dice que es una instancia de una clase), este objeto contiene una copia de las variables las cuales tendrán datos únicos a esa instancia. Al crear OTRA instancia diferente de la misma clase, ese objeto tendrá OTRA copia de esas variables. Estas variables son llamadas variables de instancia.

Generalmente se declaran estas variables de instancia al inicio de la clase. Para nuestra clase Usuario sería de la siguiente manera:

private String nombre;
private String apellido;
private int edad;
private String email;
private int estatura;

Variables de clase

Las variables de clase pertenecen a la clase en sí, no a la instancia de cada objeto, por lo que al crear diferentes instancias de la misma clase, TODOS compartirán la misma variable.

Para crear las variables de clase se utiliza la palabra reservada static. Suelen colocarse al inicio de la clase, antes de las variables de instancia. Para nuestra clase Usuario, podríamos crear una variable de clase que nos cuente la cantidad de usuarios que se han creado:

public static int numeroDeUsuariosCreados = 0;

Métodos

Los métodos son el conjunto de instrucciones dentro de una clase que hacen una labor específica sobre los datos que posee el objeto o la clase. La sintaxis para crear métodos es la siguiente:

tipoDeAcceso tipoDeDatoDeRetorno nombreDelMétodo(TipoDeDato nombreParámetro...) {

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


public String getNombre() {
     return nombre;
}

Los métodos pueden regresar algún valor de cierto tipo de dato, o pueden regresar void que es cuando no regresan ningún tipo de dato. Dependiendo del tipo de acceso definido en el método, éste puede ser visible desde otros objetos, y por lo tanto puede ser llamado por estos objetos, o solamente puede ser visible dentro de la instancia del objeto.

Con el siguiente código dentro del método main podemos crear un objeto, asignarle un valor a su variable “nombre” y luego imprimir en pantalla el nombre de ese usuario:

public static void main(String[] args) {
  Usuario usuario = new Usuario();
  usuario.setNombre("WarriorMinds");

  System.out.println("Nombre de usuario: " + usuario.getNombre());
}

Esto es lo que observamos en la consola, usando NetBeans:

run:
Nombre de usuario: WarriorMinds
BUILD SUCCESSFUL (total time: 2 seconds)

Al igual que las variables, existen métodos de instancia y métodos de clase.

Métodos de instancia

Estos métodos son los métodos que tendrá cada instancia (o copia) del objeto. Podrán modificar valores de variables de instancia como de variables de clase. Así mismo, las variables que se creen dentro del método únicamente serán visibles dentro del método.

Agreguemos un método de instancia para agregar el nombre del usuario y otro para obtener su valor.

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

public String getNombre() {
  return nombre;
}

Cada instancia de la clase Usuario puede asignarse un valor diferente a su variable nombre y obtener ese valor. Lo mismo se puede hacer con las demás variables de instancia.

public void setApellido(String apellido) {
  this.apellido = apellido;
}

public String getApellido() {
  return apellido;
}

public void setEdad(int edad) {
  this.edad = edad;
}

public int getEdad() {
  return edad;
}

public void setEmail(String email) {
  this.email = email;
}

public String getEmail() {
  return email;
}

Se pueden crear otros métodos que solamente se utilicen en operaciones internas del objeto, siendo éstos también métodos de instancia.

Métodos de clase

Los métodos de clase pertenecen a la clase en sí, no al objeto en particular. Para crear estos métodos se utiliza la palabra reservada static. Hagamos un método para aumentar el valor de nuestra variable estática para saber cuantos usuarios se han creado, y otro para obtener este valor.

public static void usuarioCreado() {
  numeroDeUsuariosCreados++;
}

public static int cuantosUsuariosCreados() {
     return numeroDeUsuariosCreados;
}

Es importante recordar que solamente se pueden modificar variables estáticas dentro de métodos estáticos. ¿Cuál es la razón de esto?

Las variables de instancia se crean cada vez que se crea la nueva instancia de la clase, utilizando la palabra reservada new. Mientras tanto, las variables de clase están creadas aún sin crear una instancia de la clase. Por lo tanto, no se puede modificar una variable de instancia dentro de un método de clase porque puede que no esté creada esa variable de instancia aún y además, solamente las variables de clase son comunes a la clase.

Es por esto que desde fuera de nuestra clase, los métodos de clase se pueden llamar de la siguiente manera:

Usuario.usuarioCreado();

No es necesario crear una instancia de Usuario para llamar este método de clase.

Envío de mensajes entre objetos

Los objetos que conforman nuestro sistema tienen que comunicarse de alguna manera para interactuar entre sí. Esto se logra mediante un envío de mensajes entre los objetos. Para realizar este envío de mensajes entre objetos se utilizan los métodos disponibles de cada objeto. Los mensajes pueden ser valores primitivos (como int, String, double, boolean) o pueden ser otros objetos. Estos valores se conocen como parámetros.

Para el envío de mensajes entre objetos, hay que tener en cuenta que existen dos maneras en que los datos son enviados. Hay que conocer ambas maneras para ver qué pasará con el dato que enviemos al método.

Pasando parámetros de tipos de datos primitivos.

Una variable a fin de cuentas es un espacio en memoria reservado en donde se almacenará un valor. Cuando enviamos como parámetros a un método cualquier tipo de dato primitivo (int, double, float, boolean, etc), se dice que el parámetro se pasa por valor.

Esto quiere decir que la variable original se copia y esta nueva copia del valor es la que se pasa al método. Por lo tanto, si el valor de la variable recibida en el método se modifica, la variable original no es modificada.

Veamos un ejemplo de esto con la clase Usuario y una variable edad con un valor predefinido.

int edad = 25;
Usuario otroUsuario = new Usuario();
otroUsuario.setEdad(edad);

El valor de la variable definida en nuestro método, y el valor de la variable edad dentro del objeto otroUsuario tienen un valor de 25.

Al modificar el valor de la variable edad, definida en nuestro método:

edad = 50;

Y al momento de obtener el valor de esa variable, y de la variable dentro del objeto Usuario, vemos que solamente cambió el valor de la variable local:

System.out.println("Valor de edad del objeto otroUsuario: " + otroUsuario.getEdad());
System.out.println("Valor de edad de la variable edad: "  + edad);

Obtenemos esta salida:

Valor de edad del objeto otroUsuario: 25
Valor de edad de la variable edad: 50

Pasando parámetros de datos por referencia

Cuando un parámetro en un método es un objeto, se dice que se pasa por referencia, ya que se pasa la referencia en donde está guardado este objeto en la memoria. Este paso de la referencia se hace por valor, por lo cual si se modifica el objeto dentro del método, el objeto original NO es modificado.

Veamos este caso con un ejemplo.

El siguiente método recibe un objeto usuario como parámetro, se le asigna un valor a su nombre. Después, se crea un nuevo objeto Usuario y se asigna al mismo parámetro usuario. Después se modifica la variable de nombre al nuevo usuario creado.

private static void modificarUsuario(Usuario usuario, String nombre) {
  usuario.setNombre(nombre);
  usuario = new Usuario();
  usuario.setNombre("Otro nombre");
}

Mandamos llamar este método de la siguiente manera.

Usuario usuario = new Usuario();
modificarUsuario(usuario, "Mi nombre es Warrior Minds.");
System.out.println(usuario.getNombre());

Obtenemos la siguiente respuesta en la consola:

Mi nombre es Warrior Minds.

Esto pasa ya que el parámetro Usuario fue pasado por valor y al momento de crear el nuevo usuario dentro del método “modificarUsuario” el parámetro original no se modificó.

Constructores

Hemos visto cómo crear un objeto, de la siguiente manera:

Usuario usuario = new Usuario();

Lo que esta instrucción hace, es llamar un método especial llamado constructor. Los constructores son métodos que se utilizan para la creación de objetos.

La sintaxis para crear métodos constructores es la siguiente:

public NombreDeLaClase(listaDeParámetros) {

}

Cada clase puede tener diferentes constructores, cada uno con una lista de parámetros diferente. Si no definimos un constructor en nuestro código, de todas formas existe uno el cual no tiene ningún parámetro. Es por esto que hemos podido crear objetos Usuario hasta ahora.

Vamos a crear dos constructores, uno sin parámetros y otro con parámetros, y ambos aumentarán el valor de nuestra variable de clase definida previamente:

public Usuario() {
  Usuario.usuarioCreado();
}

public Usuario(String email, String nombre, String apellido, int edad) {
  Usuario.usuarioCreado();

  this.email = email;
  this.nombre = nombre;
  this.apellido = apellido;
  this.edad = edad;
}

Ahora crearemos dos usuarios, uno con cada constructor y veremos el resultado de nuestra variable de clase.

Usuario usuario1 = new Usuario();
Usuario usuario2 = new Usuario("warrior.software.minds@gmail.com",  "Warrior", "Minds", 25);
System.out.println("Usuario 1: " + usuario1.getNombre());
System.out.println("Usuario 2: " + usuario2.getNombre());
System.out.println("Usuarios creados: " + Usuario.cuantosUsuariosCreados());

En este código se crean dos usuarios, con dos constructores diferentes. A usuario1 no se le asigna ningún valor, por lo que al imprimir su nombre con getNombre() obtenemos el valor null. A usuario2 se le asignan todos sus valores en el constructor, por lo que al imprimir su nombre con getNombre() obtenemos “Warrior”.

¿Cuántos usuarios se han creado hasta ahora? Si ejecutan el código como está hecho en GitHub, verán que el resultado es 6. ¿Porqué sale este valor?

Clases embebidas

Como hemos visto hasta ahora, las clases que definamos son la base para nuestros programas. Hemos visto cómo crear clases, cada clase siendo un archivo completo. Pero también se pueden crear clases dentro de otras clases. Estas clases se llaman clases embebidas y pueden ser de dos tipos: clases internas o clases estáticas.

Una ventaja de las clases embebidas es que se puede agrupar cierta lógica dentro de un objeto separado, pero que solamente se utiliza en una clase. Además, esto ayuda al momento de leer y mantener el código ya que el código de la clase embebida se encuentra cerca del lugar en donde es utilizado.

Clases Internas

Una clase interna es una clase que está definida dentro de otra clase. Esto puede ocurrir cuando tu clase necesita algún objeto que solamente esa clase va a utilizar. La clase interna puede utilizar y modificar variables y métodos de la clase padre, y sólamente la clase padre puede tener acceso a esta clase.

Veamos un ejemplo de clase interna. Crearemos una clase llamada ClasesInternas, que será un tipo de usuario, con el nombre de ClasesInternas para identificar lo que se muestra.

Dentro de esta clase, crearemos una clase Direccion, en la cual almacenaremos la dirección del usuario. Esta clase será privada, por lo que solamente se podrá usar en la clase padre ClasesInternas.

public class ClasesInternas {
  private String nombre;
  private String email;
  private int edad;
  private Direccion direccion;

  public ClasesInternas(String nombre, String email, int edad, String calle, String colonia, String codigoPostal) {
    this.nombre = nombre;
    this.email = email;
    this.edad = edad;
    this.direccion = new Direccion(calle, colonia, codigoPostal); // Se puede tener acceso a la clase Dirección desde aqui.
  }

  public String mostrarInfo() {
    return "Nombre: " + nombre + ", email: " + email + ", edad: " + edad + "\n" + direccion.mostrarInfo();
  }

  private class Direccion {
    private String calle;
    private String colonia;
    private String codigoPostal;

  public Direccion(String calle, String colonia, String codigoPostal) {
    this.calle = calle;
    this.colonia = colonia;
    this.codigoPostal = codigoPostal;
  }

  public String mostrarInfo() {
    return nombre + " vive en " + calle + " " + colonia + " " + codigoPostal;
  }
}

En este ejemplo, la clase Direccion es la clase interna. La declaramos como privada para que solamente la clase padre ClasesInternas pueda tener acceso a ella. Dentro de la clase Direccion en el método mostrarInfo() podemos ver que se hace uso de la variable “nombre”, la cual está definida en la clase padre.

Si quisiéramos utilizar la clase Direccion desde nuestro método main (o desde cualquier otro lugar) nos marcaría error, ya que la clase Direccion solamente es visible para la clase ClasesInternas.

Clases Estáticas

Una clase estática también se crea dentro de una clase padre, pero, al ser estática, no tiene acceso a las variables y métodos no estáticos de la clase padre. Esta clase se puede utilizar en cualquier lugar, solamente debe de utilizarse la siguiente notación: NombreClasePadre.NombreClaseHija. Veamos un ejemplo.

Crearemos una clase similar a la ClasesInternas. Solamente que en esta ocasión, la clase Dirección será una clase estática.

public class ClasesEstaticas {
  private String nombre;
  private String email;
  private int edad;
  private Direccion direccion;


  public ClasesEstaticas(String nombre, String email, int edad, Direccion direccion) {
    this.nombre = nombre;
    this.email = email;
    this.edad = edad;
    this.direccion = direccion;
  }

  public String mostrarInfo() {
    return "Nombre: " + nombre + ", email: " + email + ", edad: " + edad + "\n" + direccion.mostrarInfo();
  }

  public static class Direccion {
    private String calle;
    private String colonia;
    private String codigoPostal;

    public Direccion(String calle, String colonia, String codigoPostal) {
      this.calle = calle;
      this.colonia = colonia;
      this.codigoPostal = codigoPostal;
    }

  public String mostrarInfo() {
    return "Dirección: " + calle + " " + colonia + " " + codigoPostal;
  }
}

Para crear una clase estática, se necesita la siguiente sintaxis:

public static class NombreDeLaClase

En esta ocasión, NO se puede tener acceso a las variables nombre o email o edad dentro de la clase Direccion.

Para hacer uso de la clase estática Direccion desde nuestro método main, es de la siguiente manera:

ClasesEstaticas.Direccion direccion = new ClasesEstaticas.Direccion("Calle Central", "Colonia Nueva", "123456");
ClasesEstaticas usuario = new ClasesEstaticas("WarriorMinds", "warrior.software.minds@gmail.com", 25, direccion);

Se puede ver una clase estática como una clase independiente, solamente definida dentro de otra clase.

this

Se puede utilizar la palabra reservada this dentro de cualquier método o constructor dentro de la clase para tener acceso a la instancia actual del objeto.

Por ejemplo, hay veces que un constructor puede hacer uso de algún otro constructor para inicializar algunas variables. En este caso se puede utilizar this(….) con la lista de parámetros del constructor deseado.

this("sin nombre");

De igual manera, hay veces que el parámetro de algún método tiene el mismo nombre que alguna variable de instancia de nuestro objeto. Es lógico y correcto que ambas tengan el mismo nombre, ya que representan lo mismo. Pero necesitamos alguna manera de identificar la variable de instancia. En este escenario también se hace uso de la palabra this.

Si tenemos una variable de instancia llamada nombre: private String nombre;

Y el siguiente método

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

La variable nombre, hace referencia al parámetro que recibe el método, mientras que “this.nombre” hace referencia a la variable de instancia nombre.

Conclusión

Las clases y los objetos son la base de la Programación Orientada a Objetos. Una vez conociendo cómo crear clases y objetos, tipos de variables, como utilizar los métodos y sus parámetros, el concepto de clases/variables estáticas, clases internas y de cuándo utilizar “this”, podemos pasar a conceptos, e ideas que se utilizan en la POO.

Etiquetas:

Categorías:

Actualizado:

Deja un comentario