Fundamentos de la programación orientada a objetos


Introducción

La programación orientada a objetos lleva años siendo el referente dentro del desarrollo de software y sigue siendo hoy el paradigma más versátil y completo para una gran variedad de aplicaciones pequeñas o grandes.

Su manera de abstraer los elementos importantes de un problema y agrupar, tanto datos como funcionalidades de una forma natural mediante el uso del concepto de objetos, facilita considerablemente el diseño e implementación de una solución a la medida de las necesidades propias de un sistema de software.

El paradigma de programación orientada a objetos busca emular los elementos del entorno donde el software será implantado, de manera que permite modelar los datos que el propio sistema usa y adaptarlos a un formato informático.


Explicación

Definición de clase 

Una clase se podría considerar como una plantilla en la cual se plasman todas las características relevantes de un elemento que forma parte del sistema, por ejemplo, si en nuestro sistema vamos a incluir a una persona, podemos identificar en ella varias características tales como color de ojos, color de cabello, nombre, edad, domicilio, etc. De la misma manera podemos describir varios de sus comportamientos como lo son el hablar, pensar, correr, entre otros. 

Figura 1. Representación de una clase Empleado.

class Empleado {
  public String nombre;
  public int edad;
  public float salario;
}

Tabla 1. Ejemplo de representación de una clase en código Java.

Definición de objeto e instancia 

Los objetos son representaciones de las clases previamente descritas. A partir de una clase podemos tener múltiples objetos donde cada uno tenga valores distintivos para las características descritas, por ejemplo, al tener una clase llamada Empleado. cuyas características son nombre, edad y salario, se puede tener un objeto cuyo valor para el nombre sea el de Roberto, otro objeto que tenga el valor de Cecilia y así, sucesivamente. 

Figura 2. Representación de dos objetos/instancias de la clase Estudiante.

Empleado empleadoRoberto = new Empleado();
empleadoRoberto.nombre = "Roberto";
empleadoRoberto.edad = 18;

Estudiante empleadaCecilia = new Empleado();
empleadaCecilia.nombre = "Cecilia";
empleadaCecilia.edad = 20;

Tabla 2. Ejemplo de creación de objetos e inicialización de atributos.

Definición de atributos 

Los atributos son las propiedades o características importantes de una clase y estas se representan por medio de variables dentro de la clase.
Al ser variables, se siguen las mismas convenciones para asignarles un nombre, como lo son la nomenclatura de camello, o que no pueden ser ninguna palabra reservada del lenguaje Java.

Definición de comportamientos 

Los comportamientos describen todas las acciones que una clase puede realizar y estas se representan en Java mediante métodos.

Todos los métodos que se escriben dentro de una clase deben tener un sentido semántico dentro de esta. Por ejemplo, si describieras una clase empleado, el método “volar” no tendría sentido en este contexto, pues no es una acción que una persona pueda realizar, aunque si pudieras definir el método “volar” en la clase avión, y a su vez especificar que una persona usa un avión.

Concepto de sobrecarga (overloading

La sobrecarga de métodos (overloading, por su nombre en inglés) ocurre cuando dos o más métodos tienen el mismo nombre, pero tienen una lista de parámetros diferente (Boyarsky y Selikoff, 2020). A estas alturas, probablemente ya hayas estado en contacto con métodos sobrecargados. El método System.out.println() es un muy buen ejemplo.

Como puedes ver en la siguiente tabla, la única diferencia entre las diferentes versiones de este método es el tipo de los parámetros, así como también es posible ver un número distinto de parámetros.

Es importante destacar que el nombre de los parámetros no es relevante entre un método y otro sino su tipo de dato.

public void println() {
  newline();
}

public void println(newline x) {
  print(x);
  newline();
}

public void println(char x) {
  print(x);
  newline();
}

public void println(int x) {
  print(x);
  newline();
}

public void println(float x) {
  print(x);
  newline();
}

Tabla 3. Ejemplo de sobrecarga, extracto de clase PrintStream de Java (Oracle, s.f.).

Concepto de abstracción 

Esta es una de las habilidades más importantes en lo que a diseño de programas bajo el paradigma orientado a objetos concierne, ya que fundamenta la producción de programas apegados al problema que pretenden resolver.

La abstracción consiste es determinar mediante la observación del problema cuáles elementos deben formar parte de la aplicación. Por un lado, se obtienen clases para el manejo de datos y, por otro, clases cuyos objetos están pensados para contener métodos que den solución a subproblemas específicos. Por ejemplo, si tuvieras que realizar un sistema de software para una tienda departamental, es fácil determinar que una de sus necesidades será el manejo de los datos de sus empleados, de los productos que formarán parte del inventario, quizás de un sistema de cobro, tal vez tengan tienda en línea, etc. Ahora imagina que dentro de la tienda departamental puedes observar ciertos elementos decorativos, tales como plantas o carteles publicitarios; estos no tendrían mucho sentido de ser incorporados en el sistema pues no aportan un valor al manejo de la empresa.

Así, el proceso de identificar cuáles clases tiene una relevancia tal, que deben ser incorporadas al sistema de software se sustenta en la abstracción.

De la misma manera, también debes determinar qué partes de la clase a incorporar tienen relevancia para la aplicación y aportan valor. Por ejemplo, veamos el caso de los empleados: sabemos que, en la vida real, al ser personas podrían ser descritas de una manera muy amplia, como identificar atributos como el color de sus ojos, número de cabellos o comportamientos como hablar o caminar, pero está claro que ninguno de estos aporta valor al sistema.

La abstracción también se trata de identificar qué atributos y qué comportamientos de las clases que ya has incorporado a tu sistema aportan valor real.

class Empleado {
  /*
  int cabellos;
  String colorDeOjos;

  void camina(){
  // código ilustrativo
  }

  void habla(){
  // código ilustrativo
  }
  */
}

Tabla 4. Ejemplo de atributos y comportamientos no relevantes para clase “empleado”.

Citando el ejemplo de la tabla anterior se pueden elegir otros atributos que sí pudieran ser relevantes para el sistema, como el nombre del empleado o su salario.

Recuerda que hay clases que están pensadas a contener información principalmente, por lo que es común que algunas no tengan comportamientos, al menos no dentro del sistema, y de la misma manera es posible identificar otras que solo tengan métodos, pero no atributos.

Concepto de encapsulamiento 

La integridad de la información es un tema central para cualquier sistema de software y en la programación orientada a objetos no es la excepción. El encapsulamiento consiste en ocultar datos y comportamientos dentro de una clase (Loy, 2020), pero ¿por qué es esto importante? Cuando los atributos son definidos como públicos significa que serán visibles para cualquier clase, de tal manera que cualquiera que use esta clase podrá acceder a las variables definidas y modificarlas.

En la siguiente tabla puedes ver un ejemplo de una clase tipo Empleado con dos atributos, nombre y salario. Ahora imagina que el atributo salario es visible y modificable desde cualquier punto. Esto significa que fácilmente podría ser asignado un valor negativo. Por ejemplo, salario = -15000.0 y está claro que nadie querría ser ese pobre empleado que tenga ingresos negativos.

class Empleado {

  private String nombre;
  private String RFC;

  public String getNombre() {
    return nombre;
  }

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

  public String getRFC() {
    return RFC;
  }

  public void setRFC(String rfc) {
    this.RFC = rfc;
  }
}

Tabla 5. Ejemplo de clase Empleado debidamente encapsulada.

Por otro lado, cuando se marcan los atributos como privados eso significa que no serán accesibles desde ningún lugar fuera de la misma clase, y ¿para qué sirven atributos que no se puedan acceder? Bueno, cuando esto es parte de las necesidades del sistema se deben ofrecer métodos conocidos como accessors y mutators o accesores y modificadores, por sus nombres en español.

El objetivo de estos métodos es el de controlar la manera en que se interactúa con los atributos de una clase. Así, para su lectura se sigue la convención de anteponer el verbo en inglés get, seguido del nombre del atributo, con la excepción de los atributos de tipo boolean. Para estos se usa el prefijo is, y en el caso de los métodos modificadores se usa el prefijo set, seguido del nombre del atributo para todos los tipos de datos.

Es justamente en los métodos modificadores donde es común programar las validaciones pertinentes para evitar que se asignen valores que rompan con la integridad de los datos.

Así el método setSalario puede tener una validación que impida que el valor de salario se asigne por encima del salario mínimo indicado por la legislación local.

Concepto de herencia

En Java la herencia se refiere a la característica de la programación orientada a objetos que permite definir clases que sean derivadas de otras. Cuando una clase está basada en otra, se dice que esta hereda de dicha clase y se le conoce como subclase y la clase usada como base se conoce como súper clase (Lowe, 2020).

class Programador extends Empleado {
  private String lenguajeEspecialidad;

  public String getLenguajeEspecialidad() {
    return lenguajeEspecialidad;
  }

  public String setLenguajeEspecialidad(String lenguajeEspecialidad) {
    this.lenguajeEspecialidad = lenguajeEspecialidad;
  }
}

Tabla 6. Ejemplo de herencia de clase Programador, extendiendo clase Empleado.

Para heredar de una clase en Java se utiliza la palabra reservada extends, como se muestra en la tabla anterior, y con esto todos los métodos y atributos no privados estarán incluidos también en la subclase.

En Java solo es posible heredar de una clase, a esto se le llama herencia simple, y su contraparte, la herencia múltiple, no es soportada por diseño del lenguaje.

Sin embargo, la herencia múltiple no tiene un límite para el número de subclases que se pueden definir. En este sentido, se pueden tener tantas subclases como sea requerido.

Por otro lado, también se pueden definir interfaces. Estas podrían ser descritas como clases especiales, cuyos métodos están todos marcados como abstractos por default, lo que implica que no tienen una implementación definida y esta deberá ser descrita en la clase que implemente dicha interfaz.

En este caso, no existe un límite para el número de interfaces que pueden ser implementadas por una clase, ya que los métodos, al no tener cuerpo definido, no generarían un conflicto en el caso de que hubiera dos métodos repetidos en dos interfaces diferentes que estén siendo implementadas, pues al final la subclase definirá un comportamiento final y los repetidos serían interpretados como uno mismo.

El siguiente diagrama muestra un ejemplo de una super clase “empleado” siendo heredada por dos subclases, Programador y Manager, así como la interfaz Entrevistador, siendo implementada por la clase Manager.

Figura 3. Diagrama que muestra relaciones de herencia entre clases e interfaces.

La siguiente tabla muestra la representación en código Java del diagrama anterior, de donde se destaca la palabra reservada abstract, marcando la clase Empleado. Esto indicaría que la clase Empleado es un concepto abstracto y no tiene sentido crear objetos de este tipo, pues estarían incompletos. Las clases abstractas pueden o no definir métodos abstractos, también marcados con la palabra reservada abstract. En el caso de existir métodos abstractos, estos deben ser implementados por las subclases; de lo contrario, estas también deben ser marcadas como clases abstractas o causarán un error de compilación.

abstract class Empleado {
  private float salarioBase;

  public float getSalarioBase() {
    return salarioBase;
  }

  public void setSalarioBase(float salarioBase) {
    this.salarioBase = salarioBase;
  }

  public float calcularSalarioFinal(){
    return getSalarioBase();
  }
}

interface Entrevistador {
  void entrevistar();
}

class Programador extends Empleado {

  @Override
  public float calcularSalarioFinal(){
    float bonoProductivo = getSalarioBase() * ((float)0.15);
    return getSalarioBase() + bonoProductivo;
  }

}

class Manager extends Empleado implements Entrevistador{

  @Override public void entrevistar() {
    //código ilustrativo
  }

  @Override
  public float calcularSalarioFinal(){
    float bonoProductivo = getSalarioBase() * ((float)0.25);
    return getSalarioBase() + bonoProductivo;
  }
}

Tabla 7. Ejemplo de código Java que muestra clases e interfaces con relación de herencia.

En la tabla anterior puedes también notar cómo hay un par de métodos marcados con la anotación @Override (anulación por su traducción del inglés, aunque es comúnmente conocido entre hispanohablantes como sobreescritura).

La anulación o sobreescritura se realiza sobre métodos que han sido heredados por alguna interfaz o alguna super clase, realizando una implementación de este; la anulación puede ser sobre métodos abstractos o no abstractos.

Concepto de polimorfismo

Teniendo una estructura de herencia entre varias clases, y habiendo anulado algunos métodos provenientes de las super clases en las subclases, es posible generar escenarios de polimorfismo.

El polimorfismo, la propiedad de una variable del tipo de alguna super clase o interfaz de almacenar la referencia de un objeto creado a partir de una subclase.

En la siguiente clase puedes ver un ejemplo de dos objetos, uno de tipo Programador y otros de tipo Managersiendo referenciados por variables de tipo Empleado.

Si tomas como referencia el código descrito en la tabla 7, la ejecución del método mostrarSalarioReal que recibe una variable de tipo Empleado, tendrá resultados diferentes dependiendo del tipo del objeto al que este referenciando la variable.

El método mostrarSalarioReal de la tabla 8 está haciendo uso del concepto de polimorfismo.

public static void main(String arg[]) {
  Empleado e1 = new Programador();
  Empleado e2 = new Manager();

  e1.setSalarioBase(1000);
  e2.setSalarioBase(1000);

  mostrarSalarioReal(e1);
  mostrarSalarioReal(e2);
}

public static void mostrarSalarioReal(Empleado empleado){
  System.out.println("Salario total: "+empleado.calcularSalarioFinal());
}

Tabla 8. Ejemplo de polimorfismo.

En la tabla 9 puedes ver los resultados esperados al ejecutar el código de la tabla anterior.

>> 

Salario total: 1150.0
Salario total: 1250.0

Process finished with exit code 0

Tabla 9. Resultado de la ejecución del código referenciado en la tabla 8.

El polimorfismo es una herramienta muy poderosa, por lo que se te recomienda documentarte ampliamente al respecto, pues te permitirá comprender de mejor manera implementaciones de alto nivel en múltiples aplicaciones.

Concepto de composición 

Además de la herencia es posible definir y derivar clases a partir de otras mediante la composición, aunque en este caso no se genera una relación de herencia, sino una relación de acoplamiento.

Es decir, una clase puede definir como parte de su código objetos de tipo de otras clases y con esto hacer uso de sus métodos. De esta manera se puede decir que la clase que utiliza objetos de otros tipos está compuesta por estos.

Concepto de acoplamiento y cohesión 

El acoplamiento permite a una clase utilizar código escrito en otra, y la manera más adecuada de diseñar clases que tengan está relación es contemplando el concepto de cohesión.

La cohesión indica el nivel de asertividad que tienen los métodos de una clase con esta, es decir, una clase de tipo Persona que incluya un método “volar” estará violando el concepto de cohesión, ya que este no es un comportamiento propio para una clase de este tipo.

En la medida que las clases que diseñes tengan un nivel alto de cohesión, estas tendrán también un nivel cada vez más bajo de acoplamiento, lo que es un indicador de un buen diseño de clases.

Manejo de excepciones 

En Java, las excepciones son objetos especiales que son creados cuando un error inesperado que no puede ser resuelto automáticamente ocurre dentro del sistema en el tiempo en que este se está ejecutando. (Lowe, 2020).

El objeto de tipo Exception contiene información acerca del error que acaba de ocurrir, indicado primeramente por el nombre de la excepción.
Algunas excepciones comunes en java son las siguientes:

IllegalArgumentException, indica que se pasaron parámetros incorrectos a un método.

NullPointerException, indica que se está intentando acceder a un método o atributo de un objeto que tiene una referencia nula.

Algo que debes saber es que cuando un error de este tipo ocurre y una excepción se genera, se dice que esta es lanzada y la operación que desencadena esta acción se indica con la palabra reservada throw.

En el siguiente diagrama puedes ver la jerarquía de las excepciones en Java, siendo la super clase de todas las clases Throwable, que representa a cualquier tipo de error, pero también podrás observar que de esta se desprenden dos clases. Por un lado, está la clase Exception, de la que se derivan todas las excepciones y, por otro, la clase Error, de la que derivan errores que no pueden ser manejados o mitigados durante la ejecución de un programa.

Figura 4. Diagrama que muestra la jerarquía de las clases para el manejo de errores en Java.

En la figura 10 puedes observar un ejemplo sencillo de código que produce un StackOverflowError, que es un tipo de error que se lanza cuando se ha terminado la memory stack disponible para la aplicación.

public static void print(String cadena) {
  print(cadena);
}

Tabla 10. Ejemplo de código que genera un ciclo infinito que termina por agotar la memoria del sistema.

Exception in thread "main" java.lang.StackOverflowError

Tabla 11. Ejemplo del texto mostrado en pantalla cuando se lanza un error de tipo StackOverflowError.

Dentro de las excepciones se tienen dos categorías, por un lado, están las llamadas handled exceptions que son aquellas que se verifican en tiempo de compilación y que permiten al programador anticipar un posible error e incluir un manejo para cuando estas sean lanzadas.

La sentencia que provee Java para este fin se llama try catch. En la tabla 12 se puede ver un ejemplo de cómo el código perteneciente a la solución del problema que puede lanzar una excepción se sitúa dentro del bloque try y las acciones que se llevarán a cabo en caso de que una excepción se lance se ubican dentro del bloque catch. También puedes ver cómo el bloque catch incluye la declaración de la excepción entre paréntesis, delante de la palabra reservada catch y antes de la llave que da inicio al bloque de código.

public void escribirEnArchivo() {
  try (BufferedWriter writer = new BufferedWriter(
  new FileWriter("archivo.txt"))) {
    writer.write("Test");
  } catch (IOException e) {
    e.printStackTrace();
  }
}

Tabla 12. Ejemplo del código que lanza una excepción de lectura o escritura, siendo manejada por un bloque try catch.

También es posible delegar el manejo de la excepción al método que invoque el código donde esta se lanza, y para ello se hace uso de la instrucción throws, que se posiciona entre los paréntesis del método y la llave que da inicio al bloque de código.

Cuando se usa la instrucción throws, la excepción no es manejada en ese momento, sino que se obliga que el código que invoca este código elija entre usar la sentencia try catch para manejar la excepción o seguir propagando el error al método que llamó a este, y así puede seguir hasta llegar al método raíz de cualquier ejecución en Java, que es el método main().

Cuando una excepción llega al método main() y no es manejada, esta hace que el programa termine de manera abrupta.

public static void escribirEnArchivo() throws IOException{
  BufferedWriter writer = new BufferedWriter(new FileWriter("archivo.txt"))
  writer.write("Test");
}

Tabla 13. Ejemplo del código que lanza una excepción de lectura o escritura, siendo delegada con la instrucción throws.

También debes saber que las excepciones no son solo herramientas para manejo de errores utilizadas por las diferentes clases de Java, sino que tú también puedes hacer uso de ellas para indicar cuando algo ha salido mal dentro del programa.

La tabla 14 muestra una validación realizada sobre un campo llamado Edad, en cuya aplicación solo estarían permitidos los valores superiores a 18, indicando que esa funcionalidad es solo para mayores de edad.

La sentencia throw reemplaza a la sentencia return como indicador de término de un método, ya que en el momento que esta se encuentra el método, termina inmediatamente y regresa el control de ejecución al método anterior. La sentencia throw puede lanzar una excepción completamente nueva, por lo que deberás crear un objeto de la excepción que mejor describa el problema que quieres reportar, pero también puede lanzar una excepción creada previamente.

public void setEdad(int edad) {
  if(edad < 18){
    throw new IllegalArgumentException("El empleado debe tener 18 años o más");
  }
  this.edad = edad;
}

Tabla 14. Ejemplo del código que lanza una excepción, usando la sentencia throw.


Cierre

Como has podido aprender, el paradigma de programación orientado a objetos facilita el proceso de análisis de un problema, permitiendo agrupar en clases todas aquellas características que identifican cada elemento del sistema, pudiendo así separar de aquellas dedicadas al manejo de la información de las que están destinadas a su procesamiento.

No obstante, a pesar de su versatilidad, el paradigma de programación orientado a objetos no ofrece solución a todos los problemas, por lo que en las soluciones modernas es muy común encontrar otros paradigmas para el desarrollo de software, muchas veces trabajando en sinergia para producir soluciones más potentes.

¿Conoces otros paradigmas de programación que complementen o sustituyan al paradigma orientado a objetos? ¿Qué escenarios podrían sacar el mayor provecho del paradigma orientado a objetos?


Checkpoint

Asegúrate de:

  • Comprender el proceso de abstracción mediante el cual puedes identificar las variables y funciones propias de cada clase para fundamentar los procesos de análisis y diseño de tus programas.
  • Comprender y poder aplicar el concepto de encapsulamiento para mantener la integridad de los datos y desarrollar aplicaciones sostenibles y confiables.
  • Entender para qué sirve heredar propiedades de una clase a otra para emplear este proceso de una manera óptima y producir código reutilizable y fácil de mantener.
  • Entender cómo sacar provecho a la sobrecarga de métodos para construir código fácil de entender y de utilizar.
  • Comprender la utilidad del uso de excepciones y cómo facilitan el manejo de errores para desarrollar aplicaciones resilientes, robustas y confiables capaces de sobreponerse a fallos en tiempo de ejecución.

Bibliografía

  • Boyarsky, J., y Selikoff, S. (2020). OCP Oracle Certified Professional Java SE 11 Developer Complete Study Guide. Estados Unidos: Sybex. 
  • Loy, M., Niemeyer, P., y Leuck, D. (2020). Learning Java (5a ed.). Estados Unidos: O'Reilly Media, Inc.
  • Lowe, D. (2020). Java All-in-One For Dummies (6ª ed.). Estados Unidos: For Dummies.
  • Oracle. (s.f.). Class PrintStream. Recuperado de https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/io/PrintStream.html

La obra presentada es propiedad de ENSEÑANZA E INVESTIGACIÓN SUPERIOR A.C. (UNIVERSIDAD TECMILENIO), protegida por la Ley Federal de Derecho de Autor; la alteración o deformación de una obra, así como su reproducción, exhibición o ejecución pública sin el consentimiento de su autor y titular de los derechos correspondientes es constitutivo de un delito tipificado en la Ley Federal de Derechos de Autor, así como en las Leyes Internacionales de Derecho de Autor.

El uso de imágenes, fragmentos de videos, fragmentos de eventos culturales, programas y demás material que sea objeto de protección de los derechos de autor, es exclusivamente para fines educativos e informativos, y cualquier uso distinto como el lucro, reproducción, edición o modificación, será perseguido y sancionado por UNIVERSIDAD TECMILENIO.

Queda prohibido copiar, reproducir, distribuir, publicar, transmitir, difundir, o en cualquier modo explotar cualquier parte de esta obra sin la autorización previa por escrito de UNIVERSIDAD TECMILENIO. Sin embargo, usted podrá bajar material a su computadora personal para uso exclusivamente personal o educacional y no comercial limitado a una copia por página. No se podrá remover o alterar de la copia ninguna leyenda de Derechos de Autor o la que manifieste la autoría del material.