Paradigmas de programación
Me apasiona hablar, escribir y grabar sobre programación. Sin embargo, en medio de la multitud de frameworks y diversas técnicas para escribir código de calidad y sostenible a lo largo del tiempo, a veces olvidamos los fundamentos sobre los que se construye todo. Como me dijo un amigo una vez: “Los fundamentos siempre serán la base de todo lo que es y de todo lo que llegará a ser”.
Por esta razón, he decidido escribir este artículo, dirigido tanto a principiantes como a expertos, para recordar por qué invertimos una gran parte de nuestro tiempo diseñando y modelando las bases de nuestros sistemas. Espero que, al leerlo, comprendas que no necesitas complicarte demasiado para crear algo sólido y de calidad.
Paradigmas de programación
En la industria del desarrollo de software, es común escuchar acerca de los paradigmas de programación y sus diferentes usos y reglas. Pero, ¿qué son realmente? Si lo comparamos con otro contexto, podríamos pensar en las normas APA para escribir, que establecen un conjunto de reglas a seguir según el tipo de texto que estemos redactando. A continuación, exploraremos los paradigmas más utilizados en la programación: Programación Imperativa, Programación Orientada a Objetos (POO), Programación Orientada a Eventos y Programación Funcional. No te preocupes, no entraremos en definiciones exhaustivas de cada uno, pero sí entenderemos en qué casos se pueden utilizar según el sistema que deseemos diseñar.
Mantengamos las cosas simples
A menudo, tendemos a sobrecomplicar y forzar diseños cuando, en realidad, nuestra aplicación no requiere tanto esfuerzo. Uno de los escenarios más comunes es cuando una aplicación se limita a realizar operaciones de Crear, Leer, Actualizar y Borrar (CRUD) en una fuente de almacenamiento, ya sea una base de datos, un archivo Excel o un archivo plano (txt). Sabemos que esto no cambiará en el futuro cercano o que se trata de un proyecto a corto plazo. En este caso, la programación clásica es ideal, donde creamos funciones y pasos claros, en otras palabras, simple y funcional. A esto se le llama programación Imperativa, que se divide en programación estructurada y programación procedural. La primera se utiliza cuando una función cabe en un bloque y la segunda cuando organizamos las funciones en bloques más grandes.
¿Cuándo se utiliza?
La programación imperativa se utiliza ampliamente en sistemas de propósito general, aplicaciones de escritorio, juegos y sistemas integrados. También es común en la programación de bajo nivel, como controladores de hardware y sistemas operativos. Un ejemplo sería el desarrollo de un sistema de gestión de archivos para un sistema operativo, donde se requiere un control preciso de los recursos del hardware.
¿Un ejemplo en código?
#include <stdio.h>
int main() {
int x = 5;
int y = 10;
int suma = x + y;
printf("La suma es: %d", suma);
return 0;
}
Todo lo que nos rodea es un objeto
Si deseamos representar algo de la vida real en un sistema, ¿cómo lo haríamos? Para ello, creamos una representación de una entidad, a la que la industria se refiere como un objeto. Es decir, cualquier cosa que tenga características y acciones que den lugar a un comportamiento cuando interactúan con otros objetos. En teoría, es sencillo y divertido. Tomemos un ejemplo: Un perro tiene características como nombre, raza y edad, que llamaremos atributos, así como acciones como ladrar, que llamaremos métodos. Si creamos una plantilla con estos atributos y métodos, la llamamos clase en la programación orientada a objetos.
Hasta este punto, ya entendemos cómo definir una clase, pero ¿cuándo se convierte esta clase en un objeto? Si una clase es una plantilla, el uso de la plantilla sería un objeto. Veamos algunos ejemplos: el objeto “Perro San Bernardo”, el objeto “Perro Pinscher” y el objeto “Perro Bulldog”.
Pero ahora, ¿cómo representamos una relación entre objetos? ¿Qué tal si del ejemplo de perros agregamos que tengan hijos y estos se crucen para crear nuevas razas? ¿Cómo se representaría esto? Este paradigma busca representar entidades de la vida real plasmadas en objetos y por tanto, el concepto de herencia también aplica. Es decir, todos los perros hijos tendrán atributos y métodos comunes heredados de sus padres, tales como tener un nombre, raza, edad y poder ladrar. Cada uno de sus hijos podrá agregar atributos y métodos nuevos, por ejemplo, tamaño, color y tipo de pelaje. Pero al final, no olvidemos que estamos haciendo una abstracción usando clases, es decir, la clase padre tendrá clases hijas y estas podrán tener hijas a su vez.
Ahora bien, ¿qué pasa si algunos atributos no son heredados o no queremos que se pasen a la siguiente generación? Ocultamos atributos para que solo se puedan usar en una clase en particular, y esto se le llama encapsulamiento. Podremos establecer atributos privados y otros públicos (que se puedan heredar).
Para que las cosas no se salgan de control, usaremos abstracción para simplificar aún más. No es necesario mostrar y ocultar todo; podemos agrupar acciones y atributos en un solo método o varios.
Al final, las cosas intrínsecas que todos los perros pueden hacer, como ladrar, en nuestro diseño, podemos crear un único método común, “ladrar”, para todas las clases de perros. Así, aprovechamos el polimorfismo para indicar que, independientemente del objeto que represente un perro, podrán ladrar sin necesidad de conocer una raza puntual. Esto realmente marca la diferencia y genera la magia de diseñar cosas realmente complejas. ¿Te imaginas recorrer todos los posibles perros que puedan existir simplemente para hacer que ladren? Claramente, estamos hablando del diseño de un sistema.
¿Cuándo se utiliza?
La programación orientada a objetos es muy adecuada para sistemas grandes y complejos donde se pueden identificar entidades y relaciones entre ellas. Se utiliza comúnmente en el desarrollo de aplicaciones empresariales, videojuegos y sistemas de software reutilizables. Por ejemplo, su aplicación podría ser en el desarrollo de un sistema de gestión de inventario para una cadena de tiendas, donde se pueden representar productos como objetos con atributos y métodos.
¿Un ejemplo en código?
Clases:
Definiremos la clase “Perro” que servirá como plantilla para crear objetos de perros.
<python>
class Perro:
def __init__(self, nombre, raza, edad):
self.nombre = nombre
self.raza = raza
self.edad = edad
Objetos:
Crearemos dos objetos de la clase “Perro” para representar perros individuales.
<python>
perro1 = Perro("Buddy", "Labrador", 3)
perro2 = Perro("Rex", "Pastor Alemán", 5)
Encapsulación:
Agregaremos métodos para acceder y modificar los atributos de los perros de manera controlada, manteniendo los atributos como privados y proporcionando métodos públicos.
<python>
class Perro:
def __init__(self, nombre, raza, edad):
self.__nombre = nombre
self.__raza = raza
self.__edad = edad
def obtener_nombre(self):
return self.__nombre
def obtener_raza(self):
return self.__raza
def obtener_edad(self):
return self.__edad
def establecer_nombre(self, nombre):
self.__nombre = nombre
def establecer_raza(self, raza):
self.__raza = raza
def establecer_edad(self, edad):
self.__edad = edad
Herencia:
Crearemos una subclase llamada “PerroSalvaje” que hereda de la clase “Perro” y agrega un nuevo atributo.
<python>
class PerroSalvaje(Perro):
def __init__(self, nombre, raza, edad, territorio):
super().__init__(nombre, raza, edad)
self.territorio = territorio
Polimorfismo:
Agregaremos un método sonido en la clase base “Perro” y en la subclase “PerroSalvaje” para mostrar cómo diferentes tipos de perros pueden hacer sonidos diferentes.
<python>
class Perro:
# ...
def sonido(self):
return "Guau"
class PerroSalvaje(Perro):
def __init__(self, nombre, raza, edad, territorio):
super().__init__(nombre, raza, edad)
self.territorio = territorio
def sonido(self):
return "Aullido"
Abstracción:
Utilizaremos la abstracción para ocultar los detalles internos de los objetos y centrarnos solo en sus características esenciales.
<python>
perro1 = Perro("Buddy", "Labrador", 3)
perro2 = PerroSalvaje("Rex", "Pastor Alemán", 5, "Bosque")
print(f"{perro1.obtener_nombre()} es un {perro1.obtener_raza()} de {perro1.obtener_edad()} años.")
print(f"{perro2.obtener_nombre()} es un {perro2.obtener_raza()} de {perro2.obtener_edad()} años, que aulla en el {perro2.territorio}.")
Acción y reacción
¿Qué sucede cuando no podemos ver la realidad de las cosas a simple vista? ¿Qué ocurre cuando las cosas son el resultado de acciones previas? Hablamos de diseñar eventos generados por otros eventos. Paradigma Programación Orientada a Eventos es precisamente lo que viene a solucionar. Imaginemos un teclado, un periférico de la vida real con botones, luces y teclas. Cada vez que presionamos una tecla, se produce un evento que podemos modelar y controlar. Esto se puede llevar a otros contextos, como un sistema que genera una salida que se convierte en una entrada para otro sistema.
En medio de esta comunicación, existe un mensaje que se transporta y puede ser cualquier cosa, ya sea un bit, un impulso electrónico o un mensaje de texto. En última instancia, siempre hay un emisor y un receptor.
¿Cuándo se utiliza?
El paradigma de Programación Orientada a Eventos (POE) se utiliza comúnmente en el desarrollo de aplicaciones interactivas y basadas en eventos, como aplicaciones de interfaz gráfica de usuario (GUI) y juegos.
¿Un ejemplo en código?
Eventos en el contexto de una aplicación de interfaz gráfica de usuario utilizando el lenguaje de programación Python y la biblioteca Tkinter:
import tkinter as tk
# Función que se ejecutará cuando se haga clic en el botón
def saludar():
mensaje.config(text="Hola, ¡Has hecho clic en el botón!")
# Crear una ventana
ventana = tk.Tk()
ventana.title("Ejemplo de Programación Orientada a Eventos")
# Crear un botón
boton = tk.Button(ventana, text="Saludar", command=saludar)
boton.pack()
# Crear una etiqueta para mostrar el mensaje
mensaje = tk.Label(ventana, text="")
mensaje.pack()
# Ejecutar el bucle principal de la aplicación
ventana.mainloop()
Ya puedes distinguir cada paradigma y su uso en general. Tal vez ya lo sabías, pero no tenías claro cuándo usarlos. Los paradigmas de programación son como simples recetas que puedes utilizar según te convenga.
Gracias por llegar hasta aquí. Te animo a seguir profundizando y llevar la teoría a la práctica en tu lenguaje de preferencia. ¡Ánimo y un saludo!