Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
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à Capacità di creare nuove astrazioni basandosi su astrazioni esistenti.
- Polimorfismo Possibilità di implementare proprietà o metodi ereditati in modi diversi tra più astrazioni.
Nell'esercitazione precedente, introduzione alle classi, hai visto sia l'astrazione che l'incapsulamento. La BankAccount
classe ha fornito un'astrazione per il concetto di conto bancario. È possibile modificarne l'implementazione senza influire sul codice che ha usato la BankAccount
classe . Sia la classe BankAccount
che la classe 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 BankAccount
classe, sfruttando le 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, le esigenze cambiano e vengono richiesti tipi di account correlati.
- 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 prepagato che inizia con un solo deposito e può essere utilizzato solo per estinguere il credito. Può essere ricaricato una volta all'inizio di ogni mese.
Tutti questi account diversi sono simili alla BankAccount
classe definita nell'esercitazione precedente. È possibile copiare il codice, rinominare le classi e apportare modifiche. Questa tecnica funzionerebbe a breve termine, ma sarebbe più lavoro 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 BankAccount
classe creata nell'esercitazione precedente. Queste nuove classi possono estendere la BankAccount
classe 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 BankAccount
classe . Scrivere le implementazioni per funzionalità nuove e diverse in ognuna delle classi derivate. Queste classi derivate hanno già tutto il comportamento definito nella BankAccount
classe .
È 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 BankAccount
classe 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. Usi 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 che genera interessi
- Riceverà un credito di 2% sul saldo di fine mese.
- Una riga di credito:
- Può avere un saldo negativo, ma non essere maggiore in valore assoluto rispetto al limite di credito.
- Verrà addebitato un addebito di interesse ogni mese in cui il saldo di fine mese non è 0.
- Verrà addebitata una tariffa per ogni prelievo che supera il limite di credito.
- Un conto di carta regalo
- Può essere riempito con un importo specificato una volta al mese, l'ultimo giorno del mese.
È possibile notare che tutti e tre questi tipi di account hanno un'azione che viene eseguita alla fine di ogni mese. Tuttavia, ogni tipo di account esegue attività diverse. Usare il polimorfismo per implementare questo codice. Creare un singolo virtual
metodo nella BankAccount
classe :
public virtual void PerformMonthEndTransactions() { }
Il codice precedente illustra come usare la virtual
parola chiave per dichiarare un metodo nella classe base per cui una classe derivata può fornire un'implementazione diversa. Un virtual
metodo è un metodo in cui qualsiasi classe derivata può scegliere di riesemplementare. Le classi derivate usano la override
parola chiave per definire la nuova implementazione. In genere ci si riferisce a questo come "sovrascrivere l'implementazione della classe base". La virtual
parola chiave 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 abstract
metodo. 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 ritirato 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 GiftCardAccount
classe richiede due modifiche per implementare la funzionalità di fine mese. Per prima cosa, modifica il costruttore per includere un importo opzionale 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 monthlyDeposit
valore, permettendo ai chiamanti di omettere un 0
quando non è previsto alcun deposito mensile. Eseguire quindi l'override del PerformMonthEndTransactions
metodo 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 Main
metodo per testare queste modifiche per 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());
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 ed 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
Annotazioni
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 BankAccount
classe è che il saldo non può andare negativo. Invece, qualsiasi prelievo che causa un scoperto del 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 tariffa. La transazione viene accettata, costa solo di più. La prima regola può essere implementata aggiungendo un argomento facoltativo al BankAccount
costruttore 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" il tipo derivato cosa dovrebbe accadere quando è presente un overdraft. Il comportamento predefinito consiste nel rifiutare la transazione generando un'eccezione.
Si inizierà aggiungendo un secondo costruttore che include un parametro facoltativo minimumBalance
. Questo nuovo costruttore esegue tutte le azioni eseguite dal costruttore esistente. Imposta inoltre la proprietà di bilanciamento minimo. È possibile copiare il corpo del costruttore esistente, ma ciò significa dover apportare modifiche in due punti in futuro. È invece possibile usare il concatenamento dei costruttori per fare in modo che un costruttore 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 minimumBalance
campo 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 0
saldo.
Ora che la BankAccount
classe ha un campo di sola lettura per il saldo minimo, l'ultima modifica è cambiare il codice rigido 0
a minimumBalance
nel metodo MakeWithdrawal
.
if (Balance - amount < _minimumBalance)
Dopo aver esteso la BankAccount
classe, è possibile modificare il LineOfCreditAccount
costruttore 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 LineOfCreditAccount
costruttore modifica il segno del creditLimit
parametro in modo che corrisponda al significato del minimumBalance
parametro .
Regole diverse sullo scoperto bancario
L'ultima funzionalità da aggiungere consente all'utente LineOfCreditAccount
di addebitare una commissione per 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
riscrive il metodo MakeWithdrawal
in due metodi. Il nuovo metodo esegue l'azione specificata quando il ritiro porta il saldo al di sotto del minimo. Il metodo esistente MakeWithdrawal
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 è un Transaction?
. L'annotazione ?
indica che il metodo può restituire null
. Aggiungere l'implementazione seguente in LineOfCreditAccount
per addebitare una tariffa 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 di tariffa quando l'account viene ritirato. Se il ritiro non supera il limite, il metodo restituisce una null
transazione. Ciò indica che non c'è alcuna tariffa. Testare queste modifiche aggiungendo il codice seguente al Main
metodo nella 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());
Eseguire il programma e controllare i risultati.
Riassunto
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 Object-Oriented:
- È stata usata l'astrazione quando sono state definite classi per ognuno dei diversi tipi di account. Tali classi hanno descritto il comportamento per quel tipo di account.
- Hai utilizzato l'incapsulamento quando hai conservato molti dettagli
private
in ogni classe. - Hai usato l'ereditarietà quando hai sfruttato l'implementazione già creata nella classe
BankAccount
per ridurre il codice. - Hai usato Polymorphism quando hai creato
virtual
metodi che le classi derivate possono sovrascrivere per creare un comportamento specifico per quel tipo di account.