08 May

Abstracción

La abstracción es un pilar fundamental en la Programación Orientada a Objetos. Consiste en:

  • Aislar un elemento de su contexto o del resto de los elementos que lo acompañan.
  • Enfocarse en lo esencial, ignorando lo irrelevante.
  • Es una herramienta importante para reducir la complejidad del software.

Reutilización

La reutilización se refiere a la acción de volver a utilizar bienes o productos. En el contexto del software, implica:

  • Aprovechar elementos existentes (código, diseños, componentes).
  • No duplicar el trabajo necesario para crear un elemento nuevo.
  • Es una herramienta importante para reducir el esfuerzo y mejorar la eficiencia.

En la Programación Orientada a Objetos, existen diferentes tipos de reutilización:

  • Reutilizar el código.
  • Reutilizar el objeto.
  • Reutilizar la API (Interfaz de Programación de Aplicaciones).
  • Reutilizar el diseño.

Encapsulamiento

El encapsulamiento es el mecanismo por el cual:

  • Se oculta la representación interna de un objeto.
  • El código y los datos internos del objeto son inaccesibles desde el exterior; solo el propio objeto tiene acceso a su representación interna.
  • Sirve como un mecanismo para aumentar el nivel de abstracción.
  • La API (Application Programming Interface) describe la parte pública del objeto, es decir, cómo interactuar con él sin conocer sus detalles internos.

Interacción entre Objetos: Envío y Recepción de Mensajes

Envío de Mensajes

Las instancias (objetos) interactúan entre sí por medio de mensajes. Enviar un mensaje equivale a delegar una tarea a otra instancia, es decir, se le solicita que ejecute uno de sus métodos. Cada mensaje consta de tres componentes esenciales:

  1. La instancia destinataria del mensaje.
  2. El método que se debe aplicar o ejecutar.
  3. Una asignación de valores a los argumentos del método (si los tiene).

Un mensaje se envía típicamente con la sintaxis: instancia.metodo(argumentos).

Recepción de Mensajes

Al recibir un mensaje, la instancia destinataria ejecuta el método indicado con los argumentos proporcionados. En la ejecución del método influye el valor actual de los atributos de la instancia. El resultado de la ejecución de un método puede ser:

  • Función: Retorna un valor. Es necesario especificar el tipo de dato que devuelve el método.
  • Procedimiento: No devuelve ningún valor (void). Realiza un proceso que, por ejemplo, puede cambiar los valores de los atributos de la instancia de una forma concreta. Se especifica que es un procedimiento añadiendo la palabra clave void en su declaración (en lenguajes como Java o C++).

Miembros de Clase y Miembros de Instancia

  • Miembros de clase: Son atributos o métodos que pertenecen a la clase en sí misma, y no a una instancia particular de la clase. En Java, se implementan con el modificador static. Son compartidos por todas las instancias de la clase.
  • Miembros de instancia: Pertenecen únicamente a una instancia específica de la clase. Cada instancia tiene su propia copia de estos miembros, utilizando diferentes ubicaciones de memoria.

La Palabra Clave this

Cada método de instancia se aplica sobre un objeto concreto (una instancia). La palabra clave this (o similar, según el lenguaje) hace referencia a la instancia actual con la que se está trabajando dentro de un método de instancia.

Tipos Genéricos (Templates/Patrones)

Los tipos genéricos (conocidos como templates en C++ o generics en Java y C#) permiten crear definiciones de clases, interfaces y métodos que operan con tipos de datos como parámetros. Estos «patrones» o plantillas trabajan con tipos abstractos que se concretarán (especificarán) en tiempo de compilación, ofreciendo flexibilidad y seguridad de tipos.

La Clase Object en Java

En Java, existe una clase especial llamada Object que es la superclase (clase padre) de todas las demás clases, directa o indirectamente. Esto significa que se pueden aplicar los métodos definidos en la clase Object (como toString(), equals(), hashCode()) a cualquier instancia de cualquier clase.

Polimorfismo

El polimorfismo (del griego «muchas formas») es un concepto fundamental que consiste en la capacidad de tratar objetos de diferentes clases de manera uniforme si comparten una superclase o interfaz común. Permite aplicar una programación genérica, donde un objeto se puede comportar de diferentes maneras según su tipo real en tiempo de ejecución.

Los mecanismos principales para lograr el polimorfismo son:

  • Utilizar un tipo declarado (de variable) que sea diferente al tipo real de la instancia guardada (siempre que haya una relación de herencia o implementación de interfaz).
  • La sobreescritura de métodos en las subclases.

Tipo Declarado vs. Tipo Instanciado

  • Tipo declarado: Es el tipo especificado al definir la variable. Determina a qué métodos se puede llamar sobre esa variable en tiempo de compilación (solo se pueden invocar métodos definidos en el tipo declarado o sus superclases/interfaces).
  • Tipo instanciado (o tipo real): Es el tipo de la instancia que realmente está guardada en la variable en un momento dado. Debe ser una subclase (directa o indirecta) del tipo declarado o una clase que implemente la interfaz declarada.

Sobreescritura de Métodos (Overriding)

La sobreescritura consiste en volver a definir un método existente de una superclase en una subclase. Para que sea una sobreescritura válida, el método en la subclase debe conservar la misma signatura (nombre, número y tipo de parámetros) y el mismo tipo de retorno (o un subtipo covariante) que el método en la superclase, pero puede cambiar su implementación.

Sobrecarga de Métodos (Overloading)

La sobrecarga permite declarar dos o más métodos con el mismo nombre dentro de la misma clase, pero con argumentos de entrada diferentes (es decir, una signatura diferente, ya sea por el número de argumentos, el tipo de los argumentos o ambos). El compilador los trata como métodos distintos.

Binding (Enlace)

El binding o enlace se refiere a la conexión entre una llamada a método y la definición específica del método que se ejecutará. Existen dos tipos de vínculos:

  • Static Binding (Enlace Estático o Temprano): Ocurre en tiempo de compilación. El compilador ya sabe cuál es el vínculo exacto entre la llamada al método y el código del método que se ejecutará. Esto sucede con métodos final, static y private, o cuando el tipo exacto del objeto es conocido.
  • Dynamic Binding (Enlace Dinámico o Tardío): Ocurre en tiempo de ejecución. No se sabe cuál de las versiones de los métodos sobreescritos se utilizará hasta el momento de la ejecución del programa. Esto es fundamental para el polimorfismo, ya que la decisión de qué método ejecutar depende del tipo real del objeto (tipo instanciado), no del tipo declarado de la variable. El Dynamic Binding es necesario para implementar el polimorfismo de manera efectiva.

Casting (Conversión de Tipos)

El casting es el proceso de guardar una misma instancia en otra variable con un tipo declarado diferente. Es decir, se cambia el tipo declarado de la referencia, pero no el tipo instanciado del objeto. Puede ser de dos tipos:

  • Upcast (Conversión Ascendente): Cambiar a un tipo más general (al de una superclase o una interfaz implementada). Esto siempre está permitido y es implícito, ya que un objeto de una subclase «es un» tipo de la superclase.
  • Downcast (Conversión Descendente): Cambiar a un tipo más específico (al de una subclase). Esto puede fallar en tiempo de ejecución si el objeto real no es del tipo al que se intenta convertir, por lo que a menudo requiere una verificación (por ejemplo, con instanceof) y una conversión explícita.

El casting nos sirve para poder aplicar una programación genérica, tratando igual a las instancias de diferentes subclases a través de una referencia de superclase (upcast), y para acceder a miembros específicos de una subclase cuando sea necesario (downcast).

Por ejemplo: tenemos un programa con las clases Alumno y Profesor, ambas heredan de la clase Persona. Podemos hacer un upcast tanto de instancias de Alumno como de Profesor a variables de tipo Persona para tratarlos de forma genérica y aplicarles los métodos definidos en Persona. El downcast nos permitiría, si sabemos que una referencia Persona apunta a un Alumno, convertirla de nuevo a Alumno para llamar a métodos específicos de la clase Alumno que no están en Persona.

Clases y Métodos Abstractos

Tanto los métodos abstractos como las clases abstractas se crean para facilitar la herencia y el polimorfismo, permitiendo que múltiples subclases trabajen con un método que tiene el mismo nombre (definido en la superclase abstracta) pero con implementaciones diferentes en cada subclase. Se utiliza el modificador abstract para indicar un método o clase abstracta.

Método Abstracto

  • No proporcionan implementación en la clase donde se declaran; su cuerpo se reemplaza por un punto y coma (;).
  • Deben ser sobreescritos (implementados) por cualquier subclase concreta que herede de la clase abstracta.
  • No pueden ser final (porque deben ser sobreescritos), ni private (porque deben ser visibles para las subclases), ni static (porque están ligados a la instancia para su sobreescritura polimórfica).

Clases Abstractas

  • Sirven para agrupar comportamiento común de sus subclases, a menudo por medio de métodos abstractos que las subclases deben implementar. Están pensadas para ser heredadas.
  • No se pueden crear instancias directas de una clase abstracta (new ClaseAbstracta() es inválido).
  • Si una clase contiene al menos un método abstracto, la clase misma debe ser declarada como abstracta.
  • Una clase no puede ser declarada como final y abstract al mismo tiempo (final impide la herencia, abstract la requiere).
  • El tipo instanciado de una variable no puede ser el de una clase abstracta, pero el tipo declarado sí puede serlo.
  • Si se aplica un método a una variable cuyo tipo declarado es una clase abstracta, se ejecutará la versión del método correspondiente al tipo instanciado (real) del objeto, gracias al polimorfismo y al dynamic binding.

Interfaces

Una interfaz en programación orientada a objetos es un tipo de referencia, similar a una clase, que solo puede contener constantes (atributos static y final) y declaraciones de métodos abstractos (métodos sin implementación). Define un contrato que las clases pueden comprometerse a cumplir.

  • En una interfaz, todos los métodos son implícitamente public y abstract (por lo tanto, no es necesario añadir explícitamente estos modificadores, aunque se puede).
  • Una interfaz también puede contener constantes, que son implícitamente public, static y final.
  • Para crear una interfaz, se usa la palabra clave interface. Ejemplo:
    public interface Comparable {
      public int compareTo(Object o); // ¡Es abstracto!
    }
  • Para que una clase aplique o cumpla con una interfaz, se usa la palabra clave implements.
  • La clase que implementa una interfaz debe sobreescribir (proporcionar una implementación para) todos los métodos abstractos declarados en la interfaz, o bien, la clase misma debe ser declarada como abstracta.
    public class Square implements Shape {
      // ... implementaciones de los métodos de Shape ...
    }
  • ¡Una clase puede implementar múltiples interfaces! Para ello, se escribe la lista de interfaces separadas por comas:
    public class MyClass implements Comparable, Runnable, OtherInterface {
      // ... implementaciones ...
    }
  • El tipo declarado de una variable puede ser una interfaz. Sin embargo, el tipo instanciado (el objeto real asignado a esa variable) tiene que ser de una clase no abstracta que implemente dicha interfaz.
  • Las interfaces son un mecanismo poderoso para lograr la abstracción y el polimorfismo, promoviendo una programación genérica y desacoplada.

Reutilización por Inclusión (Composición)

La reutilización por inclusión, a menudo asociada con el principio de composición sobre herencia, se refiere a reutilizar la funcionalidad de una clase incorporando una instancia de esa clase dentro de otra, sin modificar la estructura interna de la clase reutilizada. Para ello, es necesario conocer su funcionalidad (cómo utilizar la clase, generalmente descrito por su API) y las limitaciones de la clase.

Packages (Paquetes)

Los packages (paquetes) en lenguajes como Java son un mecanismo para organizar clases e interfaces en espacios de nombres jerárquicos. Agrupan clases relacionadas, ayudando a evitar conflictos de nombres y a controlar la visibilidad.

  • Los ficheros fuente (.java) de las clases que pertenecen al mismo paquete suelen guardarse en una estructura de carpetas que refleja la jerarquía del paquete.
  • Para utilizar clases de otros paquetes en un fichero, se usa la declaración import.

Patrones de Diseño

Los patrones de diseño son soluciones probadas y reutilizables a problemas comunes que surgen durante el diseño de software orientado a objetos. Consisten en reutilizar un «diagrama de clases» o una estructura conceptual, adaptándola a un programa concreto.

  • Aportan experiencia de diseño a las nuevas implementaciones y ayudan a conseguir más rápidamente un buen diseño robusto y mantenible.
  • Existen muchos patrones de diseño catalogados. Algunos ejemplos comunes son:
  • Iterator (Iterador): Proporciona un mecanismo general para acceder de forma secuencial a los elementos de una colección (como una lista o un conjunto) sin exponer su representación interna. Cada estructura de datos o colección proporciona su propia implementación particular del iterador.

La Palabra Clave static

El modificador static en Java (y otros lenguajes) indica que un miembro pertenece a la clase misma, en lugar de a las instancias de la clase.

  • Atributo static (Variable de clase): Solo existe una copia de este atributo, independientemente de cuántas instancias de la clase se creen. Su valor es compartido por todos los objetos instanciados de la clase. Se accede a él usando el nombre de la clase (NombreClase.atributoStatic).
  • Método static (Método de clase): Puede ser invocado sin crear una instancia de la clase (NombreClase.metodoStatic()). Un método static únicamente tiene acceso directo a otros miembros static (atributos y métodos) de la misma clase. No puede usar la palabra clave this ni acceder directamente a miembros de instancia, ya que no opera sobre un objeto particular.

Deja un comentario