Autor: Jesús García

Programación Orientada a Objetos

Introducción

La programación orientada a objetos, en adelante POO, es un paradigma de programación que tiene por objeto estructurar el código de forma similar a cómo el ser humano organiza y clasifica la realidad que le rodea. De esta forma se busca que el código sea más fácil de organizar, comprender y por tanto mantener.

La POO se basa en organizar el código en módulos denominados clases en las que se definen las características y comportamientos que tienen en común sus instancias o ejemplares.

Como su nombre indica, la POO gira entorno al concepto de objeto. Así pues, un programa en ejecución consta de una serie de objetos relacionados que se comunican entre sí.

Clases

Las clases son una especie de plantillas que nos permiten definir cuáles son las características y comportamientos de los objetos creados a partir de ellas. Podemos ver las clases como los moldes que nos permiten crear los objetos.

La sintaxis de una clase es la siguiente:

class NombreClase{
    // definición de la clase
}

Donde NombreClase es el nombre de la clase.

Los nombres de las clases siempre empiezan en mayúscula.

Dentro de la clase definiremos sus características mediante el uso de campos o atributos y sus comportamientos mediante el uso de métodos.

Para continuar con la explicación pondremos un ejemplo que desarrollaremos a lo largo de la unidad. Imaginemos que vamos a desarrollar un programa para gestionar cuentas bancarias. Podríamos definir las siguientes características y comportamientos:

Por supuesto, se nos podrían ocurrir mil características y comportamientos mas, cuáles vamos a considerar en nuestro programa dependerá del problema que queramos resolver.

Campos o atributos

Mediante los campos o atributos representamos las características de los objetos. La sintaxis general es la siguiente:

    class NombreClase{
        accesibilidad tipo atributo1;
        accesibilidad tipo atributo2;
        ...
    }

Donde:

Podemos inicializar los campos con un valor por defecto de la siguiente forma:

    class NombreClase{
        accesibilidad tipo atributo1 = valor;
        ...
    }

Los campos son variables que están declaradas dentro de la clase, pero fuera de los métodos. Siempre los declararemos al inicio de la clase, antes de los métodos.

A continuación, se muestra la clase Cuenta con los campos numeroCuenta, titular y saldo:

class Cuenta{
    public string numeroCuenta;
    public string titular;
    public float saldo;
}

Ámbito de los campos.

Como ocurre con el resto de variables, el ámbito de los campos viene determinado por el bloque de llaves {} que lo contiene, por tanto los atributos pueden ser accedidos desde cualquier método de la clase.

Métodos

Los comportamientos de los objetos se definen mediante el uso de métodos, que no son más que funciones que se implementan dentro de una clase.

A continuación, se muestra la clase Cuenta incluyendo los métodos ingresar y reintegrar.

class Cuenta
{
    public string numeroCuenta;
    public string titular;
    public float saldo;

    public void Ingresar(float cantidad)
    {
        if(cantidad > 0) { // Comprobamos que la cantidad a ingresar es mayor que 0
            saldo = saldo + cantidad;
        }
    }

    public void Reintegrar(float cantidad)
    {
        if( saldo - cantidad >= 0){ // Comprobamos que el nuevo saldo no pase a ser negativo
            saldo = saldo - cantidad;
        }
    }
}

Como se puede ver en el ejemplo anterior, los campos son variables accesibles desde cualquier método.

Al conjunto de campos y métodos de una clase se le llama miembros de la clase.

Las variables declaradas dentro de un método reciben el nombre de variables locales.

La clase Program

Al crear una aplicación de consola siempre se crea un archivo Program.cs que define una clase Program con un método llamado Main. Así pues, en esta clase podemos declarar los campos y métodos que queramos. El método Main es el primero que se ejecuta al lanzar el programa y se denomina punto de entrada del programa.

Objetos

Hasta ahora hemos visto cómo crear las “plantillas” o “moldes” que nos van a permitir crear objetos. En una clase hemos definido las características y comportamientos de una cuenta bancaria, pero aún no hemos creado ninguna.

Creación de una instancia

Los elementos creados a partir de una clase se llaman instancias u objetos. Todos los objetos creados a partir de una clase tienen los mismos atributos y métodos, pero los atributos de cada objeto tienen sus propios valores.

Para crear una instancia de una clase en primer lugar es necesario declarar una variable. En el tipo de la variable escribiremos el nombre de la clase.

NombreClase nombreVariable;

La variable nombreVariable está declarada, pero no inicializada. Para poder utilizar la variable es imprescindible inicializarla. Para ello utilizaremos el operador new.

nombreVariable = new NombreClase();

Por supuesto, podríamos haber declarado e inicializado la variable en una única línea:

NombreClase nombreVariable = new NombreClase();

A partir de ahora, si se cumplen ciertas condiciones que veremos más adelante, podremos acceder a los atributos y métodos de la instancia mediante el uso de un punto tras el nombre de la variable como se muestra a continuación:

NombreClase nombreVariable = new NombreClase();
nombreVariable.atributo1 = nuevoValor; // Asignamos un nuevo valor al atributo atributo1
Console.WriteLine(nombreVariable.atributo1); // Mostramos el valor del atributo atirbuto1
nombreVariable.metodo1(); // Llamamos al método metodo1 de la variable nombreVariable

Siguiendo con el ejemplo de las cuentas bancarias vamos a crear varias instancias de la clase Cuenta en el método Main:

static void Main(string[] args)
{
    Cuenta c1 = new Cuenta();
    Cuenta c2 = new Cuenta();

    c1.titular = "Andresito";
    c1.Ingresar(100);

    c2.titular = "Laurita";
    c2.Ingresar(200);
    c2.Reintegro(300);

    Console.WriteLine(c1.titular + " " + c1.saldo);
    Console.WriteLine(c2.titular + " " + c2.saldo);
}

En el código anterior se crean dos instancias de la clase Cuenta. El titular de la primera cuenta pasa a ser Andresito quien ingresa 100€. La segunda cuenta pasa a ser de Laurita, quien hace un ingreso de 200€. El reintegro no se puede realizar ya que no hay saldo suficiente. Finalmente se muestra el titular y el saldo de ambas cuentas.

Ejercicio

Ejercicio 1

Programa la clase Mueble, sabiendo que las propiedades que queremos conocer son: modelo, material, tipo, altura, anchura y profundidad. La clase también tendrá un método que permitirá obtener todos los datos del mueble en un String. En el programa principal crea una instancia de Mueble, inicializa sus atributos y muéstralos por pantalla.

Ejercicio 2 - Parte A

Queremos desarrollar un programa para gestionar diferentes aspectos de las discotecas de Alicante. De una discoteca se necesita conocer su nombre, superficie en metros cuadrados y precio de la entrada. A partir de la superficie se debe poder obtener el aforo de la sala teniendo en cuenta que la ocupación mínima es de 0,5 metros cuadrados por persona. Además se quiere tener un método que, dado un número de entradas vendidas, devuelva el total de ingresos multiplicando para ello el número de entradas vendidas por su precio. Crea la(s) clase(s) que consideres necesarias para resolver el problema planteado y comprueba que funciona(n) correctamente.

Modificadores de accesibilidad

Las clases y sus miembros serán visibles o accesibles por otras clases en función de los modificadores de accesibilidad utilizados.

Modificadores de accesibilidad para clases

Los modificadores de accesibilidad que podemos aplicar a una clase en C# son los siguientes:

Que una clase sea accesible significa que puede ser instanciada.

El modificador de accesibilidad se especifica antes de la palabra reservada class.

accesibilidad class NombreClase {
    ...
}

Por ejemplo:

public class Empleado {
    ...
}

Si no indicamos el modificador de accesibilidad por defecto será internal.

Modificadores de acceso para miembros

Los modificadores de acceso disponibles en C# son los siguientes:

Como hemos visto anteriormente en los campos el modificador de accesibilidad se especifica antes del tipo.

accesibilidad tipo identificadorAtributo;

Por ejemplo:

public int edad;

En el caso de los métodos el modificador de accesibilidad se indica antes del tipo devuelto.

accesibilidad tipoDevuelto NombreMetodo (tipoParametro1 nombreParametro1,  ) 

Por ejemplo:

public int Sumar(int num1, int num2){
    ...
}

Que un método sea accesible significa que puede ser llamado. Que un campo sea accesible significa que se puede obtener o asignar un valor.

Si no indicamos el modificador de accesibilidad de un miembro por defecto será private. Podemos comprobarlo eliminando el modificador public de los atributos titular y saldo de la clase Cuenta.

class Cuenta
{
    public string numeroCuenta;
    string titular; // por defecto es private
    float saldo; // por defecto es private
    ...
}

Al hacer el cambio anterior se producirán los siguientes errores:

static void Main(string[] args)
{
    Cuenta c1 = new Cuenta();
    Cuenta c2 = new Cuenta();

    c1.titular = "Andresito"; // Error, el campo titular no es accesible
    c1.Ingresar(100);

    c2.titular = "Laurita"; // Error, el campo titular no es accesible
    c2.Ingresar(200);
    c2.Reintegro(300);

    Console.WriteLine(c1.titular + " " + c1.saldo); // Error, el campo saldo no es accesible
    Console.WriteLine(c2.titular + " " + c2.saldo); // Error, el campo saldo no es accesible
}

Podríamos pensar que para evitar problemas lo mejor sería que todos los campos fueran públicos. No obstante no es una práctica recomendada. ¿Por qué? Porque un campo público puede ser modificado desde cualquier lugar del programa con el inconveniente de que alguien podría asignar un valor inválido.

Imaginemos que no queremos que el saldo de las cuentas pueda ser inferior a 0. Si el campo es público nada impide que otro programador haga:

static void Main(string[] args)
{
    Cuenta c1 = new Cuenta();
    c1.saldo = -200; // Asignamos un valor inválido
}

C# soluciona este problema con las denominadas propiedades. Otros lenguajes como Java hacen uso de los denominados getters y setters. En ambos casos el principio es similar.

Getters y Setters

Esta solución es la que se utiliza en Java. Consiste en que los campos o atributos sean siempre privados. Para acceder a ellos se utilizan métodos set y get.

A continuación, se muestra la clase Cuenta programada en Java (podéis ver que la sintaxis es casi idéntica a C#).

public class Cuenta
{
    ... 
    private float saldo;
    ...

    public void setSaldo(float saldo){
        if(saldo > 0){
            this.saldo = saldo;
        }
    }
    
    public float getSaldo(){
        return saldo;
    }
    ...
}

Para obtener el valor del atributo saldo se hace uso del método getSaldo y para modificarlo el método setSaldo. Hacer uso de métodos nos permite escribir código para, entre otras cosas, controlar que el valor que se vaya asignar a un campo sea válido. A continuación, se muestra el uso de estos métodos:

public static void main(String[] args)
{
    Cuenta c1 = new Cuenta();
    c1.saldo = -200; // Error, el atributo saldo no es accesible
    c1.setSaldo(200); // El saldo de la cuenta pasa a ser 200
    c1.setSaldo(-500); // El saldo no se modifica ya que el valor -500 no es superior a 0.
    System.out.println(c1.getSaldo()); // Mostramos el saldo por pantalla
}

Propiedades

C# introduce el concepto de propiedades que, como veremos, es un concepto similar al de los getters y setters de Java pero con una sintaxis diferente.

Una propiedad C# se define de la siguiente forma:

private float saldo;
public float Saldo
{
   get { return saldo; }
   set { 
       if(value > 0){
           saldo = value;
       }
    }
}

Donde:

La propiedad Saldo se utilizaría del siguiente modo:

static void Main(string[] args)
{
    Cuenta c1 = new Cuenta();
    c1.saldo = 200; // Error, no podemos acceder al campo saldo ya que es privado
    // Debemos acceder a través  de la propiedad:
    c1.Saldo = 500; // El saldo pasa a ser 500
    c1.Saldo = -200; // El saldo no se modifica, ya que el valor -200 no es superior a 0
    Console.WriteLine(c1.Saldo); // Mostrará 500
}

Propiedades automáticas

A veces no se quiere añadir ningún tipo de lógica en los setters y getters. En esos casos se pueden utilizar propiedades automáticas, siguiendo la sintaxis:

public string Titular { get; set; }

En este caso no es necesario especificar el campo privado titular como sí hemos hecho a la hora de implementar la propiedad Saldo.

A la hora de implementar nuestras clases siempre utilizaremos propiedades y nunca campos o atributos privados.

Ocultación de atributos

Hasta ahora hemos visto que no podemos declarar dos variables con el mismo identificador si se encuentran en el mismo ámbito o en ámbitos anidados.

int n = int.Parse(Console.ReadLine());
if(n > 10)
{
    int n = 10; // Error: la variable n ya se ha declarado
}
public void GenerarNumero(int n)
{
    int n = int.Parse(Console.ReadLine()); // Error: la variable n ya se ha declarado
}

Sin embargo, existe una excepción: dentro de un método podemos declarar una variable con el mismo identificador que un atributo de la clase.

class Articulo
{
    string nombre = "Pepito";

    public void MostrarNombre(string nombre) // No se produce un error
    {
        nombre = "Juanito";
        Console.WriteLine(nombre);
    }
}

En el ejemplo anterior, dentro del método MostrarNombre ¿A qué variable se hace referencia cuando se utiliza la variable nombre?¿A la variable local, esto es, al parámetro del método o al atributo de la clase?.

Ante esta situación las variables locales tienen prioridad sobre el atributo. Se dice que la variable local oculta al atributo.

Por tanto, si llamamos al método MostrarNombre se mostrará por pantalla “Juanito” y el atributo nombre seguirá valiendo “Pepito”.

Ante esta situación, ¿Cómo podemos hacer referencia al atributo en vez de la variable local? Para ello disponemos de la palabra reservada this que permite hacer referencia a un atributo de la clase incluso cuando ha sido ocultado por una variable local.

A continuación, se muestra un ejemplo de uso de la palabra reservada this.

class Cuenta
{
    string numeroCuenta;
    string titular;
    float saldo;

    ...

    public void CambiarTitular (string titular)
    {
        this.titular = titular;
    }

    ...    

}

El ejemplo anterior this.titular hace referencia al atributo titular, mientras que titular hace referencia al parámetro del método. De esta forma es posible diferenciar a qué variable estamos haciendo referencia.

Ejercicio

Ejercicio 2 - Parte B
Modifica el código desarrollado en la Parte A para que haga uso de propiedades. El código debe validar que el precio de la entrada y la superficie de la discoteca no sea inferior a 0. A la hora de obtener los ingresos se debería comprobar que el número de entradas vendidas no es superior al aforo permitido, si lo es se debería devolver la cantidad de ingresos máximos de la discoteca que permite su aforo.

Constructores

Cuando se instancia una clase los campos a los que no se les asigna un valor en su declaración se inicializan con un valor por defecto. En el caso de los tipos numéricos se inicializan con un 0, las referencias se inicializan a null y los booleanos con false. Por ejemplo, al instanciar la clase Cuenta su saldo inicial siempre será 0.

Los constructores son métodos que permiten inicializar los objetos que se instancian.

Un constructor es un método especial que:

Al constructor, como a cualquier otro método, se le pueden pasar parámetros.

Veamos un ejemplo con la clase Cuenta en la que se define un constructor que recibe como parámetro el saldo inicial y se asigna a la propiedad correspondiente.

class Cuenta
{
        string numeroCuenta;
        string titular;
        private float saldo;
        public float Saldo
        {
            get { return saldo; }
            set { 
                if(value > 0)
                {
                    saldo = value;
                }
            }
        }

        // Constructor
        public Cuenta(float saldo)
        {
            Saldo = saldo;
        }
}

Si queremos instanciar la clase ahora deberemos indicar obligatoriamente su saldo.

static void Main(string[] args)
{
    Cuenta c1 = new Cuenta(500);
    Cuenta c2 = new Cuenta(); // Error, el constructor nos obliga a indicar el parámetro saldo. No podemos instanciar la clase de este modo.
    Console.WriteLine(c1.Saldo); // Mostrará 500
}

En una clase podemos tener múltiples constructores todos ellos con el mismo nombre pero con diferente número y/o tipo de parámetros de entrada. En este caso se dice que el constructor está sobrecargado.

class Cuenta
{
    public float NumeroCuenta { get; set; }
    public string Titular { get; set; }

    private float saldo;
    public float Saldo
    {
        get { return saldo; }
        set { 
            if(value > 0)
            {
                saldo = value;
            }
        }
    }

    public Cuenta(float saldo)
    {
        Saldo = saldo;
    }

    public Cuenta(float saldo, string titular)
    {
        Saldo = saldo;
        Titular = titular;
    }

    public Cuenta(float saldo, string titular, string numerCuenta)
    {
        Saldo = saldo;
        Titular = titular;
        NumeroCuenta = NumeroCuenta;
    }
    ...
}

A partir de ahora podríamos instanciar la clase Cuenta haciendo uso de cualquiera de los tres constructores.

Constructor por defecto

Si no definimos ningún constructor, por defecto C# incluirá un constructor vacío que no recibe ningún parámetro ni tiene código en su cuerpo. Por eso podemos instanciar clases aunque no hayamos implementado ningún constructor.

Ejercicio

Ejercicio 2 - Parte C
Modifica el ejercicio anterior para hacer uso de constructores de forma que no podamos crear ninguna discoteca sin indicar su nombre y superficie.

Miembros estáticos

Un miembro estático (ya sea un campo, propiedad o método) es aquel del que no existe una copia en cada objeto. Se trata de un miembro compartido por todas las instancias de la clase. Un miembro estático se declara mediante la palabra reservada static.

Imaginemos que queremos añadir una comisión mensual a nuestra clase Cuenta y que queremos que dicha comisión sea la misma para todas las cuentas. En ese caso añadiremos una propiedad estática como se muestra a continuación:

class Cuenta
{
    private static float comision;
    public static float Comision
    {
        get
        {
            return comision;
        }
        set
        {
            if (value > 0)
            {
                comision = value;
            }
        }
    }
    ...
}

Para acceder a dicha propiedad no es necesario instanciar la clase sino que podemos acceder a ella simplemente escribiendo el nombre de la clase seguida de un punto y el nombre del miembro estático:

Cuenta.Comision = 0.5f;
Cuenta c1 = new Cuenta(500, "Juan García", "ES12 1234 1234 12 123456789");
Cuenta c2 = new Cuenta(750, "Laura Marín", "ES12 1234 1234 12 123456789");

La propiedad Comision es compartida entre todas las instancias de la clase, por lo que al hacer Cuenta.Comision = 0.5f; todas las cuentas tendrán una comisión de 50 céntimos.

Servidor y cliente web

Un miembro estático puede ser accedido desde cualquier método aunque éste no sea estático. Por ejemplo, el método AplicarComision que se muestra a continuación no es estático y hace uso de la propiedad estática Comision.

class Cuenta
{
    ...
    public void AplicarComision()
    {
        Saldo -= Comision;
    }
    ...
}

También podemos tener métodos estáticos como el que se muestra a continuación:

class Cuenta
{
    ...

    public static void DuplicarComision()
    {
        Comision *= 2;
    }
    ...
}

Los métodos estáticos solo pueden acceder o llamar a otros miembros que también sean estáticos.

A continuación, se muestra un ejemplo de uso de los diferentes métodos y propiedades estáticas.

static void Main(string[] args)
{
    Cuenta.Comision = 0.5f;
    Cuenta c1 = new Cuenta(500, "Juan García", "ES12 1234 1234 12 123456789");
    c1.AplicarComision(); // Se aplica una comisión de 0,5€. El saldo pasa a ser 499.5

    Cuenta.DuplicarComision(); // La comisión de todas las cuentas pasa a ser 1€
    Cuenta c2 = new Cuenta(750, "Laura Marín", "ES12 1234 1234 12 123456789");

    c1.AplicarComision(); // Se aplica una comisión de 1€. El saldo pasa a ser 498.5
    c2.AplicarComision(); // Se aplica una comisión de 1€. El saldo pasa a ser 749

    Console.WriteLine("Cuenta 1: " + c1.Saldo);
    Console.WriteLine("Cuenta 2: " + c2.Saldo);

    Console.ReadKey();
}
Ejercicio

Ejercicio 2 - Parte 4
Debido al COVID se desea poder controlar el aforo de todas las discotecas y fijarlo en un 50%. Realiza las modificaciones pertinentes.