sábado, 21 de octubre de 2017

Curso de C#: clases

Clases

Una clase es la plantilla a partir de la cual es posible crear objetos. Todos los objetos de la misma clase comparten la interface (es decir, métodos, campos y propiedades), pero los datos que contiene cada objeto en sus campos y propiedades pueden diferir.
Por su misión específica,  las clases (y las estructuras) se pueden dividir en tres grandes grupos:

Clases que ofrecen un valor

Son aquellas en las que lo principal es el valor que contienen. Por ejemplo, para pagar un artículo de 50 €. Es posible usar un billete de 50 u otro igual, también de 50. O incluso cinco de 10 € Lo importante es pagar los 50 €, pero no los billetes que utilicemos. Los objetos cuyo único interés es su valor son intercambiables, y suelen estar implementados como estructuras (Int32, Int16, Double...).


Curso de  C#: clases

Clases que ofrecen un servicio

Aquí estarían las clases cuyo único interés es el servicio que ofrecen. Por ejemplo, para comprar el pan, lo más importante es que alguien nos lo venda, pero ese quién nos da igual. Nos interesa el servicio, pero no quién haga ese servicio. Un ejemplo de clase que ofrece un servicio es la clase Console, pues no se usan nunca instancias de ella (de hecho, no se puede instanciar), sino que, usamos sus métodos static para aprovechar los servicios que nos ofrece.

Clases de identidad

Aquí, lo más relevante no es la información que contiene ni el servicio que prestan, sino cuál es el objeto que nos ofrece sus datos y servicios. Por ejemplo, en una factura, lo más importante es la factura en sí. No es lo mismo la factura del gas del mes pasado que la de este mes, o la del teléfono de hace dos años. Por esto decimos que son tipos de identidad, porque lo más relevante es el objeto en sí.
Puede haber clases que, según los miembros que se examinen, se incluyan en dos grupos, o, incluso, en los tres. Por ejemplo, una clase tipo TarjetaDeCrédito sería, claramente, un tipo de identidad. Pero también  ofrece un servicio que no depende de la identidad del objeto, como calcular la tasa anual que cobra una determinada entidad por ella.

Herencia

Gracias a la herencia podemos definir clases nuevas basadas en clases antiguas, añadiéndoles más datos o funcionalidad. La relación de herencia debe basarse en una relación jerárquica de conjuntos y subconjuntos más pequeños incluidos en aquellos. Algunas de las clases derivadas pueden añadir algo específico de ella que no tuviera sentido en otro subconjunto distinto. Tomemos por ejemplo un dispositivo de reproducción de vídeo. Pero un reproductor de VHS no es igual que un reproductor de DVD, a pesar de que ambos son subconjuntos de un conjunto mayor; dispositivos de reproducción de video. Podemos establecer una clase base que determine e implemente los miembros  y el comportamiento o una parte del comportamiento común de todos los dispositivos de reproducción de vídeo (reproducir, parar, pausa, expulsar...), y esta clase servirá de base a las clases de reproductor VHS y reproductor de DVD, que derivarán sus miembros de la clase base. Sin embargo, alguna de las clases derivadas puede añadir algo específico de ella que no tuviera sentido en otro subconjunto distinto. Por ejemplo, la clase del reproductor de VHS necesitará incluir también un método para rebobinar la cinta, cosa que no tendría sentido con un reproductor de DVD.

Nota: La instrucción Enum
La instrucción enum sirve para agrupar constantes. En este caso, se han agrupado cuatro constantes (Parado, EnPausa, Reproduciendo y SinMedio) en un grupo llamado EstadoReproductor, de forma que es posible utilizar un código mucho más fácil de leer que usando números directamente.


public enum EstadoReproductor
{
    Parado,
    EnPausa,
    Reproduciendo,
    SinMedio
}

class DispositivoVideo
{
    protected bool reproduciendo=false;
    protected bool pausado=false;
    protected bool medio=false;

    public virtual void Reproducir()
    {
        if (!medio)
            Console.WriteLine("Inserte un medio");
        else if (reproduciendo)
            Console.WriteLine("Ya estaba reproduciendo");
        else
        {
            Console.WriteLine("Reproduciendo vídeo");
            reproduciendo=true;
        }
    }

    public virtual void Detener()
    {
        if (!medio)
            Console.WriteLine("Inserte un medio");
        else if (reproduciendo)
        {
            Console.WriteLine("Reproducción detenida");
            reproduciendo=false;
            pausado=false;
        }
        else
            Console.WriteLine("Ya estaba parado");
    }

    public virtual void Pausa()
    {
        if (!medio)
            Console.WriteLine("Inserte un medio");
        else if (reproduciendo && !pausado)
        {
            Console.WriteLine("Reproducción en pausa");
            pausado=true;
        }
        else if(reproduciendo && pausado)
        {
            Console.WriteLine("Reproducción reanudada");
            pausado=false;
        }
        else
            Console.WriteLine("No se puede pausar. Está parado");
    }

    public virtual void IntroducirMedio()
    {
        if (medio)
            Console.WriteLine("Antes debe expulsar el medio actual");
        else
        {
            Console.WriteLine("Medio introducido");
            medio=true;
        }
    }
    public virtual void Expulsar()
    {
        if (!medio)
            Console.WriteLine("Inserte un medio");
        else
        {
            medio=false;
            reproduciendo=false;
            pausado=false;

            Console.WriteLine("Expulsando medio");
        }
    }

    public EstadoReproductor Estado
    {
        get
        {
            if (!medio)
                return EstadoReproductor.SinMedio;
            else if (pausado)
                return EstadoReproductor.EnPausa;
            else if (reproduciendo)
                return EstadoReproductor.Reproduciendo;
            else
                return EstadoReproductor.Parado;
        }
    }
}

Todos los dispositivos de vídeo incorporan el comportamiento expuesto en el código de arriba. Esta clase incorpora todo aquello que, como mínimo, necesita un dispositivo de reproducción de vídeo. Podemos utilizar esta clase como base para construir otras clases de reproductores de vídeo más específicas, como un reproductor de VHS o un reproductor de DVD. Hay tres campos con el modificador de acceso protected, este identificador hace que el miembro en cuestión sea visible en las clases derivadas, pero no en el cliente. De este modo, estos tres campos estarán disponibles en cualquier clase derivada de la clase DispositivoVideo. También se añade la palabra virtual en la declaración de cada método para permitir que las clases derivadas puedan modificar la implementación de estos métodos. Si no lo hacen, se ejecutará el código de la clase base. Se escriben a continuación las clases derivadas:

class DispositivoVHS:DispositivoVideo
{
    public void Rebobinar()
    {
        if (!medio)
            Console.WriteLine("Introduzca una cinta");
        else
            Console.WriteLine("Cinta rebobinada");
    }

    public override void IntroducirMedio()
    {
        if (medio)
            Console.WriteLine("Antes debe expulsar la cinta actual");
        else
        {
            Console.WriteLine("Cinta introducida");
            medio=true;
        }
    }
}

Esta clase es mucho más corta, pues sólo tiene que añadir o modificar las partes de la clase base que no sirven para un reproductor específico de VHS. Para  indicar que queremos heredar los miembros de una clase se añaden dos puntos al final del nombre de la clase y se pone a continuación el nombre de la clase base. En la clase DispositivoVHS se ha añadido un método llamado Rebobinar que no existe en la clase base y se ha sobreescrito el método IntroducirMedio que sí se encontraba en la clase base. Para sobreescribir un método virtual de la clase base hay que utilizar la palabra override, El resto de miembros de la clase base no es necesario tocarlos.
En cuanto a la implementación de la  clase DispositivoDVD:

class DispositivoDVD:DispositivoVideo
{
    public void IrA(int escena)
    {
        if (!medio)
            Console.WriteLine("Inserte un medio");
        else
        {
            reproduciendo=true;
            pausado=true;
        }
    }
}

No se ha tocado ningún método de la clase base, y se ha añadido el método IrA, para saltar a una escena determinada. La herencia ayuda mucho porque no es necesario repetir el mismo código una y otra vez. A continuación un pequeño programa que utiliza las clases implementadas más arriba:

class Videos
{
    static void Main()
    {
        Console.WriteLine("Crea un dispositivo genérico");
        DispositivoVideo video=new DispositivoVideo();
        
        Console.WriteLine();
        Acciones(video);

        Console.WriteLine();
        Console.WriteLine("Crea un reproductor VHS");
        video=new DispositivoVHS();
        Acciones(video);

        Console.WriteLine();
        Console.WriteLine("Crea reproductor DVD");
        video=new DispositivoDVD();
        Acciones(video);

        Console.ReadLine();
    }

    static void Acciones(DispositivoVideo video)
    {
        Console.WriteLine();
        Console.WriteLine("Pulsa intro para invocar IntroducirMedio");
        Console.ReadLine();
        video.IntroducirMedio();
        Estado(video);

        Console.WriteLine();
        Console.WriteLine("Pulsa intro para invocar Reproducir");
        Console.ReadLine();
        video.Reproducir();
        Estado(video);

        Console.WriteLine();
        Console.WriteLine("Pulsa intro para invocar Pausa");
        Console.ReadLine();
        video.Pausa();
        Estado(video);

        Console.WriteLine();
        Console.WriteLine("Pulsa intro para invocar Reproducir");
        Console.ReadLine();
        video.Reproducir();
        Estado(video);

        Console.WriteLine();
        Console.WriteLine("Pulsa intro para invocar Pausa");
        Console.ReadLine();
        video.Pausa();
        Estado(video);

        Console.WriteLine();
        Console.WriteLine("Pulsa intro para invocar Detener");
        Console.ReadLine();
        video.Detener();
        Estado(video);

        Console.WriteLine();
        Console.WriteLine("Pulsa intro para invocar Pausa");
        Console.ReadLine();
        video.Pausa();
        Estado(video);

        Console.WriteLine();
        Console.WriteLine("Pulsa intro para invocar Expulsar");
        Console.ReadLine();
        video.Expulsar();
        Estado(video);

        if (video is DispositivoVHS)
        {
            DispositivoVHS videoVHS=(DispositivoVHS) video;

            Console.WriteLine();
            Console.WriteLine("Pulsa intro para invocar Rebobinar");
            Console.ReadLine();
            videoVHS.Rebobinar();
            Estado(video);
        }

        if (video is DispositivoDVD)
        {
            DispositivoDVD videoDVD=(DispositivoDVD) video;

            Console.WriteLine();
            Console.WriteLine("Pulsa intro para invocar IrA(5)");
            Console.ReadLine();
            videoDVD.IrA(5);
            Estado(video);
        }

        Console.WriteLine();
    }

    static void Estado(DispositivoVideo video)
    {
        Console.WriteLine("Estado actual del reproductor: {0}",
            video.Estado);
    }
}

En el método main se ha utilizado el objeto video, declarado de la clase DispositivoVideo para instanciar los dispositivos de los tres tipos distintos. Y se les pasa a los métodos Acciones y Estado, cuando estos solamente aceptan como argumento un objeto de la clase DispositivoVideo. Esto funciona bien porque los dispositivos VHS y DVD son también dispositivos de vídeo, pues al haber derivado estas dos clases de la clase DispositivoVideo, el compilador entiende esto mismo, de modo que los admite sin ningún tipo de problemas. Por otro lado, los métodos Rebobinar e IrA no están implementados en la clase DispositivoVideo, sino que son particulares de las otras dos clases. Por este motivo se realiza la conversión al tipo específico que implementa el método para poder invocarlo. Es lo que se ha remarcado en negrita al final del programa.
Al invocar el método IntroducirMedio una segunda vez, cuando el objeto es un reproductor de VHS. Se ejecutó el método sobre-escrito en la clase DispositivoVHS, en lugar de ejecutarse el método virtual definido en la clase base.

La herencia permite reemplazar métodos de la clase base con toda tranquilidad, porque siempre se ejecutarán correctamente. El polimorfismo es la capacidad que tienen los objetos de comportarse de un modo distinto unos de otros aun compartiendo los mismos miembros. 

No hay comentarios:

Publicar un comentario