Los 4 pilares de la Programación Orientada a Objetos

Existen muchos conceptos en programación orientada a objetos, como clases y objetos, sin embargo, en el desarrollo de software con programación orientada a objetos, existen un conjunto de ideas fundamentales que forman los cimientos del desarrollo de software. A estos 4 conceptos que vamos a ver les llamamos los 4 pilares de la programación orientada a objetos.

Esto no quiere decir que fuera de estos 4 pilares no existan otras ideas igual de importantes, sin embargo, estos 4 pilares representan la base de ideas más avanzadas, por lo que es crucial entenderlos.

Estos pilares son: abstracción, encapsulamiento, herencia y polimorfismo.

Nota: Por un tema de comodidad de referencia, coloco aquí la clase que hicimos en la entrada anterior:


public class Carro
{

    public string Marca;

    public int AñoSalidaAlMercado { get; set; }

    public void Acelerar()
    {
    }
}

Abstracción

De acuerdo a la RAE, una de las acepciones de abstraer es:

Hacer caso omiso de algo, o dejarlo a un lado.” Y ofrece como ejemplo: “Centremos la atención en lo esencial abstrayendo DE consideraciones marginales.

El ejemplo dado captura la esencia del concepto de abstraer. Cuando hacemos una abstracción, queremos omitir detalles que no son necesarios para nosotros, y queremos solamente mostrar lo que sí es relevante.

Desde el punto de vista del desarrollo de software, podemos ver que con una clase podemos realizar una abstracción de una entidad del mundo real. Tomemos por ejemplo la clase carro que hicimos, esta tiene la posibilidad de guardar datos relacionados a la marca y al año de salida al mercado del carro, pero, ¿Por qué solamente estas dos informaciones? Un carro del mundo real tiene más propiedades, como el color y el modelo. Sin embargo, debemos preguntarnos, ¿Son estas informaciones relevantes para nuestro software?

Nuestra clase abstrae todo lo que representa un carro, tomando solamente lo que nos interesa, descartando todo lo demás.

Encapsulamiento

Ya sabes que puedes utilizar clases para modelar entidades las cuales son relevantes para tu aplicación, sabes además que puedes guardar datos dentro de objetos, y también ejecutar funcionalidad. La pregunta que debemos hacernos es, ¿Debe cualquiera poder modificar de manera directa estos datos? ¿Debe cualquiera ejecutar cualquier funcionalidad de nuestros objetos en cualquier momento? Normalmente esto no es algo que queremos, nosotros queremos poder controlar la manera en que se asignen los datos, queremos poder controlar quién ve la data interna de nuestros objetos, e incluso quizás queramos controlar la ejecución de funcionalidad de nuestros objetos. Para esto tenemos el concepto de encapsulamiento.

El encapsulamiento nos permite controlar quien puede ver y utilizar los distintos módulos internos de nuestro sistema. En términos de clases, con el encapsulamiento definimos el acceso a los miembros de la clase.

En C# podemos utilizar modificadores de acceso para definir el control de agentes externos a distintas partes de nuestro sistema, como clases, miembros de las clases, interfaces, entre otros. Supongamos que tenemos una variable, llamada velocidad, la cual queremos colocar en nuestra clase Carro, para indicar la velocidad en la cual se desplaza un vehículo en particular. Sin embargo, queremos que solamente dentro de la clase podamos ver y modificar el valor de dicha variable. Esto lo podemos hacer o con un campo o con una propiedad. Hagámoslo con una propiedad:


public class Carro
{

    public string Marca;

    public int AñoSalidaAlMercado { get; set; }

    private int Velocidad { get; set; }

    public void Acelerar()
    {
        Velocidad += 10;
    }

}

Cuando hagamos una instancia de la clase Carro, no podremos acceder al valor de la propiedad Velocidad, ni tampoco podemos alterarlo desde afuera. Lo que sí podemos hacer es utilizar la función acelerar para aumentar el valor de la velocidad en 10 unidades. Esta es una de las ventajas del encapsulamiento: Nos permite controlar la manera en que se va a alterar la data interna de nuestro objeto.

Si quisiéramos que agentes externos puedan ver el valor la propiedad Velocidad, pero que no puedan alterar libremente dicho valor, podemos utilizar la siguiente sintaxis:


public int Velocidad { get; private set; }

Herencia

Compartir código es una importante y crucial característica de cualquier proyecto de software. Compartir código permite ahorrar trabajo cuando queremos hacer un cambio en nuestro sistema; permite que un solo algoritmo pueda procesar distintas clases de entidades; entre otras cosas.

Hay varias maneras de compartir código, una de ellas es utilizando herencia. La herencia es una relación especial entre dos clases, la clase base y la clase derivada, en donde la clase derivada obtiene la habilidad de utilizar ciertas propiedades y funcionalidades de la clase base, incluso pudiendo sustituir funcionalidad de la clase base. La idea es que la clase derivada “hereda” algunas de las características de la clase base.

Podemos ver un ejemplo de la clase Carro. Un carro es un tipo de vehículo, además, queremos procesar otro tipo de vehículos, cada uno con su entidad, como camión. Un carro y un camión comparten el concepto de velocidad, además, ambos tienen la capacidad de acelerar, y ambos tienen la capacidad de ir de reversa, sin embargo, cuando un camión va de reversa, este debe emitir un sonido. Finalmente, un carro debe poder encender la radio. Vamos entonces a modelar esto:


public class Vehículo
{

    public string Marca;

    public int AñoSalidaAlMercado { get; set; }

    public int Velocidad { get; private set; }

    public void Acelerar()
    {
        Velocidad += 10;
    }

    public virtual void Reversa()
    {

        Console.WriteLine("Voy de reversa!");
    }
}

public class Carro: Vehículo
{

   public void EncenderRadio()

   {
       Console.WriteLine("Encendiendo la radio");
   }
}

public class Camión: Vehículo
{
    public override void Reversa()

    {
        base.Reversa();
        Console.WriteLine("BEEP BEEP BEEP!");
    }
}

Vemos que tenemos 3 clases: Vehículo, Carro y Camión. Carro y Camión heredan de la clase Vehículo. La relación de herencia se representa de esta manera:


class Carro: Vehículo

Con esta sintaxis decimos que Carro es una clase derivada de Vehículo.

Vemos además que la función Acelerar está definida en la clase Vehículo, esto hace que todas las clases derivadas pueden hacer uso de dicha función. Lo mismo sucede con los campos y propiedades.

Ciertamente las clases Carro y Camión pueden definir sus propios miembros que no se relacionan con la clase Vehículo. Por ejemplo, la clase Carro tiene el método EncenderRadio el cual solo esta lo tiene.

Podemos también modificar funcionalidad de la clase base. Para esto, en la clase base, el método debe estar marcado como virtual. Y cuando se quiera sobrescribir, es decir, cambiar o agregar funcionalidad, esto lo podemos hacer haciendo un override, tal cual vemos en la clase Camión. Dentro del método Reversa de la clase Camión, tenemos el código base.Reversa(); el cual sirve para invocar el método reversa de la clase base.

Podemos utilizar el código anterior de la siguiente manera:


Carro miCarro = new Carro();

miCarro.AñoSalidaAlMercado = 2018;

miCarro.Acelerar();

Console.WriteLine(miCarro.Velocidad);

miCarro.Reversa();

Console.WriteLine("-------");

Camión miCamion = new Camión();

miCamion.Acelerar();

miCamion.AñoSalidaAlMercado = 2012;

miCamion.Reversa();

Clases Abstractas

¿Qué tal si quisiéramos que la clase Vehículo no pudiera ser instanciada? Podemos marcarla como una clase abstracta. Una clase abstracta es aquella que no puede ser instanciada. Es útil en situaciones de herencia donde no queremos que los usuarios instancien la clase base, sino que queremos que instancien solamente las clases derivadas. Para marcar la clase Vehículo como abstracta utilizamos abstract:


public abstract class Vehículo

¿Qué tal si quisiéramos obligar a las clases derivadas a implementar una función específica, sin que la clase base dé una implementación por defecto? Para esto podemos marcar el método como abstract. Ejemplo:


public abstract void MetodoObligatorio();

Interfaces

Las interfaces nos ayudan a realizar otro tipo de herencia. Mientras que una clase base nos ofrece implementación por defecto de algunos métodos, como el método reversa de la clase Vehículo, las interfaces nos ofrecen un conjunto de miembros que las clases que implementan la interfaz deben implementar. Las interfaces no pueden ser instanciadas, igual que las clases abstractas.

Históricamente, una diferencia fundamental entre interfaces y clases abstractas es que las clases abstractas nos permiten crear implementaciones por defecto de métodos y las interfaces no. Sin embargo, es posible que en C# 8 eso cambie con la introducción de implementaciones por defecto en interfaces.

Nota: Aunque las interfaces son un tipo de herencia, es normal referirse a herencia solamente al caso en el que tenemos una clase base.

Polimorfismo

Cuando empezamos a hablar de herencia, dijimos que la herencia “permite que un solo algoritmo pueda procesar distintas clases de entidades”. La idea es que podemos tener una función la cual reciba un parámetro, como una clase base, y podemos pasarle a ese método objetos que sean instancias de las clases derivadas de dicha clase base. Lo mismo ocurre si el método recibe como parámetro una interfaz. Podemos pasarle a dicho método cualquier clase que implemente dicha interfaz.

Polimorfismo significa de muchas formas. En nuestro caso llamamos polimorfismo cuando un método recibe un parámetro que abarca varios tipos.

Veamos un ejemplo de polimorfismo donde pasamos a un método la clase base Vehículo:


static void Reparar(Vehículo vehículo)

{

    Console.WriteLine("Iniciando reparación");

    Console.WriteLine("Probando acelerador");

    Console.WriteLine($"Velocidad inicial {vehículo.Velocidad}");

    vehículo.Acelerar();

    Console.WriteLine($"Velocidad final {vehículo.Velocidad}");

    Console.WriteLine("Probando reversa");

    vehículo.Reversa();

    Console.WriteLine("Listo!");

}

Este método invoca los métodos Acelerar y Reversa del vehículo que se le envíe como parámetro. La ventaja que esto ofrece es que podemos generalizar algoritmos para que funcionen con distintos tipos. En este caso, este método va a funciona con cualquier clase que herede de Vehículo, en tal sentido, incluso si en el futuro agregamos la clase Motocicleta, la cual hereda de Vehículo, podemos utilizar esta nueva clase con el método Reparar, y va a funcionar perfectamente. De esta manera se da el polimorfismo, pues el método reparar puede trabajar con varios tipos distintos.

En el método reparar no podemos hacer uso del método EncenderRadio de la clase Carro, pues la clase Vehículo no implementa dicho método. Lo que podríamos hacer es utilizar el operador is para castear a Carro en caso de que el Vehículo sea un carro:


if (vehículo is Carro miCarro)

{

    miCarro.EncenderRadio();

}

Esta sintaxis es una manera resumida de decir:


if (vehículo is Carro)

{

    Carro miCarro = vehículo as Carro;

    miCarro.EncenderRadio();

}

Conclusión

En esta entrada vimos los 4 pilares de la programación orientada a objetos: Abstracción, encapsulamiento, herencia y polimorfismo. También hablamos acerca de las clases abstractas e interfaces.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s