Exploración de la programación orientada a objetos con clases y objetos

En este tutorial, creará una aplicación de consola y conocerá las características básicas orientadas a objetos que forman parte del lenguaje C#.

Requisitos previos

Creación de una aplicación

En una ventana de terminal, cree un directorio denominado Clases. Creará la aplicación ahí. Cambie a ese directorio y escriba dotnet new console en la ventana de la consola. Este comando crea la aplicación. Abra Program.cs. El resultado debería tener un aspecto similar a este:

// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");

En este tutorial, se van a crear tipos nuevos que representan una cuenta bancaria. Normalmente los desarrolladores definen cada clase en un archivo de texto diferente. De esta forma, la tarea de administración resulta más sencilla a medida que aumenta el tamaño del programa. Cree un archivo denominado BankAccount.cs en el directorio Classes.

Este archivo va a contener la definición de una cuenta bancaria. La programación orientada a objetos organiza el código creando tipos en forma de clases. Estas clases contienen el código que representa una entidad específica. La clase BankAccount representa una cuenta bancaria. El código implementa operaciones específicas a través de métodos y propiedades. En este tutorial, la cuenta bancaria admite el siguiente comportamiento:

  1. Tiene un número de diez dígitos que identifica la cuenta bancaria de forma única.
  2. Tiene una cadena que almacena el nombre o los nombres de los propietarios.
  3. Se puede consultar el saldo.
  4. Acepta depósitos.
  5. Acepta reintegros.
  6. El saldo inicial debe ser positivo.
  7. Los reintegros no pueden generar un saldo negativo.

Definición del tipo de cuenta bancaria

Puede empezar por crear los datos básicos de una clase que define dicho comportamiento. Cree un archivo con el comando File:New. Asígnele el nombre BankAccount.cs. Agregue el código siguiente al archivo BankAccount.cs:

namespace Classes;

public class BankAccount
{
    public string Number { get; }
    public string Owner { get; set; }
    public decimal Balance { get; }

    public void MakeDeposit(decimal amount, DateTime date, string note)
    {
    }

    public void MakeWithdrawal(decimal amount, DateTime date, string note)
    {
    }
}

Antes de avanzar, se va a dar un repaso a lo que ha compilado. La declaración namespace permite organizar el código de forma lógica. Este tutorial es relativamente pequeño, por lo que deberá colocar todo el código en un espacio de nombres.

public class BankAccount define la clase o el tipo que quiere crear. Todo lo que se encuentra entre { y } después de la declaración de clase define el estado y el comportamiento de la clase. La clase BankAccount cuenta con cinco miembros. Los tres primeros son propiedades. Las propiedades son elementos de datos que pueden contener código que exige la validación u otras reglas. Los dos últimos son métodos. Los métodos son bloques de código que realizan una única función. La lectura de los nombres de cada miembro debe proporcionar suficiente información tanto al usuario como a otro desarrollador para entender cuál es la función de la clase.

Apertura de una cuenta nueva

La primera característica que se va a implementar es la apertura de una cuenta bancaria. Cuando un cliente abre una cuenta, debe proporcionar un saldo inicial y la información sobre el propietario o los propietarios de esa cuenta.

Para crear un objeto de tipo BankAccount, es necesario definir un constructor que asigne esos valores. Un constructor es un miembro que tiene el mismo nombre que la clase. Se usa para inicializar los objetos de ese tipo de clase. Agregue el siguiente constructor al tipo BankAccount. Coloque el siguiente código encima de la declaración de MakeDeposit.

public BankAccount(string name, decimal initialBalance)
{
    this.Owner = name;
    this.Balance = initialBalance;
}

En el código anterior se identifican las propiedades del objeto que se está construyendo mediante la inclusión del calificador this. Ese calificador suele ser opcional y se omite. También podría haber escrito lo siguiente:

public BankAccount(string name, decimal initialBalance)
{
    Owner = name;
    Balance = initialBalance;
}

El calificador this solo es necesario cuando una variable o un parámetro local tiene el mismo nombre que el campo o la propiedad. El calificador this se omite en el resto de este artículo, a menos que sea necesario.

A los constructores se les llama cuando se crea un objeto mediante new. Reemplace la línea Console.WriteLine("Hello World!"); de Program.cs por la siguiente línea (reemplace <name> por su nombre):

using Classes;

var account = new BankAccount("<name>", 1000);
Console.WriteLine($"Account {account.Number} was created for {account.Owner} with {account.Balance} initial balance.");

Vamos a ejecutar lo que se ha creado hasta ahora. Si usa Visual Studio, seleccione Iniciar sin depurar en el menú Depurar. Si va a usar una línea de comandos, escriba dotnet run en el directorio en el que ha creado el proyecto.

¿Ha observado que el número de cuenta está en blanco? Es el momento de solucionarlo. El número de cuenta debe asignarse cuando se construye el objeto. Sin embargo, el autor de la llamada no es el responsable de crearlo. El código de la clase BankAccount debe saber cómo asignar nuevos números de cuenta. Una manera sencilla de empezar es con un número de diez dígitos. Increméntelo cuando cree cada cuenta. Por último, almacene el número de cuenta actual cuando se construya un objeto.

Agregue una declaración de miembro a la clase BankAccount. Coloque la siguiente línea de código después de la llave de apertura { al principio de la clase BankAccount:

private static int s_accountNumberSeed = 1234567890;

accountNumberSeed es un miembro de datos. Tiene el estado private, lo que significa que solo se puede acceder a él con el código incluido en la clase BankAccount. Es una forma de separar las responsabilidades públicas (como tener un número de cuenta) de la implementación privada (cómo se generan los números de cuenta). También es static, lo que significa que lo comparten todos los objetos BankAccount. El valor de una variable no estática es único para cada instancia del objeto BankAccount. accountNumberSeed es un campo private static y, por tanto, lleva el prefijo s_ según las convenciones de nomenclatura de C#. s indica static y _ indica el campo private. Agregue las dos líneas siguientes al constructor para asignar el número de cuenta: Colóquelas después de la línea donde pone this.Balance = initialBalance:

Number = s_accountNumberSeed.ToString();
s_accountNumberSeed++;

Escriba dotnet run para ver los resultados.

Creación de depósitos y reintegros

La clase de la cuenta bancaria debe aceptar depósitos y reintegros para que el funcionamiento sea adecuado. Se van a implementar depósitos y reintegros con la creación de un diario de cada transacción de la cuenta. Hacer un seguimiento de cada transacción ofrece algunas ventajas con respecto a limitarse a actualizar el saldo en cada transacción. El historial se puede utilizar para auditar todas las transacciones y administrar los saldos diarios. Con el cálculo del saldo a partir del historial de todas las transacciones, cuando proceda, nos aseguramos de que todos los errores de una única transacción que se solucionen se reflejarán correctamente en el saldo cuando se haga el siguiente cálculo.

Se va a empezar por crear un tipo para representar una transacción. La transacción es un tipo simple que no tiene ninguna responsabilidad. Necesita algunas propiedades. Cree un archivo denominado Transaction.cs. Agregue el código siguiente a él:

namespace Classes;

public class Transaction
{
    public decimal Amount { get; }
    public DateTime Date { get; }
    public string Notes { get; }

    public Transaction(decimal amount, DateTime date, string note)
    {
        Amount = amount;
        Date = date;
        Notes = note;
    }
}

Ahora se va a agregar List<T> de objetos Transaction a la clase BankAccount. Agregue la siguiente declaración después del constructor en el archivo BankAccount.cs:

private List<Transaction> _allTransactions = new List<Transaction>();

Ahora vamos a calcular correctamente Balance. El saldo actual se puede averiguar si se suman los valores de todas las transacciones. Como el código es actual, solo puede obtener el saldo inicial de la cuenta, así que tiene que actualizar la propiedad Balance. Reemplace la línea public decimal Balance { get; } de BankAccount.cs con el código siguiente:

public decimal Balance
{
    get
    {
        decimal balance = 0;
        foreach (var item in _allTransactions)
        {
            balance += item.Amount;
        }

        return balance;
    }
}

En este ejemplo se muestra un aspecto importante de las propiedades. Ahora va a calcular el saldo cuando otro programador solicite el valor. El cálculo enumera todas las transacciones y proporciona la suma como el saldo actual.

Después, implemente los métodos MakeDeposit y MakeWithdrawal. Estos métodos aplicarán las dos reglas finales: el saldo inicial debe ser positivo, y ningún reintegro debe generar un saldo negativo.

Las reglas presentan el concepto de las excepciones. La forma habitual de indicar que un método no puede completar su trabajo correctamente consiste en generar una excepción. El tipo de excepción y el mensaje asociado a ella describen el error. En este caso, el método MakeDeposit genera una excepción si el importe del depósito no es mayor que 0. El método MakeWithdrawal genera una excepción si el importe del reintegro no es mayor que 0 o si el procesamiento de la operación tiene como resultado un saldo negativo: Agregue el código siguiente después de la declaración de la lista _allTransactions:

public void MakeDeposit(decimal amount, DateTime date, string note)
{
    if (amount <= 0)
    {
        throw new ArgumentOutOfRangeException(nameof(amount), "Amount of deposit must be positive");
    }
    var deposit = new Transaction(amount, date, note);
    _allTransactions.Add(deposit);
}

public void MakeWithdrawal(decimal amount, DateTime date, string note)
{
    if (amount <= 0)
    {
        throw new ArgumentOutOfRangeException(nameof(amount), "Amount of withdrawal must be positive");
    }
    if (Balance - amount < 0)
    {
        throw new InvalidOperationException("Not sufficient funds for this withdrawal");
    }
    var withdrawal = new Transaction(-amount, date, note);
    _allTransactions.Add(withdrawal);
}

La instrucción throwproduce una excepción. La ejecución del bloque actual finaliza y el control se transfiere al primer bloque catch coincidente que se encuentra en la pila de llamadas. Se agregará un bloque catch para probar este código un poco más adelante.

El constructor debe obtener un cambio para que agregue una transacción inicial, en lugar de actualizar el saldo directamente. Puesto que ya escribió el método MakeDeposit, llámelo desde el constructor. El constructor terminado debe tener este aspecto:

public BankAccount(string name, decimal initialBalance)
{
    Number = s_accountNumberSeed.ToString();
    s_accountNumberSeed++;

    Owner = name;
    MakeDeposit(initialBalance, DateTime.Now, "Initial balance");
}

DateTime.Now es una propiedad que devuelve la fecha y hora actuales. Para probar este código, agregue algunos depósitos y reintegros en el método Main, siguiendo el código con el que se crea un elemento BankAccount:

account.MakeWithdrawal(500, DateTime.Now, "Rent payment");
Console.WriteLine(account.Balance);
account.MakeDeposit(100, DateTime.Now, "Friend paid me back");
Console.WriteLine(account.Balance);

Después, compruebe si detecta las condiciones de error intentando crear una cuenta con un saldo negativo. Agregue el código siguiente después del código anterior que acaba de agregar:

// Test that the initial balances must be positive.
BankAccount invalidAccount;
try
{
    invalidAccount = new BankAccount("invalid", -55);
}
catch (ArgumentOutOfRangeException e)
{
    Console.WriteLine("Exception caught creating account with negative balance");
    Console.WriteLine(e.ToString());
    return;
}

Use la instrucción try-catch para marcar un bloque de código que puede generar excepciones y para detectar los errores que se esperan. Puede usar la misma técnica para probar el código que genera una excepción para un saldo negativo. Agregue el código siguiente antes de la declaración de invalidAccount en el método Main:

// Test for a negative balance.
try
{
    account.MakeWithdrawal(750, DateTime.Now, "Attempt to overdraw");
}
catch (InvalidOperationException e)
{
    Console.WriteLine("Exception caught trying to overdraw");
    Console.WriteLine(e.ToString());
}

Guarde el archivo y escriba dotnet run para probarlo.

Desafío: registro de todas las transacciones

Para finalizar este tutorial, puede escribir el método GetAccountHistory que crea string para el historial de transacciones. Agregue este método al tipo BankAccount:

public string GetAccountHistory()
{
    var report = new System.Text.StringBuilder();

    decimal balance = 0;
    report.AppendLine("Date\t\tAmount\tBalance\tNote");
    foreach (var item in _allTransactions)
    {
        balance += item.Amount;
        report.AppendLine($"{item.Date.ToShortDateString()}\t{item.Amount}\t{balance}\t{item.Notes}");
    }

    return report.ToString();
}

En el historial se usa la clase StringBuilder para dar formato a una cadena que contiene una línea para cada transacción. Se ha visto anteriormente en estos tutoriales el código utilizado para dar formato a una cadena. Un carácter nuevo es \t. Inserta una pestaña para dar formato a la salida.

Agregue esta línea para probarla en Program.cs:

Console.WriteLine(account.GetAccountHistory());

Ejecute el programa para ver los resultados.

Pasos siguientes

Si se ha quedado bloqueado, puede consultar el origen de este tutorial en el repositorio de GitHub.

Puede continuar con el tutorial de la programación orientada a objetos.

Puede aprender más sobre estos conceptos en los artículos siguientes: