Explorar programação orientada a objeto com classes e objetos

Neste tutorial, você vai compilar um aplicativo de console e ver os recursos básicos orientados a objeto que fazem parte da linguagem C#.

Pré-requisitos

Criar o aplicativo

Usando uma janela de terminal, crie um diretório chamado Classes. Você compilará o aplicativo nesse diretório. Altere para esse diretório e digite dotnet new console na janela do console. Esse comando cria o aplicativo. Abra Program.cs. Ele deverá ser parecido com:

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

Neste tutorial, você criará novos tipos que representam uma conta bancária. Normalmente, os desenvolvedores definem cada classe em um arquivo de texto diferente. Isso facilita o gerenciamento à medida que o tamanho do programa aumenta. Crie um novo arquivo chamado BankAccount.cs no diretório Classes.

Esse arquivo conterá a definição de uma conta bancária. A programação Orientada a Objetos organiza o código por meio da criação de tipos na forma de classes. Essas classes contêm o código que representa uma entidade específica. A classe BankAccount representa uma conta bancária. O código implementa operações específicas por meio de métodos e propriedades. Neste tutorial, a conta bancária dá suporte a este comportamento:

  1. Ela tem um número com 10 dígitos que identifica exclusivamente a conta bancária.
  2. Ela tem uma cadeia de caracteres que armazena o nome ou os nomes dos proprietários.
  3. O saldo pode ser recuperado.
  4. Ela aceita depósitos.
  5. Ele aceita saques.
  6. O saldo inicial deve ser positivo.
  7. Os saques não podem resultar em um saldo negativo.

Definir o tipo de conta bancária

Você pode começar criando as noções básicas de uma classe que define esse comportamento. Crie um novo arquivo usando o comando File:New. Nomeie-o bankAccount.cs. Adicione o seguinte código ao arquivo 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 continuar, vamos dar uma olhada no que você compilou. A declaração namespace fornece uma maneira de organizar logicamente seu código. Este tutorial é relativamente pequeno, portanto, você colocará todo o código em um namespace.

public class BankAccount define a classe ou o tipo que você está criando. Tudo que vem dentro de { e } logo após a declaração da classe define o estado e o comportamento da classe. Há cinco membros na classe BankAccount. Os três primeiros são propriedades. Propriedades são elementos de dados que podem ter um código que impõe a validação ou outras regras. Os dois últimos são métodos. Os métodos são blocos de código que executam uma única função. A leitura dos nomes de cada um dos membros deve fornecer informações suficientes para você, ou outro desenvolvedor, entender o que a classe faz.

Abrir uma nova conta

O primeiro recurso a ser implementado serve para abrir uma conta bancária. Quando um cliente abre uma conta, ele deve fornecer um saldo inicial e informações sobre o proprietário, ou proprietários, dessa conta.

A criação de novo objeto do tipo BankAccount significa a definição de um construtor que atribui esses valores. Um construtor é um membro que tem o mesmo nome da classe. Ele é usado para inicializar objetos desse tipo de classe. Adicione o seguinte construtor ao tipo BankAccount. Insira o código a seguir acima da declaração de MakeDeposit:

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

O código anterior identifica as propriedades do objeto que está sendo construído, incluindo o qualificador this. Esse qualificador geralmente é opcional e omitido. Você também poderia ter escrito:

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

O qualificador this só é necessário quando uma variável ou parâmetro locais têm o mesmo nome desse campo ou propriedade. O qualificador this é omitido durante todo o restante deste artigo, a menos que seja necessário.

Construtores são chamados quando você cria um objeto usando new. Substitua a linha Console.WriteLine("Hello World!"); no arquivo Program.cs pelo seguinte código (substitua <name> pelo seu nome):

using Classes;

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

Vamos executar o que você construiu até agora. Se você estiver usando o Visual Studio, selecione Iniciar sem depuração no menu Depurar. Se estiver usando uma linha de comando, digite dotnet run no diretório em que criou seu projeto.

Você notou que o número da conta está em branco? É hora de corrigir isso. O número da conta deve ser atribuído na construção do objeto. Mas não é responsabilidade do chamador criá-lo. O código da classe BankAccount deve saber como atribuir novos números de conta. Uma maneira simples de fazer isso é começar com um número de 10 dígitos. Incremente-o à medida que novas contas são criadas. Por fim, armazene o número da conta atual quando um objeto for construído.

Adicione uma declaração de membro à classe BankAccount. Coloque a seguinte linha de código após a chave de abertura { no início da classe BankAccount:

private static int s_accountNumberSeed = 1234567890;

O accountNumberSeed é um membro de dados. Ele é private, o que significa que ele só pode ser acessado pelo código dentro da classe BankAccount. É uma maneira de separar as responsabilidades públicas (como ter um número de conta) da implementação privada (como os números de conta são gerados). Ele também é static, o que significa que é compartilhado por todos os objetos BankAccount. O valor de uma variável não estática é exclusivo para cada instância do objeto BankAccount. O accountNumberSeed é um campo private static e, portanto, tem o prefixo s_ de acordo com as convenções de nomenclatura do C#. O campo de sdenotação static e de _ denotação private. Adicione as duas linhas a seguir ao construtor para atribuir o número da conta. Coloque-as depois da linha que diz this.Balance = initialBalance:

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

Digite dotnet run para ver os resultados.

Criar depósitos e saques

A classe da conta bancária precisa aceitar depósitos e saques para funcionar corretamente. Vamos implementar depósitos e saques criando um diário de todas as transações da conta. Rastrear todas as transações tem algumas vantagens em comparação à simples atualização do saldo em cada transação. O histórico pode ser usado para auditar todas as transações e gerenciar os saldos diários. Calcular o saldo do histórico de todas as transações quando necessário assegura que todos os erros corrigidos em uma única transação serão refletidos corretamente no saldo no próximo cálculo.

Vamos começar criando um novo tipo para representar uma transação. Essa transação é um tipo simples que não tem qualquer responsabilidade. Ele precisa de algumas propriedades. Crie um novo arquivo chamado Transaction.cs. Adicione os seguintes códigos a ela:

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;
    }
}

Agora, vamos adicionar um List<T> de Transaction objetos à classe BankAccount. Adicione a seguinte declaração após o construtor no arquivo BankAccount.cs:

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

Agora, vamos calcular corretamente o Balance. O saldo atual pode ser encontrado somando os valores de todas as transações. De acordo com a forma atual do código, você só pode obter o saldo inicial da conta. Portanto, você terá que atualizar a propriedade Balance. Substitua a linha public decimal Balance { get; } em BankAccount.cs pelo seguinte código:

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

        return balance;
    }
}

Este exemplo mostra um aspecto importante das propriedades. Agora, você está calculando o saldo quando outro programador solicita o valor. Seu cálculo enumera todas as transações e fornece a soma como o saldo atual.

Depois, implemente os métodos MakeDeposit e MakeWithdrawal. Esses métodos vão impor as duas últimas regras: que o saldo inicial deve ser positivo, e que qualquer saque não pode criar um saldo negativo.

Essas regras introduzem o conceito de exceções. A forma padrão de indicar que um método não pode concluir seu trabalho com êxito é lançar uma exceção. O tipo de exceção e a mensagem associada a ele descrevem o erro. Aqui, o método MakeDeposit lançará uma exceção se o valor do depósito for menor ou igual a 0. O método MakeWithdrawal lançará uma exceção se o valor do saque for menor ou igual a 0 ou se a aplicação do saque resultar em um saldo negativo. Adicione o seguinte código depois da declaração da 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);
}

A instrução throwgera uma exceção. A execução do bloco atual é encerrada e o controle transferido para o bloco catch da primeira correspondência encontrado na pilha de chamadas. Você adicionará um bloco catch para testar esse código um pouco mais tarde.

O construtor deve receber uma alteração para que adicione uma transação inicial, em vez de atualizar o saldo diretamente. Como você já escreveu o método MakeDeposit, chame-o de seu construtor. O construtor concluído deve ter esta aparência:

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

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

DateTime.Now é uma propriedade que retorna a data e a hora atuais. Teste esse código adicionando alguns depósitos e saques em seu método Main, seguindo o código que cria um novo 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);

Em seguida, teste se você está recebendo condições de erro ao tentar criar uma conta com um saldo negativo. Adicione o seguinte código após o código anterior que você acabou de adicionar:

// 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 a instrução try-catch para marcar um bloco de código que possa gerar exceções e detectar esses erros esperados. Use a mesma técnica a fim de testar o código que gera uma exceção para um saldo negativo. Adicione o seguinte código antes da declaração de invalidAccount no seu 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());
}

Salve o arquivo e digite dotnet run para testá-lo.

Desafio – registrar em log todas as transações

Para concluir este tutorial, escreva o método GetAccountHistory que cria um string para o histórico de transações. Adicione esse método ao 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();
}

O histórico usa a classe StringBuilder para formatar uma cadeia de caracteres que contém uma linha para cada transação. Você viu o código de formatação da cadeia de caracteres anteriormente nesses tutoriais. Um caractere novo é \t. Ele insere uma guia para formatar a saída.

Adicione esta linha para testá-la no Program.cs:

Console.WriteLine(account.GetAccountHistory());

Execute o programa para ver os resultados.

Próximas etapas

Se você não conseguir avançar, veja a origem deste tutorial em nosso repositório GitHub.

Você pode continuar com o tutorial de programação orientada a objeto.

Saiba mais sobre esses conceitos nestes artigos: