DEV Community

Cover image for Explorando los Fundamentos de la Programación Orientada a Objetos en C#
Manuel Dávila
Manuel Dávila

Posted on • Edited on

Explorando los Fundamentos de la Programación Orientada a Objetos en C#

¿Te has encontrado alguna vez con código tan desordenado que parece un rompecabezas imposible de resolver? En el mundo actual, donde dependemos de aplicaciones para casi todo, mantener el código organizado y eficiente es crucial. Aquí es donde entra en juego la Programación Orientada a Objetos (POO). Este paradigma nos ayuda a que nuestros proyectos sean más fáciles de entender, mantener y expandir.

Es cierto que incluso con POO, el código puede volverse desordenado, especialmente en sistemas legacy que han evolucionado sin una dirección clara. Sin embargo, al adoptar buenas prácticas de POO, podemos minimizar estos problemas.

Imagen de perro de caricatura en incendio

En este artículo, vamos a desglosar los conceptos básicos de la POO en C#, explicando por qué es tan valiosa y cómo puedes usarla en tus propios proyectos.

¿Qué es la Programación Orientada a Objetos?

La Programación Orientada a Objetos (POO) es un paradigma de programación que utiliza "objetos" para representar cosas del mundo real. ¿Cómo funciona? Imagina que tienes una aplicación y quieres crear algo que represente un "Personaje" en un juego. Ese personaje puede tener atributos como salud y fuerza, y métodos para atacar o defenderse. Así, en lugar de escribir todo de una manera desordenada, puedes organizar tu código en objetos que son fáciles de manejar.

Probablemente, alguna vez te pasó que el código que hiciste hace una semana ya no lo recuerdas 😅, no lo podemos evitar pero podemos hacerlo más fácil de manejar con POO. Alguno de sus beneficios:

  • Organiza el código de una forma más entendible, haciendo que sea más fácil trabajar en equipo o volver al proyecto después de un tiempo.
  • Facilita la actualización y mejora del software, ya que puedes agregar nuevas funcionalidades sin romper todo lo que ya funciona.
  • Promueve buenas prácticas de programación como el encapsulamiento, la herencia y el polimorfismo, lo que se traduce en un código más limpio y reutilizable.

¡Ojo! La POO no es la solución mágica. Incluso con este enfoque, es posible que el código se vuelva un quilombo, sobre todo en sistemas antiguos o mal diseñados. Por eso, es clave seguir buenas prácticas y mantener el código lo más prolijo posible.

Persona con una varita mágica

Entender bien los conceptos de la POO es fundamental para evitar los errores comunes y sacar el máximo provecho de este paradigma. No te olvides de practicar y de leer código de otros desarrolladores para ver cómo aplican estos conceptos en situaciones reales.

Clases y Objetos en C#

En la POO, las clases son como los planos o moldes para crear objetos. Imagina una clase como una receta de cocina que define qué ingredientes se necesitan y cómo combinarlos para hacer un plato. En nuestro caso, la "receta" es una clase, y los "platos" que creamos a partir de esa receta son los objetos.

Clase

Una clase en C# define un tipo de datos y especifica qué propiedades y métodos tendrá. Pensemos en un videojuego: tienes varios personajes, y cada uno tiene características y habilidades diferentes. Para manejar esto, podríamos crear una clase llamada Personaje que sirva como base para todos los personajes del juego, con propiedades como Nombre, Vida, Fuerza, y métodos como Atacar() y Defender().

public class Personaje
{
    public string Nombre { get; set; }
    public int Vida { get; set; }
    public int Fuerza { get; set; }

    public void Atacar()
    {
        Console.WriteLine($"{Nombre} ataca con una fuerza de {Fuerza}.");
    }

    public void Defender()
    {
        Console.WriteLine($"{Nombre} se defiende, reduciendo el daño.");
    }
}
Enter fullscreen mode Exit fullscreen mode

Imagen de un maniqui para referenciar a un molde o plantilla

Objetos

Un objeto es una instancia concreta de una clase. Imagina que la clase Personaje es el diseño de un personaje; un objeto sería un personaje específico dentro del juego, como un guerrero o un mago, cada uno con sus propios valores para Nombre, Vida, y Fuerza.

Personaje guerrero = new Personaje();
guerrero.Nombre = "Thorin";
guerrero.Vida = 150;
guerrero.Fuerza = 75;
guerrero.Atacar(); // Thorin ataca con una fuerza de 75.
Enter fullscreen mode Exit fullscreen mode

En este ejemplo, guerrero es un objeto de la clase Personaje que representa a un personaje específico en el videojuego. Cada objeto puede tener valores diferentes para las mismas propiedades, permitiendo una gran flexibilidad y personalización en el juego.

Propiedades

Las propiedades en C# actúan como características del objeto para almacenar datos. Funcionan como variables, pero con un mayor control sobre cómo se asignan y recuperan los valores. En el caso del videojuego, las propiedades de la clase Personaje incluyen atributos como Nombre, Vida y Fuerza.

public class Personaje
{
    public string Nombre { get; set; }
    public int Vida { get; set; }
    public int Fuerza { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Estas propiedades permiten acceder y modificar los atributos del personaje de manera controlada. Por ejemplo, si quieres ajustar la vida de un personaje cuando recibe daño, simplemente actualizás la propiedad Vida.

Métodos

Los métodos son funciones definidas dentro de una clase que permiten la comunicación entre objetos. En nuestro videojuego, los métodos Atacar() y Defender() definen cómo los personajes interactúan entre sí.

public class Personaje
{
    public void Atacar()
    {
        Console.WriteLine($"{Nombre} ataca con una fuerza de {Fuerza}.");
    }

    public void Defender()
    {
        Console.WriteLine($"{Nombre} se defiende, reduciendo el daño.");
    }
}
Enter fullscreen mode Exit fullscreen mode

Ahora imaginamos que queremos que nuestro personaje Thorin ataque y luego se defienda. Utilizaríamos los métodos de la siguiente manera:

Personaje thorin = new Personaje();
thorin.Nombre = "Thorin";
thorin.Vida = 150;
thorin.Fuerza = 75;

thorin.Atacar(); // Thorin ataca con una fuerza de 75.
thorin.Defender(); // Thorin se defiende, reduciendo el daño.
Enter fullscreen mode Exit fullscreen mode

[Imagen de un personaje en accion]

Constructores

Un constructor es un método especial en una clase que se llama automáticamente cuando se crea un nuevo objeto de esa clase. Su propósito principal es inicializar los atributos del objeto. En C#, un constructor tiene el mismo nombre que la clase y no tiene un tipo de retorno.

En este ejemplo, el constructor Personaje(string nombre) se utiliza para inicializar el nombre del personaje y establecer valores predeterminados para la vida y la fuerza:

public class Personaje
{
    public string Nombre { get; set; }
    public int Vida { get; set; }
    public int Fuerza { get; set; }

    // Constructor
    public Personaje(string nombre)
    {
        Nombre = nombre;
        Vida = 100;
        Fuerza = 50;
    }
}
Enter fullscreen mode Exit fullscreen mode

Cada vez que utilizas "new" haces uso del constructor para crear un objeto.

Imagen de una fabrica de construcción de maniquis para referenciar a la instancia de un clase

Sobrecarga

La sobrecarga de métodos, incluido el constructor, permite definir múltiples métodos con el mismo nombre pero con diferentes parámetros. Esto es útil para proporcionar diversas maneras de inicializar un objeto.

Por ejemplo, podríamos tener varios constructores para Personaje que permitan diferentes niveles de detalle en la creación de un personaje:

public class Personaje
{
    public string Nombre { get; set; }
    public int Vida { get; set; }
    public int Fuerza { get; set; }

    // Constructor 1: Sólo nombre
    public Personaje(string nombre)
    {
        Nombre = nombre;
        Vida = 100;
        Fuerza = 50;
    }

    // Constructor 2: Nombre y vida
    public Personaje(string nombre, int vida)
    {
        Nombre = nombre;
        Vida = vida;
        Fuerza = 50;
    }

    // Constructor 3: Nombre, vida y fuerza
    public Personaje(string nombre, int vida, int fuerza)
    {
        Nombre = nombre;
        Vida = vida;
        Fuerza = fuerza;
    }
}
Enter fullscreen mode Exit fullscreen mode

Aquí, hemos definido tres constructores diferentes para Personaje, permitiendo crear objetos con solo el nombre, con nombre y vida, o con nombre, vida y fuerza. Esto da flexibilidad para inicializar objetos según las necesidades del juego.

La sobrecarga también se puede utilizar en los métodos de la clase, no sólo en constructores.

Principios fundamentales de la POO

La Programación Orientada a Objetos se basa en cuatro principios clave que ayudan a construir software más modular, reutilizable y fácil de mantener. Estos principios son Encapsulamiento, Herencia, Polimorfismo, y Abstracción. Vamos a ver de qué se tratan con ejemplos aplicados al desarrollo de un videojuego.

Encapsulamiento

El encapsulamiento consiste en ocultar los detalles internos de un objeto y exponer sólo lo necesario. Esto se logra mediante el uso de propiedades y métodos públicos para interactuar con el objeto, mientras que los detalles internos se mantienen privados.

En el contexto de un videojuego, el encapsulamiento asegura que los atributos de un personaje, como su vida o fuerza, no sean modificados directamente desde fuera de la clase. En cambio, se utilizan métodos para alterar estos atributos, lo que permite agregar lógica adicional, como validaciones.

public class Personaje
{
    private int vida;

    public int Defensa { get; set; }

    public int Vida
    {
        get { return vida; }
        set
        {
            if (value < 0)
                vida = 0;
            else
                vida = value;
        }
    }

    public void RecibirDano(int dano)
    {
        var danoFinal = dano - Defensa;
        Vida = danoFinal <= 0 ? Vida : Vida - danoFinal;
    }
}
Enter fullscreen mode Exit fullscreen mode

En este ejemplo, el atributo vida es privado, y se accede a él mediante la propiedad Vida. El método RecibirDano() controla cómo se reduce la vida del personaje, asegurando que no baje de cero.

El encapsulamiento no sólo protege los datos de cambios no deseados, sino que también facilita el mantenimiento del código al centralizar la lógica relacionada.

Herencia

La herencia permite crear una nueva clase que reutiliza, extiende o modifica el comportamiento de otra clase. Es como crear una plantilla general y luego hacer variantes de ella para casos específicos.

En nuestro videojuego, podemos tener una clase base Personaje y luego derivar clases como Guerrero o Mago que heredan atributos y métodos de Personaje, pero con características adicionales.

public class Personaje
{
    public string Nombre { get; set; }
    public int Vida { get; set; }

    public void Atacar()
    {
        Console.WriteLine($"{Nombre} ataca.");
    }
}

public class Guerrero : Personaje
{
    public int Fuerza { get; set; }

    public void AtacarConEspada()
    {
        Console.WriteLine($"{Nombre} ataca con una espada con fuerza de {Fuerza}.");
    }
}

public class Mago : Personaje
{
    public int Mana { get; set; }

    public void LanzarHechizo()
    {
        Console.WriteLine($"{Nombre} lanza un hechizo utilizando {Mana} de mana.");
    }
}
Enter fullscreen mode Exit fullscreen mode

Aquí, Guerrero y Mago heredan de Personaje, compartiendo propiedades comunes como "Nombre" y "Vida", pero también añadiendo su propia funcionalidad específica.

La herencia debe usarse con cuidado, ya que puede llevar a una estructura de clases compleja y difícil de mantener si se abusa de ella.

Polimorfismo

El polimorfismo permite a los objetos de diferentes clases ser tratados como objetos de una clase base común. En términos simples, permite usar un método de la misma manera para diferentes tipos de objetos.

public void RealizarAccion(Personaje personaje)
{
    personaje.Atacar();
}

Guerrero guerrero = new Guerrero { Nombre = "Thorin", Fuerza = 100 };
Mago mago = new Mago { Nombre = "Gandalf", Mana = 200 };

RealizarAccion(guerrero); // Thorin ataca con una espada con fuerza de 100.
RealizarAccion(mago); // Gandalf lanza un hechizo utilizando 200 de mana.
Enter fullscreen mode Exit fullscreen mode

En este ejemplo, aunque RealizarAccion llama al mismo método "Atacar" en ambos casos, la implementación específica de cada clase derivada es la que se ejecuta.

Abstracción

La abstracción consiste en simplificar la complejidad del sistema al mostrar sólo los detalles esenciales y ocultar los innecesarios. En POO, esto se logra mediante clases abstractas y interfaces, que definen una estructura común sin implementar todos los detalles.

Imaginemos que queremos definir una interfaz IPoder para habilidades especiales que todos los personajes deben tener, pero cada personaje puede implementar estas habilidades de manera diferente.

public interface IPoder
{
    void UsarPoder();
}

public class Guerrero : Personaje, IPoder
{
    public void UsarPoder()
    {
        Console.WriteLine($"{Nombre} usa su poder especial: Golpe Feroz.");
    }
}

public class Mago : Personaje, IPoder
{
    public void UsarPoder()
    {
        Console.WriteLine($"{Nombre} usa su poder especial: Llamas Místicas.");
    }
}
Enter fullscreen mode Exit fullscreen mode

En este ejemplo, Guerrero y Mago implementan la interfaz IPoder, cada uno proporcionando su propia versión del método "UsarPoder".

Podrías realizar el mismo ejemplo utilizando una clase abstracta:

public abstract Poder
{
    void UsarPoder();
}

public class Guerrero : Poder
{
    public void UsarPoder()
    {
        Console.WriteLine($"{Nombre} usa su poder especial: Golpe Feroz.");
    }
}

public class Mago : Poder
{
    public void UsarPoder()
    {
        Console.WriteLine($"{Nombre} usa su poder especial: Llamas Místicas.");
    }
}
Enter fullscreen mode Exit fullscreen mode

El problema es que al ser una clase, no podrás heredera de ninguna otra. Eso a su vez genera un alto acoplamiento. Es por eso que se recomienda utilizar una interface sobre las clases abstractas.

Cohesión y Acoplamiento

En la Programación Orientada a Objetos, dos conceptos clave para escribir código limpio y manejable son la cohesión y el acoplamiento.

  • Cohesión se refiere a qué tan bien se agrupan las responsabilidades dentro de una clase. Una clase con alta cohesión tiene un propósito claro y realiza un conjunto específico de tareas. Por ejemplo, en un videojuego, una clase Inventario debería manejar todo lo relacionado con los ítems del jugador, y no encargarse de las habilidades o el estado del personaje
  • Acoplamiento es la medida en que una clase depende de otras. El bajo acoplamiento es deseable porque permite que los cambios en una clase no afecten otras. Imaginemos que tenemos una clase Arma y una clase Personaje. Si Personaje depende demasiado de la estructura interna de Arma, cualquier cambio en Arma podría requerir modificaciones en Personaje. Mantener bajo acoplamiento facilita la reutilización de código y mejora la mantenibilidad.

Al diseñar tus clases, trata de que cada una tenga una única responsabilidad clara (alta cohesión) y minimiza las dependencias entre ellas (bajo acoplamiento). Esto hará que tu código sea más fácil de entender, mantener y expandir.

Una vez que domines estos conceptos podrás mejorar tu código aún más con los principios SOLID y patrones de diseño. 😎

Espero que este post te haya sido de utilidad para entender mejor la POO en C#. Aplica esto en proyectos personales y no compliques el diseño de clases innecesariamente; a veces, la solución más simple es la mejor.

!Happing coding! ☕

Top comments (0)