Principios esenciales de programación para un mejor software

ago. 28, 2023

En el vasto universo de la programación existen principios que, aunque no sean mandamientos, son una gran orientación para aquellos que deseen escribir código limpio, mantenible y eficiente. Estos principios no sólo representan buenas prácticas sino que también son el fruto de años de experiencia y aprendizaje de la comunidad de desarrolladores. A continuación, profundizaremos en algunos de estos principios.

DRY (No te repitas)

La repetición es la ruina del programador.

Esta frase encapsula la esencia del principio DRY - Don’t Repeat Yourself (No te repitas), busca reducir duplicidades en nuestro código ya que la repetición conduce a errores, ineficiencias y dificultades en la creación de software.

Motivación

  • Mantenimiento: Si una lógica está repetida en múltiples lugares, cualquier cambio o corrección en esa lógica debe hacerse en todos esos lugares. Esto no sólo consume más tiempo sino que también aumenta la posibilidad de errores.
  • Eficiencia: La reutilización del código conduce a programas más pequeños, ya que se eliminan redundancias.
  • Legibilidad: Un código sin repeticiones es más fácil de leer y entender, las repeticiones pueden distraer y confundir al lector.

Las violaciones comunes del principio DRY incluyen prácticas como copiar y pegar código de manera reiterada, implementar funciones o métodos que ejecutan operaciones muy parecidas o idénticas y diseñar clases con estructuras que presentan similitudes notorias.

Ejemplo


    # Wrong approach
    def calculate_rectangle_area(width, height):
        return width * height
    
    def calculate_circle_area(radius):
        return 3.14 * radius * radius
    
    # Using DRY Principle
    class Shape:
        def area(self):
            pass
    
    class Rectangle(Shape):
        def __init__(self, width, height):
            self.width = width
            self.height = height
    
        def area(self):
            return self.width * self.height
    
    class Circle(Shape):
        def __init__(self, radius):
            self.radius = radius
    
        def area(self):
            return 3.14 * self.radius * self.radius
            

Consejo Práctico

Abstraer para evitar repetición es esencial pero no querrás complicar más las cosas, antes de abstraer pregúntate si la abstracción complica el código o si sigue siendo legible. En ocasiones, un poco de repetición es mejor si facilita la comprensión.

KISS (Mantenlo simple, estúpido)

Lo simple es mejor que lo complejo, y lo complejo es mejor que lo complicado.

Esta es una variante de la filosofía detrás de KISS - Keep It Simple Stupid (Mantenlo simple estúpido), un principio que promueve la simplicidad en diseño y código por encima de soluciones complejas y rebuscadas. La simplicidad es un objetivo deseable en el desarrollo de software ya que conduce a un código más limpio, fácil de entender y mantener.

Motivación

  • Facilidad de mantenimiento: Códigos simples son más fáciles de mantener. Las correcciones, mejoras o extensiones se pueden realizar con menos esfuerzo y riesgo de introducir errores.
  • Legibilidad: El código simple es más fácil de leer y entender, lo que permite que otros desarrolladores, o incluso uno mismo en el futuro, puedan familiarizarse rápidamente con él.
  • Reducido potencial de errores: Menos complejidad generalmente significa menos posibilidades de errores. Las soluciones simples suelen tener menos casos extremos y excepciones a considerar.

La clave para implementar KISS es la reflexión y la autoevaluación constante. Siempre que estés por escribir o diseñar algo pregúntate: “¿Hay una forma más simple de hacer esto?”.

Ejemplo

Supongamos que queremos filtrar números pares de una lista:


    # Aproximación exagerada
    def rectangle_area(width, height):
        return width * height

    def circle_area(radius):
        return 3.14159 * radius * radius

    # Y así para cada forma...
    

Una solución más simple y escalable es usar clases y polimorfismo:


    # Usando el principio KISS
    class Shape:
        def area(self):
            pass

    class Rectangle(Shape):
        def __init__(self, width, height):
            self.width = width
            self.height = height

        def area(self):
            return self.width * self.height

    class Circle(Shape):
        def __init__(self, radius):
            self.radius = radius

        def area(self):
            return 3.14159 * self.radius * self.radius
            

Con este enfoque basado en clases tienes una estructura más limpia que permite añadir nuevas figuras sin necesidad de crear una nueva función para cada una. Es una simplificación en diseño más que una reducción en la cantidad de código.

Consejo Práctico

No te dejes seducir por la elegancia técnica o soluciones inteligentes. A menudo el método más directo y sencillo es el mejor. Al enfrentarte a un problema, antes de sumergirte en el código, toma un momento para pensar y planificar. Recuerda que lo que parece simple y claro para ti ahora, podría no serlo para alguien más (o incluso para ti mismo) en el futuro.

YAGNI (No Lo Vas a Necesitar)

El mejor código es el código que nunca escribes.

YAGNI, que significa No Lo Vas a Necesitar por sus siglás en Inglés de You aren’t gonna need it, es un principio que anima a los desarrolladores a implementar funcionalidades sólo cuando son necesarias. Advierte contra la sobredimensión o añadir características basadas en especulaciones sobre necesidades futuras.

Motivación

  • Simplicidad: Al evitar optimizaciones prematuras o características innecesarias te aseguras de que tu base de código permanezca limpia y al grano.
  • Eficiencia: Escribir sólo lo que es necesario significa menos código para probar, depurar y mantener, lo que ahorra tiempo y recursos.
  • Reducción de desperdicios: Las características innecesarias o soluciones demasiado complicadas no sólo inflan el software, sino que también pueden nunca llegar a usarse, convirtiéndolas en un desperdicio de esfuerzo.

Muchos desarrolladores caen en la trampa de anticipar cada posible requerimiento futuro, lo que lleva a soluciones sobredimensionadas y demasiado complejas. YAGNI sugiere que sólo deberías añadir funcionalidad cuando hay un requisito definitivo para ello.

Ejemplo

Supongamos que estás creando una plataforma de blogs. Aunque un sistema de etiquetas o una búsqueda avanzada podrían ser útiles en el futuro, si los requisitos actuales sólo especifican características básicas de blogs, YAGNI aconsejaría no implementarlas desde el principio.


  # Enfoque Sobredimensionado
  class Blog:
    def __init__(self):
        self.posts = []
        self.tags = []
        self.advanced_search = []

  # Principio YAGNI
  class Blog:
    def __init__(self):
        self.posts = []
        

Consejo Práctico

Siempre prioriza los requisitos actuales y concretos sobre los especulativos. Al diseñar o codificar, pregúntate: “¿Necesito esto ahora? ¿Existe un requisito claro para esto?” Si la respuesta es “no” o “no estoy seguro”, considera posponer esa característica o funcionalidad. Evita la tentación de añadir código “por si acaso”. Recuerda, siempre puedes añadirlo más tarde cuando sea realmente necesario.

Separación de intereses

Divide y vencerás.

Este viejo adagio tiene un profundo significado en el diseño de software y la Separación de Intereses es su estandarte. Este principio nos insta a descomponer un programa en piezas distintas donde cada una tenga una responsabilidad única y claramente definida.

Motivación

  • Mantenibilidad: Facilita las actualizaciones y correcciones. Si cada pieza del software tiene un propósito único, modificar una función no debería afectar otras áreas del programa.
  • Legibilidad y organización: Al mantener responsabilidades separadas el código resulta más fácil de leer y de navegar, haciendo que tanto la incorporación de nuevos miembros al equipo como el trabajo colaborativo sean más sencillos.
  • Reusabilidad: Los componentes individuales, al ser autónomos, se pueden reutilizar en diferentes partes del proyecto o incluso en otros proyectos.
  • Testeabilidad: Es más fácil y rápido testear una funcionalidad específica cuando está aislada de otras, esto lleva a pruebas más robustas y a una identificación más rápida de errores.

Siempre tener en mente la singularidad de responsabilidad y pensar en el software en términos de módulos o componentes autónomos. Si una función o clase tiene más de una responsabilidad probablemente se deba dividir en piezas más pequeñas. No es solo una buena práctica, es una filosofía de diseño que debería ser una prioridad en cualquier proyecto de software, ya que mejora la calidad, eficiencia y escalabilidad del código.

Ejemplo

Imagina una aplicación web que involucra una base de datos, una interfaz de usuario y un servidor:


    # Aproximación incorrecta
    class App:
        def get_user_data(self, user_id):
            # Fetch from database
            pass
        def render_user_page(self, user_data):
            # Render user page
            pass
        def handle_request(self, request):
            # Handle web request
            pass
    
    # Aproximación correcta
    class Database:
        def get_user_data(self, user_id):
            # Fetch from database
            pass
    
    class UserInterface:
        def render_user_page(self, user_data):
            # Render user page
            pass
    
    class Server:
        def handle_request(self, request, db, ui):
            user_data = db.get_user_data(request.user_id)
            ui.render_user_page(user_data)
            

Consejo práctico

Imagina que estás resolviendo un rompecabezas, si mezclas las piezas de diferentes rompecabezas se tornará extremadamente difícil, si no imposible, completar la imagen. Del mismo modo, en el desarrollo de software, cada componente o función debe ser como una pieza única de un rompecabezas, con un propósito y forma definidos. Mantén tus componentes y funciones enfocados en una sola tarea o responsabilidad. Al hacerlo, no solo te será más fácil identificar y resolver problemas, sino que también podrás reutilizar, probar y entender tu código con mucha más facilidad. Cuando un componente o función comienza a manejar múltiples tareas, es hora de hacer una pausa, reevaluar y, probablemente, dividirlo.

Ley de Deméter

La Ley de Deméter, a menudo referida como el Principio de menor conocimiento, tiene sus raíces en la idea de reducir las dependencias entre módulos o clases en un software. Esencialmente, sugiere que cada unidad debería tener un conocimiento limitado sobre otras unidades y sólo debería interactuar con sus “amigos cercanos”.

Motivación

  • Acoplamiento Reducido: Menos interdependencia entre clases significa cambios más sencillos y menos propensos a errores involuntarios.
  • Mayor Mantenibilidad: Con menos enlaces entre clases, es más fácil realizar cambios en el código sin temor a efectos secundarios no deseados.
  • Mayor Modularidad: Facilita la reutilización de código, las clases son más autónomas.
  • Testeabilidad: Las clases menos acopladas son más fáciles de probar al tener menos dependencias externas.

La Ley de Deméter se ve incumplida cuando se navega a través de múltiples objetos accediendo a métodos en cadena, como a.b().c(), cuando se inicializan objetos en una clase que deberían ser inyectados como dependencias o cuando una clase interactúa con varias otras para obtener datos o ejecutar funciones. Los beneficios en términos de claridad y estructura del código son invaluables, como con todos los principios de diseño, la clave está en aplicarlo con juicio y equilibrio.

Ejemplo

Imaginemos un caso donde un conductor quiere encender un coche:


    # Aproximación incorrecta
    class Engine:
        class SparkPlug:
            def ignite(self):
                pass
    
    class Car:
        def __init__(self):
            self.engine = Engine()
    
        def start(self):
            self.engine.SparkPlug().ignite()
    
    # Aproximación correcta
    class Engine:
        def start(self):
            spark_plug = SparkPlug()
            spark_plug.ignite()
    
    class SparkPlug:
        def ignite(self):
            pass
    
    class Car:
        def __init__(self):
            self.engine = Engine()
    
        def start(self):
            self.engine.start()
            

En el enfoque correcto, Car no necesita conocer la existencia de SparkPlug. Simplemente sabe que el motor tiene un método start(). La implementación interna de cómo el motor arranca queda encapsulada dentro de la clase Engine.

Consejo práctico

Piensa en la Ley de Deméter como un manual de instrucciones para un dispositivo, si necesitas realizar una función específica en éste el manual te dirige a un botón o comando directo para realizar esa acción. No te dice que presiones un botón que luego te lleve a otro panel, donde debes girar una perilla para luego llevarte a otro lugar donde finalmente realizas la función. En la programación este principio nos sugiere interactuar directamente con los componentes que necesitamos, en lugar de encadenar múltiples acciones a través de diferentes módulos.

Principio de mínima sorpresa

Los usuarios, tanto desarrolladores como finales, esperan que los componentes, funciones o módulos del software funcionen de una manera específica. El principio de la mínima sorpresa insta a que el comportamiento de cualquier pieza de software no debería sorprender o confundir a quien lo utiliza, debería ser claro, coherente y predecible.

Cuando un usuario interactúa con un sistema ya tiene ciertas expectativas sobre cómo debería funcionar basándose en sus experiencias pasadas o en convenciones establecidas. Una aplicación se comporta de una manera que contradice esas expectativas puede causar frustración y conducir a errores.

Motivación

  • Intuitividad: Cuando un software se comporta según las expectativas los usuarios pueden interactuar con él de manera más intuitiva, sin tener que gastar tiempo y energía tratando de entender por qué ciertas cosas suceden.
  • Confianza: Un comportamiento predecible fortalece la confianza del usuario en el software. Si un usuario sabe que el software se comportará de una manera consistente y sin sorpresas, es más probable que confíe en usarlo para tareas críticas.
  • Eficiencia en el desarrollo: Reducir las sorpresas significa que los desarrolladores pasan menos tiempo resolviendo problemas inesperados o explicando características no convencionales. Esto permite un proceso de desarrollo más fluido y eficiente.
  • Adopción del usuario: Los usuarios tienden a adoptar y recomendar software que “simplemente funciona” según sus expectativas. Esto puede llevar a una mayor satisfacción del cliente y a una adopción más amplia del producto.

Ejemplo

En muchos sistemas, hacer clic en la “X” de una de las esquinas superiores de una ventana la cierra. Si, en cambio, al hacer clic en esa “X” se abriera una nueva ventana o se eliminara un archivo, esto iría en contra de la expectativa del usuario y violaría el principio de la mínima sorpresa.


    # Wrong
    def saveFile(file):
        # logic to save the file
        sendEmail()
        deleteAnotherFile()


    # Right
    def saveFile(file):
        # logic to save the file
        pass
        

En el segundo ejemplo, además de guardar el archivo se llevan a cabo otras dos operaciones que no se esperarían basándose en el nombre de la función. Estas acciones adicionales son inesperadas y podrían ser perjudiciales especialmente si el usuario no es consciente de ellas.

Consejo práctico

Imagina que estás leyendo un libro y, sin previo aviso, en medio de una historia de detectives, aparece un dragón lanzando fuego. Esa súbita transición genera sorpresa y confusión. Lo mismo ocurre en el desarrollo de software, si estás diseñando una función o componente hazlo de tal forma que sus acciones sean intuitivas y esperadas.
Mantén la coherencia y evita las “sorpresas” inesperadas en tu diseño, la claridad y previsibilidad son esenciales para un software robusto y confiable.

Fallar rápidamente

El principio Fail-Fast (fallar rápidamente) sugiere que un sistema o aplicación debe identificar condiciones de error o anomalías tan pronto como ocurran, detener su operación y notificar el problema de inmediato. La idea es que es mucho mejor detectar problemas temprano y tratarlos de inmediato, en lugar de permitir que un sistema continúe funcionando en un estado potencialmente inestable.

Este principio es fundamental en muchos aspectos de la programación, desde el diseño de algoritmos hasta el desarrollo de sistemas completos. Al detectar y manejar errores tan pronto como suceden, se pueden evitar consecuencias indeseadas más adelante en el proceso, como cálculos incorrectos, corrupción de datos o incluso fallos en el sistema.

Aplicar este principio no solo ayuda a construir sistemas más robustos y confiables sino que también facilita la detección y corrección de errores durante las fases de desarrollo y prueba. Esto puede ahorrar tiempo y recursos significativos en comparación con la corrección de problemas después de que un producto se haya lanzado o implementado.

Además, al proporcionar retroalimentación inmediata sobre problemas, se mejora la experiencia del usuario o del desarrollador, ya que pueden abordar y corregir problemas con prontitud.

Motivación

  • Prevención de riesgos: Fallar rápidamente evita que los errores se propaguen a través del sistema, previniendo posibles daños mayores o corrupción de datos. Un sistema que falla tarde podría tener repercusiones en muchas otras áreas, complicando la identificación y corrección del problema original.
  • Confianza en el sistema: Un sistema que notifica errores de inmediato es percibido como transparente. Los usuarios y desarrolladores pueden confiar más en un sistema que les informa inmediatamente sobre problemas, en lugar de uno que podría estar ocultando o ignorando fallos.
  • Mejora Continua: El principio de fallar rápidamente no solo se trata de detectar errores, sino también de aprender de ellos. Cuando los errores se detectan y se abordan prontamente, se crea una cultura de mejora continua, donde los errores se ven como oportunidades de aprendizaje y no solo como fallos a corregir.

Ejemplo

Imagina una función que realiza una operación de división. En lugar de permitir que la operación continúe con un divisor de cero (lo que resultaría en un error), es mejor verificar esta condición y generar un error de inmediato, informando al usuario o desarrollador sobre el problema.


    # Incorrecto
    def divide_incorrect(a, b):
        if b == 0:
            return 0  # Retorna un valor por defecto sin notificar el error
        return a / b
    
    # Correcto
    def divide(a, b):
        if b == 0:
            raise ValueError("El divisor no puede ser cero.")
        return a / b
        

Consejo práctico

Imagina que estás vertiendo agua en un colador creyendo que es un vaso, cuanto antes te des cuenta del error menos agua desperdiciarás. De igual manera, en el desarrollo de software, cuando algo va mal, es vital darse cuenta y actuar al instante. Si tu código detecta una condición anómala o un error potencial, no lo ignores ni lo ocultes; haz que tu sistema lo señale de inmediato. Adopta una mentalidad de fallar rápido para que tus programas te informen y no al contrario. Un sistema que falla rápidamente y de manera informativa es un sistema que se preocupa por su integridad y la de sus usuarios.

Resumen

A través de los años y la experiencia acumulada en el campo del desarrollo se han establecido una serie de principios y prácticas que actúan como guías para crear código de alta calidad. No se trata de simples reglas o estándares a seguir, sino la destilación del conocimiento colectivo para diseñar e implementar software robusto, mantenible y efectivo.

Aplicar estos principios refleja el compromiso del desarrollador de producir software que no sólo funcione, sino que también sea duradero y valioso para sus usuarios. En el camino hacia el dominio de la programación estos principios son las balizas que nos guían. Sin embargo, es crucial recordar que una aplicación excesiva o rígida de estos puede ser contraproducente.

Artículos relacionados

Quizá te puedan interesar