DEV Community

Cover image for POO entrando a un nuevo viejo mundo
g-esman
g-esman

Posted on • Edited on

POO entrando a un nuevo viejo mundo

Hola dev, potencial dev o persona curiosa del otro lado! Bienvenido a mi primer Post, en el que hablaremos sobre POO (Programación Orientada a Objetos).

Me llamo Gastón Esman, me desempeño como desarrollador de software hace mas de 10 años. Este blog fue pensado como un espacio para compartir los conocimientos que adquirí durante este tiempo, espero que sea de tu utilidad!

¿Estás listo? Vamos desde el principio:

¿Que es la Programación Orientada a Objetos?
POO es un paradigma de programación que apunta a representar conceptos del mundo real como objetos. Un ejemplo rápido: vos y yo somos individuos, "Personas". Si bien somos únicos, a un nivel de abstracción los dos podemos ser representados por una clase genérica, "Persona". En programación las clases sirven como templetes, o planos que se utilizan para crear objetos. En este caso, del templete de "Persona" podemos obtener tanto un objeto llamado Gastón como el tuyo "Lector".

¿Por qué usar este paradigma?
POO ofrece una manera simple de representar conceptos del mundo real, haciendo que los programas sean mas modulares, reusables y escalables.

Clases y Objetos:
Las clases son planos que definen comportamiento y atributos, mientras que un objeto es una instancia creada tomando ese plano como referencia.

Entonces, volvamos a nuestro ejemplo ¿Qué tenemos en común? Somos seres humanos, poseemos un nombre, una edad, y tenemos la habilidad de realizar acciones, como hablar. Entonces podemos abstraernos de mí, Gastón, y de vos, "lector", y crear el plano o clase "Persona".
Veamos un ejemplo para dejarlo mas claro:

public class Persona
{
    public string Nombre { get; set; }
    public int Edad { get; set; }
    public void Saludar()
    {
        Console.WriteLine($"Hola, me llamo {Nombre} y tengo {Edad} años.");
    }

}

Persona persona1 = new Persona
{
    Nombre = "Gastón",
    Edad = 32
};
persona1.Saludar(); //output Hola, me llamo Gastón y tengo 32 años.

Persona persona2 = new Persona
{
    Nombre = "Lector",
    Edad = 23
};
persona2 .Saludar(); //output Hola, me llamo Lector y tengo 23 años.
Enter fullscreen mode Exit fullscreen mode

En el ejemplo, Persona es el plano o clase. persona1 y persona2 son objetos instanciados de esa clase.
Cada objeto persona posee su nombre y edad propias, llamados atributos. Además, puede realizar acciones, como Saludar. Estas acciones se denominan comportamientos o, técnicamente hablando, métodos.

Encapsulamiento
Se trata de esconder los detalles internos de una clase y solo exponer lo absolutamente necesario al resto del programa.

public class Persona
{
    private int Documento;
    private string nombre;

    public string GetNombre()
    {
        return nombre;
    }

    public void SetNombre(string nuevoNombre)
    {
        if (!string.IsNullOrEmpty(nuevoNombre))
        {
            nombre= nuevoNombre;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

¿Que vemos en el código?:

  • Los atributos Nombre y Documento son privados, por lo que ocultamos esta información al resto del programa, sin poder acceder directamente por fuera de la clase, de esta manera protejo su integridad.
  • Para interactuar con Nombre proveemos un getter (si pretendemos obtener este valor) y un setter (para modificarlo), asegurándonos que tenemos el control sobre los posibles cambios al atributo.
  • Por ejemplo, si alguien quiere dejar sin nombre al objeto "Lector", la validación no se lo permitirá, ya que la única forma es pasar por el setter.

¿Porque es importante?
El Encapsulamiento asegura que el estado interno de un objeto no sea accidental o maliciosamente modificado. Ofrece un código más robusto y fácil de mantener.

Herencia
Este concepto permite que una clase hijo pueda heredar tanto atributos como comportamiento de una clase padre.
El objetivo principal tras este concepto es reducir la redundancia y crear un código más reusable, ahorrando tiempo al no tener que repetir código innecesariamente.

¿Cómo es que funciona?
Clase hija:

  • Hereda atributos y comportamiento de la clase padre.
  • Puede extender a la clase padre añadiendo nuevo comportamiento o atributos.
  • Tiene la capacidad de sobrescribir total o parcialmente el comportamiento heredado, al proveer una implementación especifica.
public class Animal
{
    public string Nombre{ get; set; }

    public virtual void Mover()
    {
        Console.WriteLine("me muevo.");
    }
}

public class Loro : Animal
{
    // nuevo método exclusivo para loro
    public void Hablar(string loQueDijiste)
    {
        Console.WriteLine($"Khaaaa {loQueDijiste} Khaaaa {loQueDijiste}.");
    }

    // Sobrescribir el método Mover
    public override void Mover()
    {
        Console.WriteLine("vuelo.");
    }
}

// Aplicado
Loro poly = new Loro ();
poly.Nombre= "Poly";
poly.Mover(); // Output: vuelo.
poly.Hablar("quiero una galleta"); // Output: Khaaaa quiero una galleta Khaaaa quiero una galleta.
Enter fullscreen mode Exit fullscreen mode

¿Qué pasó acá?
La clase Loro heredó de la clase Animal.
Como resultado, tiene acceso a la propiedad Nombre y al comportamiento de la clase Animal.
Agregamos en la clase Loro un comportamiento propio, el método Hablar.
Sobrescribimos Mover, para darle una implementación más específica: "volar".
¡Por qué es importante esto?
La herencia ayuda a modelar una aplicación de forma jerárquica, logrando que sea más fácil de representar relaciones entre entidades, ahorrándonos tiempo y líneas de código, consiguiendo módulos más compactos.

Polimorfismo
Este concepto provee de gran flexibilidad al código, lo que nos permite trabajar con diferentes tipos de objetos que comparten un mismo padre, sin la necesidad de conocer específicamente a cada clase hija, ya que todos implementan al padre, solo nos tenemos que comunicar con el padre, y los hijos van a saber qué hacer.

Veamos un ejemplo: sigamos con nuestra clase padre Animal, pero esta vez agregaremos una segunda clase hija además de Loro, la clase Salmon, Usando polimorfismo, no tenemos que entrar en el detalle de la implementación de cada hijo en métodos heredados del padre, como Mover. En su lugar, nos enfocamos en las definiciones del padre y confiamos en que cada hijo manejará de forma apropiada su comportamiento específico.

Llevémoslo a código:

public class Animal
{
    public string Nombre { get; set; }
    public virtual void Mover()
    {
        Console.WriteLine($"Me muevo");
    }
}

public class Loro : Animal
{
    public void Hablar(string loQueDijiste)
    {
        Console.WriteLine($"Khaaaa {loQueDijiste} Khaaaa {loQueDijiste}.");
    }

    public override void Mover()
    {
        Console.WriteLine($"Vuelo.");
    }
}

public class Salmon : Animal
{
    public override void Mover()
    {
        Console.WriteLine($"Nado.");
    }
}

public class AnimalHandler
{
    private readonly Animal _animal;

    public AnimalHandler(Animal animal)
    {
        _animal = animal;
    }

    public void MoverAnimal()
    {
        _animal.Mover();
    }
}


public class Program
{
    public static void Main(string[] args)
    {
        AnimalHandler loroHandler = new AnimalHandler(new Loro());
        AnimalHandler salmonHandler = new AnimalHandler(new Salmon());

        loroHandler.MoverAnimal(); // Output: Vuelo.
        salmonHandler.MoverAnimal();  // Output: Nado.
    }
}
Enter fullscreen mode Exit fullscreen mode

¿Qué paso acá?

  • Clase Padre:
    La clase Animal define el método Mover, el cual puede ser sobrescrito por los hijos.

  • Clases Hijas:
    Loro sobrescribe Mover para proveer una implementación más específica, "Volar", Salmon hace lo mismo pero su implementación específica es "Nadar".

Comportamiento del polimorfismo:

  • La clase AnimalHandler trabaja con un objeto del tipo Animal, pero gracias al polimorfismo, puede manejar cualquier clase hija de Animal (Loro y Salmon para nuestro caso). Al método MoverAnimal no le importa si trabaja con un Loro o un Salmon, él llama a Mover, y la implementación correspondiente se ejecuta.

Abstracción
¡Bienvenido a la caja negra! La abstracción permite poner el foco en qué hace un componente en lugar de cómo lo hace. En programación esto significa que podemos definir una clase abstracta o una interfaz que especifique qué mensajes (métodos) comprenderá la clase, sin importar los detalles de las implementaciones internas.

La responsabilidad de la implementación, el cómo, recae en las clases que hereden de estas abstracciones, generando un código más limpio, modular y fácil de mantener.

Interfaces vs Clases abstractas
Ambas son herramientas para definir la estructura de clases potenciales, pero tienen sus propósitos específicos.
Veamos las diferencias:

Interfaces

  • Definición: es un contrato que especifica métodos que una clase tiene obligación de implementar.

  • Características:

  1. No posee atributos ni estado.
  2. Permite herencia múltiple.
  3. Todos los métodos son implícitamente abstractos y deben ser implementados por las clases que la hereden.
  • Caso de uso: cuando solo necesitamos definir un set de acciones sin comportamiento predefinido.

Clase abstracta

  • Definición: es una clase que puede tener métodos abstractos (a ser implementados por las clases hijas) y métodos concretos (con un comportamiento predefinido).

  • Características:

  1. Puede incluir atributos y estados.
  2. No admite herencia múltiple, solo puede heredar una clase abstracta.
  3. Sirve de base para clases que tienen comportamiento en común.
  • Caso de uso: cuando precisamos no solo establecer un contrato, sino predefinir algún tipo de comportamiento.
public abstract class Animal
{
    public string Nombre { get; set; }
    public abstract void Comer()
    {
        Console.WriteLine($"Estoy comiendo");
    }
}
public interface IComunicacion
{
    public void HacerSonido();
}

public interface IMovimiento 
{
    public void Mover();
}

public class Salmon: Animal, IMovimiento , IComunicacion
{
    public void HacerSonido()
    {
        Console.WriteLine($"Glu glu");
    }

    public void Mover()
    {
        Console.WriteLine($"Nadar.");
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        Salmon salmon = new Salmon ();
        salmon.Comer(); // Output: Estoy comiendo
        salmon.Move(); // Output: Nadar.
        salmon.HacerSonido(); // Output: Glu glu.
    }
}

Enter fullscreen mode Exit fullscreen mode

Composición vs Herencia
Mientras que la herencia describe una relación de "es-un", la composición describe una relación de "tiene-un".

Herencia:

  • Establece una relación padre-hijo.
  • Ejemplo: Salmon ES UN animal.

Composición:

  • Describe cómo un objeto es contenido en otro objeto.
  • Crea más flexibilidad en el código y, como resultado, menos acoplamiento comparado con la herencia.
  • Ejemplo: Salmon TIENE UNAS escamas.
public abstract class Animal
{
    public string Nombre { get; set; }
    public abstract void Comer()
    {
        Console.WriteLine($"Comer.");
    }
}

public class Escamas
{
    public string Color { get; set; }
}

public class Salmon : Animal
{
    public Escamas Escamas{ get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Llegamos al final de este posteo. Espero que las explicaciones hayan sido claras, igualmente te invito a dejar tus dudas, sugerencias o ejemplos en los comentarios. Te leo!

Top comments (0)