Programación Orientada a Objetos

La Programación Orientada a Objetos (POO) es un paradigma de programación que se ha vuelto indispensable en la actualidad. Este enfoque modela elementos del mundo real como “objetos” que tienen propiedades y comportamientos, lo cual permite crear programas más intuitivos y fáciles de mantener. En este artículo veremos los conceptos básicos de POO y sus ventajas frente a otros paradigmas como la programación procedural. ¡Empecemos!

Este paradigma se basa en dos conceptos fundamentales:

  • Objetos: entidades que combinan estado (datos) y comportamiento (operaciones) en una misma unidad. Por ejemplo, un objeto “coche” tendría propiedades como color, número de puertas, velocidad máxima, etc. Y comportamientos como acelerar, frenar, girar, etc.
  • Clases: especificaciones que definen la estructura y comportamiento común de un grupo de objetos. La clase “coche” serviría como molde para crear objetos coche con las mismas características.

Como explica el programador Alan Kay, uno de los creadores de la POO:

“La idea central de POO es que los usuarios deben manipular objetos conceptuales más que máquinas de Turing. Las interfaces con el mundo real deben, por lo tanto, ser construidas en términos de objetos conceptuales.”1

Es decir, la POO modela conceptualmente elementos del mundo real para hacer la programación más intuitiva.


Paradigmas de programación

Antes de profundizar en la POO, conviene entender que existen diferentes paradigmas o enfoques para abordar la programación. Los principales son:

Programación procedural

Secuencia ordenada de instrucciones que el programa debe seguir paso a paso. El foco está en procedimientos y funciones. Por ejemplo, C es un lenguaje orientado a la programación procedural.

La programación procedural es mejor para:

  • Problemas sencillos o algoritmos secuenciales.
  • Código que no necesitará reusarse ni expandirse mucho.
  • Casos donde el rendimiento y eficiencia son críticos.

Programación orientada a objetos

Modelo basado en objetos que contienen datos y código en unidades cohesivas. El foco está en las clases y en la interacción entre objetos. Por ejemplo, Java y Python son lenguajes orientados a objetos.

La POO permite modelar de forma más directa elementos del mundo real, encapsular mejor los datos y reutilizar código a través de la herencia entre clases.

Las principales ventajas de POO frente a la programación procedural son:

  • Modularidad: los objetos agrupan datos y operaciones relacionadas, encapsulando la complejidad interna. Esto permite trabajar con módulos independientes.
  • Ocultación de información: Los objetos pueden exponer una interfaz simple y ocultar detalles de implementación internos. Esto reduce acoplamientos.
  • Reusabilidad: Las clases permiten reuse de código. Una clase abstracta puede heredar a múltiples subclases.
  • Extensibilidad: Podemos extender el comportamiento de clases padres creando nuevas subclases.
  • Mapeo conceptual: Los objetos representan entidades del mundo real, lo cual facilita la traducción de requerimientos a código.

Sin embargo, la POO también tiene desventajas. Según el programador Paul Graham:

“La programación orientada a objetos suele ser una molestia. Hace que las cosas sean más difíciles de lo que deberían ser.”2

Por ejemplo, para problemas simples la POO puede resultar excesiva. Y en proyectos grandes existe el riesgo de abusar de la herencia y el polimorfismo, volviendo el código difícil de seguir.

En definitiva, la POO es más adecuada cuando:

  • El problema a modelar tiene entidades claras y estructuradas.
  • Queremos reutilizar código encapsulado en clases modulares.
  • Trabajamos en sistemas que deben extenderse y mantenerse con facilidad.

Conceptos básicos de POO

Ahora que conocemos las ideas generales detrás de la POO, veamos algunos de los conceptos clave:

Objetos

Un objeto es una combinación de datos (propiedades) y comportamientos (métodos). Por ejemplo, un objeto Coche tendría propiedades como marca, modelo, color y métodos como acelerar, frenar, etc.

# Clase Coche
class Coche:

  def __init__(self, marca, color):
    self.marca = marca
    self.color = color

  def acelerar(self):
    print("Acelerando", self.marca)

# Crear Objeto
mi_coche = Coche("Toyota", "Rojo")
  • mi_coche es ahora un objeto Coche con propiedades y métodos definidos en su clase.

Clases

Una clase define los atributos (propiedades) y métodos (funciones) comunes a un grupo de objetos. Funciona como un molde para crear objetos similares.

Por convención las clases se definen con la primera letra en mayúscula. Las propiedades y métodos de una clase reciben el prefijo self para indicar que pertenecen a esa instancia de objeto.

class Circulo:

  def __init__(self, radio):
    self.radio = radio

  def area(self):
    return 3.1416 * (self.radio ** 2)
  • Circulo define la clase con propiedad radio y método area().

Métodos

Los métodos son funciones que definen el comportamiento de un objeto. Se declaran dentro de la clase y pueden acceder a las propiedades del objeto mediante self.

Un constructor es un método especial (__init__) que se ejecuta al crear objetos para inicializar sus propiedades.

class Persona:

  def __init__(self, nombre, edad):
    self.nombre = nombre
    self.edad = edad

  def saludar(self):
    print(f"Hola! Soy {self.nombre}")

juan = Persona("Juan", 30) # Ejecuta __init__
juan.saludar() # "Hola! Soy Juan"
  • El constructor asigna nombre y edad. El método saludar() accede al atributo nombre.

Propiedades

Las propiedades son variables asociadas a un objeto que definen sus características o estado. Se declaran en la clase y se accede a ellas mediante la referencia del objeto.

class Rectangulo:

  def __init__(self, alto, ancho):
    self.alto = alto
    self.ancho = ancho

r = Rectangulo(3,4)
print(r.alto) # 3
r.ancho = 8
print(r.ancho) # 8

Se recomienda declarar las propiedades como privadas y acceder mediante métodos getters/setters para respetar el encapsulamiento.

Encapsulamiento

Consiste en ocultar los detalles de implementación internos de un objeto exponiendo solo una interfaz pública. Esto se logra declarando métodos y propiedades con los modificadores public o private.

En Python se denota con guion bajo prefijo para métodos/propiedades privadas:

class CuentaBancaria:

  def __init__(self, saldo=0):
    self.saldo = saldo

  def depositar(self, monto):
    self._saldo += monto

  def consultar_saldo(self):
    return self._saldo
  • _saldo es privado, solo se accede internamente o por consultar_saldo().

El encapsulamiento facilita cambiar partes internas de una clase sin afectar su interfaz pública.


Conclusión

La Programación Orientada a Objetos modela elementos del mundo real como clases y objetos, priorizando la modularidad, ocultación de información y reuso de código para crear programas más robustos y fáciles de mantener.

Aunque puede resultar excesiva para problemas simples, la POO es ideal para sistemas de mediana/gran escala que necesitan expandirse y evolucionar en complejidad con el tiempo.

Conceptos como herencia, polimorfismo, abstracción e interfaces permiten aprovechar al máximo las ventajas de este paradigma. Con una comprensión sólida de sus fundamentos estamos listos para aplicar la POO en cualquier lenguaje y proyecto de programación.

Referencias


  1. Kay, Alan. The early history of Smalltalk. http://gagne.homedns.org/~tgagne/contrib/EarlyHistoryST.html ↩︎

  2. Graham, Paul. Why Arc Isn’t Especially Object-Oriented. http://www.paulgraham.com/noop.html ↩︎

1 - Clases y Objetos

En programación orientada a objetos, las clases y objetos son los conceptos centrales para entender cómo modelamos elementos de la realidad y definimos su estructura y comportamiento dentro del software. Veamos en detalle la anatomía de una clase, cómo crear objetos a partir de ella para usar sus propiedades y métodos, y otros detalles clave de su relación.

Anatomía de una clase

Una clase actúa como un plano o molde para construir objetos similares, definiendo sus características comunes y funcionalidades. Es similar al plano para construir casas de un mismo barrio: todas comparten ciertos atributos clave.

Los componentes típicos de una clase son:

Atributos (propiedades): Variables que caracterizan al objeto. Por ejemplo, para una clase Persona, atributos como nombre, edad, DNI, etc.

class Persona:
  dni = ""
  nombre = ""
  edad = 0

Métodos: Funciones que definen comportamientos. Por ejemplo, una Persona puede caminar(), hablar(), comer(), etc. Acceden a los atributos para implementar dicha funcionalidad.

Constructor: Método especial __init__() que se ejecuta al instanciar la clase y permite inicializar los atributos.

Destructor: Método __del__() que se ejecuta al eliminar la instancia liberando recursos. Opcional en algunos lenguajes.


Creando objetos

A partir de la clase generamos objetos, que son instancias concretas con sus propios atributos definidos. Digamos que la clase Casa es el plano, y una casa específica en una calle determinada es el objeto.

En código creamos un objeto invocando la clase como si fuera un método:

# Clase Persona
class Persona:
    def __init__(self, n, e):
        self.nombre = n
        self.edad = e

# Objeto Persona específico
pepe = Persona("Pepe", 30)
juan = Persona("Juan", 35)

Cada objeto comparte la estructura y comportamiento general, pero puede almacenar distintos datos.

Utilizando Propiedades y Métodos

Ya tenemos una clase Persona y un objeto pepe de tipo Persona. ¿Cómo interactuamos con el objeto?

  • Propiedades: Es posible acceder al valor de un atributo del objeto utilizando la referencia al objeto (pepe) y el nombre del atributo.
pepe.nombre  # "Pepe"
pepe.edad    # 30
  • Métodos: De la misma manera en la que se accede a los atributos pero agregando un paréntesis dentro del cual se pasan los argumentos si es que recibe alguno.
# Clase Persona
class Persona:
    def __init__(self, n, e):
        self.nombre = n
        self.edad = e

    def comer(self, comida):
        print(f"Comiendo {comida}")

# Objeto Persona específico
pepe = Persona("Pepe", 30)
pepe.comer("pizza") # Imprime "Comiendo pizza"

El objeto pepe tiene ahora estado (propiedades) y comportamiento (métodos) propios.


Self vs This

Un detalle importante en los métodos es cómo acceden a los atributos y otros métodos del objeto. Aquí entra otra diferencia entre lenguajes:

  • Self: En Python, los atributos y métodos se acceden dentro de la clase anteponiendo self. Esto apunta al objeto instanciado.
class Persona:

  def __init__(self, nombre):
    self.nombre = nombre

  def saludar(self):
    print(f"Hola! Soy {self.nombre}")

juan = Persona("Juan")
juan.saludar()
# Imprime "Hola! Soy Juan"
  • This: En Java o C#, se utiliza this en lugar de self. Cumple la misma funcionalidad de apuntar a los miembros del objeto.
public class Person {

  private String nombre;

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

  public void saludar() {
    System.out.println("Hola! Soy " + this.nombre);
  }
}

Person juan = new Person("Juan");
juan.saludar();
// Imprime "Hola! Soy Juan"

Conclusión

Las clases y objetos son los conceptos clave de la POO, permitiendo modelar entidades de la realidad y generar componentes modulares y genéricos de nuestro sistema para construir programas más robustos y fáciles de entender y mantener.

2 - Encapsulamiento

Uno de los pilares fundamentales de la programación orientada a objetos es la encapsulación. Esta poderosa característica nos permite controlar el acceso a los miembros de una clase, ocultando los detalles de implementación y protegiendo el estado de nuestros objetos. En este artículo profundizaremos en el concepto de encapsulación, la utilidad de los getters, setters, propiedades y métodos públicos/privados, así como en los importantes beneficios que esto nos proporciona como desarrolladores.

Uno de los pilares fundamentales de la programación orientada a objetos es el encapsulamiento. Esta potente característica nos permite controlar el acceso a los miembros de una clase, ocultando los detalles de implementación y protegiendo el estado de nuestros objetos. En este artículo veremos en profundidad el concepto de encapsulamiento, la utilidad del uso de getters, setters, propiedades y métodos públicos/privados, y los importantes beneficios que esto nos brinda como desarrolladores.

El diccionario de la Real Academia Española define encapsulamiento como el “envuelto o contenido como dentro de una cápsula”. Esto es precisamente lo que buscamos, “empaquetar” datos y código dentro de una única cápsula (la clase) para ocultar su complejidad interna.

La definición formal sería:

“El encapsulamiento es el empaquetamiento de los datos y funciones que manipulan estos datos en una única entidad o módulo de programa.”1

Es decir, mantener juntos datos y comportamientos relacionados para restringir el acceso directo a dichos datos desde otras partes del programa, interactuando solo a través de una interfaz controlada (API pública).

Esto provee ventajas como las siguientes:

  • Control sobre modificación de datos.
  • Flexibilidad al poder cambiar partes internas sin afectar otras partes.
  • Protección del estado consistente de los objetos.
  • Ocultación de complejidad al usuario.

Veamos con ejemplos concretos cómo encapsular en POO.


Getters y Setters

Digamos que tenemos una clase CuentaBancaria, con propiedades como nombre, número de cuenta y saldo:

class CuentaBancaria:

    nombre = ""
    nro_cuenta = 0
    saldo = 0.0

Podemos acceder directamente a los atributos así:

cuenta1 = CuentaBancaria()
cuenta1.nombre = "Juan"
cuenta1.nro_cuenta = 1234
cuenta1.saldo = 2500

El problema es que cualquier otro código puede modificar el saldo a valores inválidos:

cuenta1.saldo = -9900 # ¡Saldo no puede ser negativo es este banco!

Esto permite estado inconsistente. Para encapsular usamos getters y setters:

class CuentaBancaria:

    def __init__(self):
        self.__saldo = 0

    def get_saldo(self):
        return self.__saldo

    def set_saldo(self, valor):
        if valor < 0:
            raise Exception("Saldo no puede ser negativo en este banco")
        self.__saldo = valor
  • __saldo es ahora privado. Solo se manipula mediante los getters y setters públicos.

  • El setter controla que no se ingresen valores no válidos.

En Python, anteponer doble guion bajo __ denota un método o atributo privado de la clase. Con guion simple _ es por convención un elemento protegido, o sea, accesible desde la clase y subclases pero no desde fuera. Y sin guiones, los métodos y atributos son públicos.

En Java es explícito utilizando las palabras claves public, protected y private:

public class Persona {

    private String nombre; // Privado

    public String getNombre() { // Público
        return this.nombre;
    }

}

Esta notación nos ayuda a declarar adecuadamente la visibilidad deseada para aplicar encapsulamiento.


Beneficios del encapsulamiento

Esta poderosa técnica nos proporciona grandes ventajas:

  • Ocultación de información: Los detalles de implementación son invisibles para otros objetos, reduciendo acoplamientos. Podemos cambiar código interno minimizando impacto.
  • Control sobre datos: Se garantiza la integridad y validez de estado mediante los setters/validadores.
  • Código flexible: El aislamiento entre interfaces y detalles específicos permite construir sistemas más extensibles y fáciles de mantener en el tiempo.

“Todo módulo oculta la complejidad de su contenido detrás de una fachada (interfaz) simple”, Gang of Four2.

En definitiva, cuando necesitamos controlar cómo se manipula el estado interno de una clase desde otras partes de la aplicación, el encapsulamiento es la mejor solución.


Conclusión

Aplicar encapsulamiento restringiendo el acceso directo a los datos y codificando cuidadosamente una interfaz de acceso pública, nos permite construir sistemas POO más robustos, seguros y sustentables en el tiempo.

Dominar estas técnicas requiere experiencia y buen criterio para encontrar el balance adecuado entre ocultación de información y provisión de flexibilidad. Pero sin dudas vale la pena el esfuerzo para exprimir los beneficios que hemos visto de este maravilloso principio de la POO.


Referencias


  1. Byron, Jeff. Encapsulation in Java. https://stackify.com/encapsulation-in-java/ ↩︎

  2. Gamma, Erich et al. Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley. 1994. ↩︎