Programação orientada a objetos (C#)

C# é uma linguagem de programação orientada a objetos. 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 através 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 múltiplas abstrações.

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

Neste tutorial, você estenderá esse aplicativo para fazer uso de herança e polimorfismo para adicionar novos recursos. Você também adicionará recursos à BankAccount classe, aproveitando as técnicas de abstração e encapsulamento aprendidas 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. Com o tempo, as necessidades mudam 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 um saldo negativo, mas quando há saldo, há uma cobrança de juros todos os meses.
  • Uma conta de cartão presente pré-paga que começa com um único depósito e só pode ser paga. Pode ser reabastecido uma vez no início de cada mês.

Todas essas contas diferentes são semelhantes à BankAccount classe 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 trabalhosa ao longo do tempo. Quaisquer 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 BankAccount classe criada no tutorial anterior. Essas novas classes podem estender a BankAccount classe 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 BankAccount classe. 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 BankAccount classe.

É 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 uma das ferramentas, 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 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 acontece sem qualquer código extra. A BankAccount classe 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ê mesmo define um construtor. 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 este novo construtor correspondem ao tipo de parâmetro e nomes do construtor de classe base. Use a : base() sintaxe 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 forma:

  • Uma conta de ganho de juros:
    • Receberá um crédito de 2% do saldo de fim de 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 quando o saldo do final do mês não for 0.
    • Incorrerá numa taxa por cada levantamento que ultrapasse o limite de crédito.
  • Uma conta de cartão-presente:
    • Pode ser recarregado com uma quantidade especificada uma vez por mês, no último dia do mês.

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

public virtual void PerformMonthEndTransactions() { }

O código anterior mostra como você usa a virtual palavra-chave para declarar um método na classe base para o qual uma classe derivada pode fornecer uma implementação diferente. Um virtual método é um método onde qualquer classe derivada pode optar por reimplementar. As classes derivadas usam a override palavra-chave para definir a nova implementação. Normalmente, você se refere a isso como "substituindo a implementação da classe base". A virtual palavra-chave especifica que as classes derivadas podem substituir o comportamento. Você também pode declarar abstract métodos em que as classes derivadas devem substituir o comportamento. A classe base não fornece uma implementação para um abstract método. Em seguida, você precisa definir a implementação para duas das novas classes que você criou. Comece com o 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 LineOfCreditAccountarquivo . O código anula 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 GiftCardAccount classe precisa de duas alterações para implementar sua funcionalidade de fim de mês. Primeiro, modifique o construtor para incluir um valor opcional para adicionar 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 monthlyDeposit valor para que os chamadores possam omitir um 0 para nenhum depósito mensal. Em seguida, substitua o PerformMonthEndTransactions método 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 depósito mensal definido no construtor. Adicione o seguinte código ao Main método para testar essas alterações para o GiftCardAccount e o 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 o 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());

Quando você adiciona o código anterior e executa o programa, você verá algo como o 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

Nota

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

Esse código falha porque o BankAccount pressupõe que o saldo inicial deve ser maior que 0. Outro pressuposto da BankAccount classe é que o saldo não pode ser negativo. Em vez disso, qualquer retirada que extrapole a conta é rejeitada. Ambos os pressupostos têm de mudar. A linha de crédito começa em 0, e geralmente terá um saldo negativo. Além disso, se um cliente pedir muito dinheiro emprestado, ele incorre em uma taxa. A transação é aceita, apenas custa mais. A primeira regra pode ser implementada BankAccount adicionando um argumento opcional ao construtor que especifica o saldo mínimo. A predefinição é 0. A segunda regra requer um mecanismo que permita que classes derivadas modifiquem o algoritmo padrão. Em certo sentido, 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 opcional minimumBalance . Este novo construtor faz todas as ações feitas pelo construtor existente. Além disso, 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 construtores 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 minimumBalance campo é marcado como readonly. Isso significa que o valor não pode ser alterado depois que o objeto é construído. Uma vez que um BankAccount é criado, o minimumBalance não pode mudar. Em segundo lugar, o construtor que usa dois parâmetros como : this(name, initialBalance, 0) { } sua implementação. A : this() expressão 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, mesmo que o código do cliente possa escolher um dos muitos construtores.

Esta implementação só é necessária MakeDeposit se o saldo inicial for superior 0a . Isso preserva a regra de que os depósitos devem ser positivos, mas permite que a conta de crédito abra com saldo 0 .

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

if (Balance - amount < _minimumBalance)

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

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

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

Regras diferentes do cheque especial

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

Uma técnica é definir uma função virtual onde você implementa o comportamento necessário. A BankAccount classe refatora o MakeWithdrawal método em dois métodos. O novo método faz a ação especificada quando a retirada 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 as classes derivadas possam mudar o comportamento. O tipo de retorno é um Transaction?arquivo . A ? anotação indica que o método pode retornar null. Adicione a seguinte implementação no LineOfCreditAccount para cobrar uma taxa quando o limite de retirada 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 levantamento não ultrapassar o limite, o método devolve uma null transação. Isso indica que não há taxa. Teste essas alterações adicionando o seguinte código ao seu Main método na Program classe:

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ê ficou preso, você pode ver a fonte para este tutorial em nosso repositório GitHub.

Este tutorial demonstrou muitas das técnicas usadas na programação orientada a objetos:

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