Programmazione orientata agli oggetti (C#)

C# è un linguaggio di programmazione orientato agli oggetti. I quattro principi di base della programmazione orientata agli oggetti sono:

  • Astrazione Modellazione degli attributi e delle interazioni pertinenti delle entità come classi per definire una rappresentazione astratta di un sistema.
  • Incapsulamento Nascondere lo stato interno e la funzionalità di un oggetto e consentire l'accesso solo tramite un set pubblico di funzioni.
  • Ereditarietà Possibilità di creare nuove astrazioni in base alle astrazioni esistenti.
  • Polimorfismo Capacità di implementare proprietà o metodi ereditati in modi diversi tra più astrazioni.

Nell'esercitazione precedente, introduzione alle classi che hanno visto sia l'astrazione che l'incapsulamento. La classe BankAccount ha fornito un'astrazione per il concetto di conto bancario. È possibile modificarne l'implementazione senza influire sul codice che ha usato la classe BankAccount. Entrambe le classi BankAccount e Transaction forniscono l'incapsulamento dei componenti necessari per descrivere tali concetti nel codice.

In questa esercitazione si estenderà l'applicazione per usare l'ereditarietà e il polimorfismo per aggiungere nuove funzionalità. Si aggiungeranno anche funzionalità alla classe BankAccount, sfruttando i vantaggi delle tecniche di astrazione e incapsulamento apprese nell'esercitazione precedente.

Creare diversi tipi di account

Dopo aver compilato questo programma, si ricevono richieste di aggiunta di funzionalità. Funziona bene nella situazione in cui c'è un solo tipo di conto bancario. Nel corso del tempo, vengono richiesti i tipi di conto correlati e le esigenze:

  • Un conto degli utili di interesse che accumula interessi alla fine di ogni mese.
  • Una linea di credito che può avere un saldo negativo, ma, quando è presente un saldo, è presente un addebito di interesse ogni mese.
  • Un conto carta regalo con pagamento anticipato che inizia con un solo deposito e può essere estinto. Può essere ricaricato una volta all'inizio di ogni mese.

Tutti questi account diversi sono simili alla classe BankAccount definita nell'esercitazione precedente. È possibile copiare il codice, rinominare le classi e apportare modifiche. Questa tecnica funzionerebbe nel breve termine, ma sarebbe più impegnativa nel tempo. Eventuali modifiche verranno copiate in tutte le classi interessate.

È invece possibile creare nuovi tipi di conto bancario che ereditano metodi e dati dalla classe BankAccount creata nell'esercitazione precedente. Queste nuove classi possono estendere la classe BankAccount con il comportamento specifico necessario per ogni tipo:

public class InterestEarningAccount : BankAccount
{
}

public class LineOfCreditAccount : BankAccount
{
}

public class GiftCardAccount : BankAccount
{
}

Ognuna di queste classi eredita il comportamento condiviso dalla classe base condivisa, ovvero dalla classe BankAccount. Scrivere le implementazioni per funzionalità nuove e diverse in ognuna delle classi derivate. Queste classi derivate hanno già tutto il comportamento definito nella classe BankAccount.

È consigliabile creare ogni nuova classe in un file di origine diverso. In Visual Studioè possibile fare clic con il pulsante destro del mouse sul progetto e selezionare Aggiungi classe per aggiungere una nuova classe in un nuovo file. In Visual Studio Code selezionare File e quindi Nuovo per creare un nuovo file di origine. In entrambi gli strumenti assegnare un nome al file in modo che corrisponda alla classe: InterestEarningAccount.cs, LineOfCreditAccount.cs e GiftCardAccount.cs.

Quando si creano le classi come illustrato nell'esempio precedente, si scopre che nessuna delle classi derivate viene compilata. Un costruttore è responsabile dell'inizializzazione di un oggetto. Un costruttore di classe derivata deve inizializzare la classe derivata e fornire istruzioni su come inizializzare l'oggetto classe di base incluso nella classe derivata. L'inizializzazione corretta avviene normalmente senza codice aggiuntivo. La classe BankAccount dichiara un costruttore pubblico con la firma seguente:

public BankAccount(string name, decimal initialBalance)

Il compilatore non genera un costruttore predefinito quando si definisce manualmente un costruttore. Ciò significa che ogni classe derivata deve chiamare in modo esplicito questo costruttore. Si dichiara un costruttore che può passare argomenti al costruttore della classe base. Il codice seguente illustra il costruttore per InterestEarningAccount:

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

I parametri di questo nuovo costruttore corrispondono al tipo di parametro e ai nomi del costruttore della classe base. Usare la sintassi : base() per indicare una chiamata a un costruttore della classe di base. Alcune classi definiscono più costruttori e questa sintassi consente di selezionare il costruttore della classe di base chiamato. Dopo aver aggiornato i costruttori, è possibile sviluppare il codice per ognuna delle classi derivate. I requisiti per le nuove classi possono essere indicati come segue:

  • Conto degli utili di interesse:
    • Otterrà un credito del 2% del saldo di fine mese.
  • Una linea di credito:
    • Può avere un saldo negativo, ma non essere maggiore in valore assoluto rispetto al limite di credito.
    • Se il saldo di fine mese non è pari a 0, ogni mese verrà addebitato un interesse.
    • Verrà addebitata una commissione per ogni prelievo che supera il limite di credito.
  • Un conto carta regalo:
    • Può essere ricaricato con un importo specificato una volta al mese, l'ultimo giorno del mese.

È possibile notare che tutti e tre questi tipi di conto hanno un'azione che viene eseguita alla fine di ogni mese. Tuttavia, ogni tipo di conto esegue attività diverse. Usare il polimorfismo per implementare questo codice. Creare un singolo metodo virtual nella classe BankAccount:

public virtual void PerformMonthEndTransactions() { }

Il codice precedente illustra come usare la parola chiave virtual per dichiarare un metodo nella classe di base per cui una classe derivata può fornire un'implementazione diversa. Un metodo virtual è un metodo in cui qualsiasi classe derivata può scegliere di ripetere l'implementazione. Le classi derivate usano la parola chiave override per definire la nuova implementazione. In genere si fa riferimento a questo come "override dell'implementazione della classe di base". La parola chiave virtual specifica che le classi derivate possono eseguire l'override del comportamento. È anche possibile dichiarare abstract metodi in cui le classi derivate devono eseguire l'override del comportamento. La classe base non fornisce un'implementazione per un metodo abstract. Successivamente, è necessario definire l'implementazione per due delle nuove classi create. Iniziare con InterestEarningAccount:

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

Aggiungere il codice seguente a LineOfCreditAccount. Il codice nega il saldo per calcolare un addebito di interesse positivo prelevato dal conto:

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

La classe GiftCardAccount richiede due modifiche per implementare la funzionalità di fine mese. Prima di tutto, modificare il costruttore in modo da includere un importo facoltativo da aggiungere ogni mese:

private readonly decimal _monthlyDeposit = 0m;

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

Il costruttore fornisce un valore predefinito per il valore monthlyDeposit in modo che i chiamanti possano omettere un 0 senza deposito mensile. Eseguire quindi l'override del metodo PerformMonthEndTransactions per aggiungere il deposito mensile, se è stato impostato su un valore diverso da zero nel costruttore:

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

L'override applica il deposito mensile impostato nel costruttore. Aggiungere il codice seguente al metodo Main per testare queste modifiche per il GiftCardAccount e il 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());

Verificare i risultati. Aggiungere ora un set simile di codice di test per 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 si aggiunge il codice precedente e si esegue il programma, viene visualizzato un errore simile al seguente:

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

L'output effettivo include il percorso completo della cartella con il progetto. I nomi delle cartelle sono stati omessi per brevità. Inoltre, a seconda del formato di codice, i numeri di riga possono essere leggermente diversi.

Questo codice ha esito negativo perché BankAccount presuppone che il saldo iniziale sia maggiore di 0. Un altro presupposto inserito nella classe BankAccount è che il saldo non può andare in negativo. Invece, qualsiasi prelievo che mandi in rosso il conto viene rifiutato. Entrambi questi presupposti devono cambiare. La riga di conto di credito inizia a 0 e in genere avrà un saldo negativo. Inoltre, se un cliente prende in prestito troppo denaro, viene addebitata una commissione. La transazione viene accettata, costa solo di più. La prima regola può essere implementata aggiungendo un argomento facoltativo al costruttore BankAccount che specifica il saldo minimo. Il valore predefinito è 0. La seconda regola richiede un meccanismo che consente alle classi derivate di modificare l'algoritmo predefinito. In un certo senso, la classe base "chiede" al tipo derivato cosa dovrebbe accadere quando c’è uno scoperto. Il comportamento predefinito consiste nel rifiutare la transazione generando un'eccezione.

Si inizierà aggiungendo un secondo costruttore che include un parametro minimumBalance facoltativo. Questo nuovo costruttore esegue tutte le azioni eseguite dal costruttore esistente. Imposta inoltre la proprietà di saldo minimo. È possibile copiare il corpo del costruttore esistente, ma ciò significa che ci saranno due posizioni da cambiare in futuro. È invece possibile usare il concatenamento dei costruttori per fare in modo che un costruttore ne chiami un altro. Il codice seguente illustra i due costruttori e il nuovo campo aggiuntivo:

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

Il codice precedente illustra due nuove tecniche. In primo luogo, il campo minimumBalance viene contrassegnato come readonly. Ciò significa che il valore non può essere modificato dopo la costruzione dell'oggetto. Dopo aver creato un oggetto BankAccount, l'oggetto minimumBalance non può cambiare. In secondo luogo, il costruttore che accetta due parametri usa : this(name, initialBalance, 0) { } come implementazione. L'espressione : this() chiama l'altro costruttore, quello con tre parametri. Questa tecnica consente di avere una singola implementazione per l'inizializzazione di un oggetto anche se il codice client può scegliere uno dei molti costruttori.

Questa implementazione chiama MakeDeposit solo se il saldo iniziale è maggiore di 0. Ciò mantiene la regola che i depositi devono essere positivi, ma consente l'apertura del conto di credito con un saldo 0.

Ora che la classe BankAccount ha un campo di sola lettura per il saldo minimo, la modifica finale consiste nel modificare il codice rigido 0 in minimumBalance nel metodo MakeWithdrawal:

if (Balance - amount < _minimumBalance)

Dopo aver esteso la classe BankAccount, è possibile modificare il costruttore LineOfCreditAccount per chiamare il nuovo costruttore di base, come illustrato nel codice seguente:

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

Si noti che il costruttore LineOfCreditAccount modifica il segno del parametro creditLimit in modo che corrisponda al significato del parametro minimumBalance.

Regole di overdraft diverse

L'ultima funzionalità da aggiungere consente a LineOfCreditAccount di addebitare una commissione per far superare il limite di credito anziché rifiutare la transazione.

Una tecnica consiste nel definire una funzione virtuale in cui si implementa il comportamento richiesto. La classe BankAccount esegue il refactoring del metodo MakeWithdrawal in due metodi. Il nuovo metodo esegue l'azione specificata quando il prelievo accetta il saldo al di sotto del minimo. Il metodo MakeWithdrawal esistente ha il codice seguente:

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

Sostituirlo con il codice seguente:

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

Il metodo aggiunto è protected, il che significa che può essere chiamato solo da classi derivate. Tale dichiarazione impedisce ad altri client di chiamare il metodo. È anche virtual in modo che le classi derivate possano modificare il comportamento. Il tipo restituito è Transaction?. L'annotazione ? indica che il metodo può restituire null. Aggiungere l'implementazione seguente in LineOfCreditAccount per addebitare una commissione quando viene superato il limite di prelievo:

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

L'override restituisce una transazione per la commissione quando si preleva dal conto. Se il prelievo non supera il limite, il metodo restituisce una transazione null. Ciò indica che non c'è alcuna commissione. Testare queste modifiche aggiungendo il codice seguente al metodo Main nella 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());

Eseguire il programma e controllare i risultati.

Riepilogo

In caso di problemi, è possibile visualizzare il codice sorgente per questa esercitazione nel repository GitHub.

Questa esercitazione ha illustrato molte delle tecniche usate nella programmazione orientata agli oggetti:

  • È stata usata l'astrazione quando sono state definite classi per ognuno dei diversi tipi di conto. Tali classi hanno descritto il comportamento per quel tipo di conto.
  • È stato usato l’incapsulamento quando sono stati mantenuti molti dettagli private in ogni classe.
  • È stata usata l’ereditarietà quando è stata usata l'implementazione già creata nella classe BankAccount per salvare il codice.
  • È stato usato il Polimorfismo quando sono stati creati dei metodi virtual che le classi derivate possono sovrascrivere per creare un comportamento specifico per tale tipo di conto.