Programação orientada a objeto (C#)

O C# é uma linguagem de programação orientada a objeto. Os quatro princípios básicos da programação orientada a objetos são:

  • Abstração Modelando os atributos e interações relevantes de entidades como classes para definir uma representação abstrata de um sistema.
  • Encapsulamento Ocultando o estado interno e a funcionalidade de um objeto e permitindo apenas o acesso por meio de um conjunto público de funções.
  • Herança Capacidade de criar novas abstrações com base em abstrações existentes.
  • Polimorfismo Capacidade de implementar propriedades ou métodos herdados de diferentes maneiras em várias abstrações.

No tutorial anterior, introdução às classes que você viu abstração e encapsulamento. A classe BankAccount forneceu uma abstração para o conceito de conta bancária. Você pode modificar sua implementação sem afetar nenhum dos códigos que usaram a classe BankAccount. As classes BankAccount e Transaction classes fornecem encapsulamento dos componentes necessários para descrever esses conceitos no código.

Neste tutorial, você estenderá esse aplicativo para usar herança e polimorfismo para adicionar novos recursos. Você também adicionará recursos à BankAccount classe, aproveitando as técnicas de abstração e encapsulamento que aprendeu no tutorial anterior.

Criar diferentes tipos de contas

Depois de criar este programa, você recebe solicitações para adicionar recursos a ele. Funciona muito bem na situação em que há apenas um tipo de conta bancária. Ao longo do tempo, as necessidades são alteradas e os tipos de conta relacionados são solicitados:

  • Uma conta de ganho de juros que acumula juros no final de cada mês.
  • Uma linha de crédito que pode ter saldo negativo, mas quando há saldo, há cobrança de juros a cada mês.
  • Uma conta de cartão presente pré-paga que começa com um único depósito, e só pode ser paga. Ela pode ser preenchida novamente uma vez no início de cada mês.

Todas essas contas diferentes são semelhantes à classe BankAccount definida no tutorial anterior. Você pode copiar esse código, renomear as classes e fazer modificações. Essa técnica funcionaria a curto prazo, mas seria mais trabalho ao longo do tempo. Todas as alterações seriam copiadas em todas as classes afetadas.

Em vez disso, você pode criar novos tipos de conta bancária que herdam métodos e dados da classe BankAccount criada no tutorial anterior. Essas novas classes podem estender a classe BankAccount com o comportamento específico necessário para cada tipo:

public class InterestEarningAccount : BankAccount
{
}

public class LineOfCreditAccount : BankAccount
{
}

public class GiftCardAccount : BankAccount
{
}

Cada uma dessas classes herda o comportamento compartilhado de sua classe base compartilhada, a classe BankAccount. Escreva as implementações para funcionalidades novas e diferentes em cada uma das classes derivadas. Essas classes derivadas já têm todo o comportamento definido na classe BankAccount.

É uma boa prática criar cada nova classe em um arquivo de origem diferente. No Visual Studio, você pode clicar com o botão direito do mouse no projeto e selecionar adicionar classe para adicionar uma nova classe em um novo arquivo. No Visual Studio Code, selecione Arquivo e Novo para criar um novo arquivo de origem. Em qualquer ferramenta, nomeie o arquivo para corresponder à classe: InterestEarningAccount.cs, LineOfCreditAccount.cs e GiftCardAccount.cs.

Ao criar as classes, conforme mostrado no exemplo anterior, você descobrirá que nenhuma das suas classes derivadas é compilada. Um construtor é responsável por inicializar um objeto. Um construtor de classe derivada deve inicializar a classe derivada e fornecer instruções sobre como inicializar o objeto de classe base incluído na classe derivada. A inicialização adequada normalmente ocorre sem nenhum código extra. A classe BankAccount declara um construtor público com a seguinte assinatura:

public BankAccount(string name, decimal initialBalance)

O compilador não gera um construtor padrão quando você define um construtor por conta própria. Isso significa que cada classe derivada deve chamar explicitamente esse construtor. Você declara um construtor que pode passar argumentos para o construtor de classe base. O código a seguir mostra o construtor para o InterestEarningAccount:

public InterestEarningAccount(string name, decimal initialBalance) : base(name, initialBalance)
{
}

Os parâmetros para esse novo construtor correspondem ao tipo de parâmetro e aos nomes do construtor de classe base. Use a sintaxe : base() para indicar uma chamada para um construtor de classe base. Algumas classes definem vários construtores e essa sintaxe permite que você escolha qual construtor de classe base você chama. Depois de atualizar os construtores, você pode desenvolver o código para cada uma das classes derivadas. Os requisitos para as novas classes podem ser declarados da seguinte maneira:

  • Uma conta de ganho de juros:
    • Obterá um crédito de 2% do saldo final do mês.
  • Uma linha de crédito:
    • Pode ter um saldo negativo, mas não ser maior em valor absoluto do que o limite de crédito.
    • Incorrerá em uma cobrança de juros a cada mês em que o saldo de fim de mês não seja 0.
    • Incorrerá em uma taxa em cada saque que ultrapassa o limite de crédito.
  • Uma conta de cartão presente:
    • Pode ser preenchido novamente com um valor especificado uma vez por mês, no último dia do mês.

Você pode ver que todos os três tipos de conta têm uma ação que ocorre no final de cada mês. No entanto, cada tipo de conta faz tarefas diferentes. Você usa polimorfismo para implementar esse código. Crie um único método virtual na classe BankAccount:

public virtual void PerformMonthEndTransactions() { }

O código anterior mostra como você usa a palavra-chave virtual para declarar um método na classe base para o qual uma classe derivada pode fornecer uma implementação diferente. Um método virtual é um método em que qualquer classe derivada pode optar por reimplementar. As classes derivadas usam a palavra-chave override para definir a nova implementação. Normalmente, você se refere a isso como "substituindo a implementação da classe base". A palavra-chave virtual especifica que as classes derivadas podem substituir o comportamento. Você também pode declarar métodos abstract em que classes derivadas devem substituir o comportamento. A classe base não fornece uma implementação para um método abstract. Em seguida, você precisa definir a implementação para duas das novas classes que você criou. Inicie com um InterestEarningAccount:

public override void PerformMonthEndTransactions()
{
    if (Balance > 500m)
    {
        decimal interest = Balance * 0.02m;
        MakeDeposit(interest, DateTime.Now, "apply monthly interest");
    }
}

Adicione o seguinte código ao LineOfCreditAccount. O código nega o saldo para calcular uma taxa de juros positiva que é retirada da conta:

public override void PerformMonthEndTransactions()
{
    if (Balance < 0)
    {
        // Negate the balance to get a positive interest charge:
        decimal interest = -Balance * 0.07m;
        MakeWithdrawal(interest, DateTime.Now, "Charge monthly interest");
    }
}

A classe GiftCardAccount precisa de duas alterações para implementar sua funcionalidade de fim de mês. Primeiro, modifique o construtor para incluir um valor opcional a ser adicionado a cada mês:

private readonly decimal _monthlyDeposit = 0m;

public GiftCardAccount(string name, decimal initialBalance, decimal monthlyDeposit = 0) : base(name, initialBalance)
    => _monthlyDeposit = monthlyDeposit;

O construtor fornece um valor padrão para o valor monthlyDeposit para que os chamadores possam omitir um 0 sem depósito mensal. Em seguida, substitua o método PerformMonthEndTransactions para adicionar o depósito mensal, se ele foi definido como um valor diferente de zero no construtor:

public override void PerformMonthEndTransactions()
{
    if (_monthlyDeposit != 0)
    {
        MakeDeposit(_monthlyDeposit, DateTime.Now, "Add monthly deposit");
    }
}

A substituição aplica o conjunto de depósito mensal no construtor. Adicione o seguinte código ao método Main para testar essas alterações para GiftCardAccount e InterestEarningAccount:

var giftCard = new GiftCardAccount("gift card", 100, 50);
giftCard.MakeWithdrawal(20, DateTime.Now, "get expensive coffee");
giftCard.MakeWithdrawal(50, DateTime.Now, "buy groceries");
giftCard.PerformMonthEndTransactions();
// can make additional deposits:
giftCard.MakeDeposit(27.50m, DateTime.Now, "add some additional spending money");
Console.WriteLine(giftCard.GetAccountHistory());

var savings = new InterestEarningAccount("savings account", 10000);
savings.MakeDeposit(750, DateTime.Now, "save some money");
savings.MakeDeposit(1250, DateTime.Now, "Add more savings");
savings.MakeWithdrawal(250, DateTime.Now, "Needed to pay monthly bills");
savings.PerformMonthEndTransactions();
Console.WriteLine(savings.GetAccountHistory());

Verifique os resultados. Agora, adicione um conjunto semelhante de código de teste para LineOfCreditAccount:

var lineOfCredit = new LineOfCreditAccount("line of credit", 0);
// How much is too much to borrow?
lineOfCredit.MakeWithdrawal(1000m, DateTime.Now, "Take out monthly advance");
lineOfCredit.MakeDeposit(50m, DateTime.Now, "Pay back small amount");
lineOfCredit.MakeWithdrawal(5000m, DateTime.Now, "Emergency funds for repairs");
lineOfCredit.MakeDeposit(150m, DateTime.Now, "Partial restoration on repairs");
lineOfCredit.PerformMonthEndTransactions();
Console.WriteLine(lineOfCredit.GetAccountHistory());

Ao adicionar o código anterior e executar o programa, você verá algo semelhante ao seguinte erro:

Unhandled exception. System.ArgumentOutOfRangeException: Amount of deposit must be positive (Parameter 'amount')
   at OOProgramming.BankAccount.MakeDeposit(Decimal amount, DateTime date, String note) in BankAccount.cs:line 42
   at OOProgramming.BankAccount..ctor(String name, Decimal initialBalance) in BankAccount.cs:line 31
   at OOProgramming.LineOfCreditAccount..ctor(String name, Decimal initialBalance) in LineOfCreditAccount.cs:line 9
   at OOProgramming.Program.Main(String[] args) in Program.cs:line 29

Observação

A saída real inclui o caminho completo para a pasta com o projeto. Os nomes das pastas foram omitidos para brevidade. Além disso, dependendo do formato de código, os números de linha podem ser ligeiramente diferentes.

Esse código falha porque BankAccount pressupõe que o saldo inicial deve ser maior que 0. Outra suposição feita na classe BankAccount é que o saldo não pode ficar negativo. Em vez disso, qualquer retirada que sobrecarrega a conta é rejeitada. Ambas as suposições precisam mudar. A linha de conta de crédito começa em 0 e geralmente terá um saldo negativo. Além disso, se um cliente empresta muito dinheiro, ele incorre em uma taxa. A transação é aceita, só custa mais. A primeira regra pode ser implementada adicionando um argumento opcional ao construtor BankAccount que especifica o saldo mínimo. O padrão é 0. A segunda regra requer um mecanismo que permite que classes derivadas modifiquem o algoritmo padrão. De certa forma, a classe base "pergunta" ao tipo derivado o que deve acontecer quando há um cheque especial. O comportamento padrão é rejeitar a transação lançando uma exceção.

Vamos começar adicionando um segundo construtor que inclui um parâmetro minimumBalance opcional. Esse novo construtor faz todas as ações feitas pelo construtor existente. Além disso, ele define a propriedade de saldo mínimo. Você pode copiar o corpo do construtor existente, mas isso significa dois locais a serem alterados no futuro. Em vez disso, você pode usar o encadeamento de construtor para que um construtor chame outro. O código a seguir mostra os dois construtores e o novo campo adicional:

private readonly decimal _minimumBalance;

public BankAccount(string name, decimal initialBalance) : this(name, initialBalance, 0) { }

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

    Owner = name;
    _minimumBalance = minimumBalance;
    if (initialBalance > 0)
        MakeDeposit(initialBalance, DateTime.Now, "Initial balance");
}

O código anterior mostra duas novas técnicas. Primeiro, o campo minimumBalance é marcado como readonly. Isso significa que o valor não pode ser alterado depois que o objeto é construído. Depois que BankAccount é criado, minimumBalance não pode alterar. Em segundo lugar, o construtor que usa dois parâmetros : this(name, initialBalance, 0) { } como implementação. A expressão : this() chama o outro construtor, aquele com três parâmetros. Essa técnica permite que você tenha uma única implementação para inicializar um objeto, embora o código do cliente possa escolher um dos muitos construtores.

Essa implementação chamará MakeDeposit somente se o saldo inicial for maior que 0. Isso preserva a regra de que os depósitos devem ser positivos, mas permite que a conta de crédito abra com um saldo 0.

Agora que a classe BankAccount tem um campo somente leitura para o saldo mínimo, a alteração final é alterar o código físico 0 para minimumBalance no método MakeWithdrawal:

if (Balance - amount < _minimumBalance)

Depois de estender a classe BankAccount, você pode modificar o construtor LineOfCreditAccount para chamar o novo construtor base, conforme mostrado no seguinte código:

public LineOfCreditAccount(string name, decimal initialBalance, decimal creditLimit) : base(name, initialBalance, -creditLimit)
{
}

Observe que o construtor LineOfCreditAccount altera o sinal do parâmetro creditLimit para que corresponda ao significado do parâmetro minimumBalance.

Regras de cheque especial diferentes

O último recurso a ser adicionado permite que LineOfCreditAccount cobre uma taxa por ultrapassar o limite de crédito em vez de recusar a transação.

Uma técnica é definir uma função virtual na qual você implementa o comportamento necessário. A classe BankAccount refatora o método MakeWithdrawal em dois métodos. O novo método faz a ação especificada quando o saque leva o saldo abaixo do mínimo. O método existente MakeWithdrawal tem o seguinte código:

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 < _minimumBalance)
    {
        throw new InvalidOperationException("Not sufficient funds for this withdrawal");
    }
    var withdrawal = new Transaction(-amount, date, note);
    _allTransactions.Add(withdrawal);
}

Substitua-o pelo seguinte código:

public void MakeWithdrawal(decimal amount, DateTime date, string note)
{
    if (amount <= 0)
    {
        throw new ArgumentOutOfRangeException(nameof(amount), "Amount of withdrawal must be positive");
    }
    Transaction? overdraftTransaction = CheckWithdrawalLimit(Balance - amount < _minimumBalance);
    Transaction? withdrawal = new(-amount, date, note);
    _allTransactions.Add(withdrawal);
    if (overdraftTransaction != null)
        _allTransactions.Add(overdraftTransaction);
}

protected virtual Transaction? CheckWithdrawalLimit(bool isOverdrawn)
{
    if (isOverdrawn)
    {
        throw new InvalidOperationException("Not sufficient funds for this withdrawal");
    }
    else
    {
        return default;
    }
}

O método adicionado é protected, o que significa que ele pode ser chamado apenas de classes derivadas. Essa declaração impede que outros clientes chamem o método. É também virtual para que classes derivadas possam alterar o comportamento. O tipo de retorno é Transaction?. A anotação ? indica que o método pode retornar null. Adicione a seguinte implementação para LineOfCreditAccount cobrar uma taxa quando o limite de saque for excedido:

protected override Transaction? CheckWithdrawalLimit(bool isOverdrawn) =>
    isOverdrawn
    ? new Transaction(-20, DateTime.Now, "Apply overdraft fee")
    : default;

A substituição retorna uma transação de taxa quando a conta é sacada. Se o saque não ultrapassar o limite, o método retornará uma transação null. Isso indica que não há nenhuma taxa. Teste essas alterações adicionando o seguinte código ao seu método Main na classe Program:

var lineOfCredit = new LineOfCreditAccount("line of credit", 0, 2000);
// How much is too much to borrow?
lineOfCredit.MakeWithdrawal(1000m, DateTime.Now, "Take out monthly advance");
lineOfCredit.MakeDeposit(50m, DateTime.Now, "Pay back small amount");
lineOfCredit.MakeWithdrawal(5000m, DateTime.Now, "Emergency funds for repairs");
lineOfCredit.MakeDeposit(150m, DateTime.Now, "Partial restoration on repairs");
lineOfCredit.PerformMonthEndTransactions();
Console.WriteLine(lineOfCredit.GetAccountHistory());

Execute o programa e verifique os resultados.

Resumo

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

Este tutorial demonstrou muitas das técnicas usadas na programação Orientada por objeto:

  • Você usou Abstração quando definiu classes para cada um dos diferentes tipos de conta. Essas classes descreveram o comportamento desse tipo de conta.
  • Você usou Encapsulamento quando manteve muitos detalhes private em cada classe.
  • Você usou Herança quando aproveitou a implementação já criada na classe para salvar o código BankAccount.
  • Você usou Polimorfismo ao criar métodos virtual que as classes derivadas poderiam substituir para criar um comportamento específico para esse tipo de conta.