Objektorienterad programmering (C#)
C# är ett objektorienterat programmeringsspråk. De fyra grundläggande principerna för objektorienterad programmering är:
- Abstraktion Modellering av relevanta attribut och interaktioner för entiteter som klasser för att definiera en abstrakt representation av ett system.
- Inkapsling Dölj det interna tillståndet och funktionerna i ett objekt och endast tillåta åtkomst via en offentlig uppsättning funktioner.
- Arvsförmågan att skapa nya abstraktioner baserat på befintliga abstraktioner.
- Polymorfism Förmåga att implementera ärvda egenskaper eller metoder på olika sätt i flera abstraktioner.
I föregående självstudie , introduktion till klasser som du såg både abstraktion och inkapsling. Klassen BankAccount
tillhandahöll en abstraktion för begreppet bankkonto. Du kan ändra dess implementering utan att påverka någon av koden som använde BankAccount
klassen. Både klasserna BankAccount
och Transaction
ger inkapsling av de komponenter som behövs för att beskriva dessa begrepp i kod.
I den här självstudien utökar du programmet till att använda arv och polymorfism för att lägga till nya funktioner. Du kommer också att lägga till funktioner i BankAccount
klassen och dra nytta av de abstraktions- och inkapslingstekniker som du lärde dig i föregående självstudie.
Skapa olika typer av konton
När du har skapat det här programmet får du begäranden om att lägga till funktioner i det. Det fungerar bra i den situation där det bara finns en bankkontotyp. Med tiden begärs ändringar i behov och relaterade kontotyper:
- Ett ränteintäkterkonto som uppbär ränta i slutet av varje månad.
- En kreditrad som kan ha ett negativt saldo, men när det finns ett saldo debiteras en ränteavgift varje månad.
- Ett förbetalt presentkortskonto som börjar med en enda insättning och som endast kan betalas av. Den kan fyllas på en gång i början av varje månad.
Alla dessa olika konton liknar BankAccount
den klass som definierades i den tidigare självstudien. Du kan kopiera koden, byta namn på klasserna och göra ändringar. Den tekniken skulle fungera på kort sikt, men det skulle vara mer arbete över tid. Alla ändringar kopieras i alla berörda klasser.
I stället kan du skapa nya bankkontotyper som ärver metoder och data från klassen BankAccount
som skapades i föregående självstudie. Dessa nya klasser kan utöka BankAccount
klassen med det specifika beteende som krävs för varje typ:
public class InterestEarningAccount : BankAccount
{
}
public class LineOfCreditAccount : BankAccount
{
}
public class GiftCardAccount : BankAccount
{
}
Var och en av dessa klasser ärver det delade beteendet från deras delade basklass, BankAccount
klassen. Skriv implementeringarna för nya och olika funktioner i var och en av de härledda klasserna. Dessa härledda klasser har redan alla beteenden som definierats i BankAccount
klassen.
Det är en bra idé att skapa varje ny klass i en annan källfil. I Visual Studio kan du högerklicka på projektet och välja Lägg till klass för att lägga till en ny klass i en ny fil. I Visual Studio Code väljer du Arkiv och sedan Nytt för att skapa en ny källfil. I båda verktygen namnger du filen så att den matchar klassen: InterestEarningAccount.cs, LineOfCreditAccount.cs och GiftCardAccount.cs.
När du skapar klasserna enligt föregående exempel ser du att ingen av dina härledda klasser kompileras. En konstruktor ansvarar för att initiera ett objekt. En konstruktor för härledd klass måste initiera den härledda klassen och ge instruktioner om hur du initierar basklassobjektet som ingår i den härledda klassen. Rätt initiering sker normalt utan extra kod. Klassen BankAccount
deklarerar en offentlig konstruktor med följande signatur:
public BankAccount(string name, decimal initialBalance)
Kompilatorn genererar inte någon standardkonstruktor när du definierar en konstruktor själv. Det innebär att varje härledd klass uttryckligen måste anropa den här konstruktorn. Du deklarerar en konstruktor som kan skicka argument till basklasskonstruktorn. Följande kod visar konstruktorn för InterestEarningAccount
:
public InterestEarningAccount(string name, decimal initialBalance) : base(name, initialBalance)
{
}
Parametrarna för den nya konstruktorn matchar parametertypen och namnen på basklasskonstruktorn. Du använder syntaxen : base()
för att ange ett anrop till en basklasskonstruktor. Vissa klasser definierar flera konstruktorer, och med den här syntaxen kan du välja vilken basklasskonstruktor du anropar. När du har uppdaterat konstruktorerna kan du utveckla koden för var och en av de härledda klasserna. Kraven för de nya klasserna kan anges på följande sätt:
- Ett ränteintäkterkonto:
- Får en kredit på 2 % av månadsslutssaldot.
- En kreditrad:
- Kan ha ett negativt saldo, men inte vara större i absolut värde än kreditgränsen.
- Debiteras en ränteavgift varje månad där saldot i slutet av månaden inte är 0.
- Kommer att medföra en avgift för varje uttag som går över kreditgränsen.
- Ett presentkortskonto:
- Kan fyllas på med ett angivet belopp en gång i månaden, den sista dagen i månaden.
Du kan se att alla tre av dessa kontotyper har en åtgärd som sker i slutet av varje månad. Varje kontotyp utför dock olika uppgifter. Du använder polymorfism för att implementera den här koden. Skapa en enskild virtual
metod i BankAccount
klassen:
public virtual void PerformMonthEndTransactions() { }
Föregående kod visar hur du använder nyckelordet virtual
för att deklarera en metod i basklassen som en härledd klass kan tillhandahålla en annan implementering för. En virtual
metod är en metod där en härledd klass kan välja att omimplementeras. De härledda klasserna använder nyckelordet override
för att definiera den nya implementeringen. Vanligtvis refererar du till detta som "åsidosättande av basklassimplementeringen". Nyckelordet virtual
anger att härledda klasser kan åsidosätta beteendet. Du kan också deklarera abstract
metoder där härledda klasser måste åsidosätta beteendet. Basklassen tillhandahåller ingen implementering för en abstract
metod. Därefter måste du definiera implementeringen för två av de nya klasser som du har skapat. Börja med InterestEarningAccount
:
public override void PerformMonthEndTransactions()
{
if (Balance > 500m)
{
decimal interest = Balance * 0.02m;
MakeDeposit(interest, DateTime.Now, "apply monthly interest");
}
}
Lägg till följande kod i LineOfCreditAccount
. Koden negerar saldot för att beräkna en positiv ränteavgift som tas ut från kontot:
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");
}
}
Klassen GiftCardAccount
behöver två ändringar för att implementera sin månadsslutsfunktion. Ändra först konstruktorn så att den innehåller ett valfritt belopp att lägga till varje månad:
private readonly decimal _monthlyDeposit = 0m;
public GiftCardAccount(string name, decimal initialBalance, decimal monthlyDeposit = 0) : base(name, initialBalance)
=> _monthlyDeposit = monthlyDeposit;
Konstruktorn tillhandahåller ett standardvärde för monthlyDeposit
värdet så att anropare kan utelämna en 0
för ingen månatlig insättning. Åsidosätt PerformMonthEndTransactions
sedan metoden för att lägga till den månatliga insättningen, om den har angetts till ett värde som inte är noll i konstruktorn:
public override void PerformMonthEndTransactions()
{
if (_monthlyDeposit != 0)
{
MakeDeposit(_monthlyDeposit, DateTime.Now, "Add monthly deposit");
}
}
Åsidosättningen tillämpar den månatliga insättningsuppsättningen i konstruktorn. Lägg till följande kod i Main
metoden för att testa dessa ändringar för GiftCardAccount
och 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());
Kontrollera resultatet. Lägg nu till en liknande uppsättning testkod för 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());
När du lägger till föregående kod och kör programmet visas något som liknar följande fel:
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
Kommentar
De faktiska utdata innehåller den fullständiga sökvägen till mappen med projektet. Mappnamnen utelämnades för korthet. Beroende på kodformatet kan radnumren också vara något annorlunda.
Den här koden misslyckas eftersom BankAccount
förutsätter att det ursprungliga saldot måste vara större än 0. Ett annat antagande som bakats in i BankAccount
klassen är att saldot inte kan bli negativt. I stället avvisas alla uttag som övertrassrar kontot. Båda dessa antaganden måste ändras. Kreditkontot börjar vid 0 och har i allmänhet ett negativt saldo. Dessutom, om en kund lånar för mycket pengar, de ådrar sig en avgift. Transaktionen accepteras, det kostar bara mer. Den första regeln kan implementeras genom att lägga till ett valfritt argument i BankAccount
konstruktorn som anger det minsta saldot. Standardvärdet är 0
. Den andra regeln kräver en mekanism som gör det möjligt för härledda klasser att ändra standardalgoritmen. På sätt och vis frågar basklassen den härledda typen vad som ska hända när det finns en övertrassering. Standardbeteendet är att avvisa transaktionen genom att utlösa ett undantag.
Vi börjar med att lägga till en andra konstruktor som innehåller en valfri minimumBalance
parameter. Den här nya konstruktorn utför alla åtgärder som utförs av den befintliga konstruktorn. Dessutom anger den minsta saldoegenskapen. Du kan kopiera brödtexten för den befintliga konstruktorn, men det innebär två platser att ändra i framtiden. I stället kan du använda konstruktorlänkning för att låta en konstruktor anropa en annan. Följande kod visar de två konstruktorerna och det nya ytterligare fältet:
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");
}
Föregående kod visar två nya tekniker. Först markeras fältet minimumBalance
som readonly
. Det innebär att värdet inte kan ändras när objektet har konstruerats. När en BankAccount
har skapats minimumBalance
kan inte ändras. För det andra använder : this(name, initialBalance, 0) { }
konstruktorn som använder två parametrar som implementering. Uttrycket : this()
anropar den andra konstruktorn, den med tre parametrar. Med den här tekniken kan du ha en enda implementering för att initiera ett objekt även om klientkoden kan välja en av många konstruktorer.
Den här implementeringen anropar MakeDeposit
endast om det ursprungliga saldot är större än 0
. Det bevarar regeln att insättningar måste vara positiva, men låter kreditkontot öppnas med ett 0
saldo.
Nu när BankAccount
klassen har ett skrivskyddat fält för minsta saldo är den slutliga ändringen att ändra hårdkoden 0
till minimumBalance
i MakeWithdrawal
-metoden:
if (Balance - amount < _minimumBalance)
När du har utökat BankAccount
klassen kan du ändra LineOfCreditAccount
konstruktorn så att den anropar den nya baskonstruktorn, enligt följande kod:
public LineOfCreditAccount(string name, decimal initialBalance, decimal creditLimit) : base(name, initialBalance, -creditLimit)
{
}
Observera att LineOfCreditAccount
konstruktorn ändrar tecknet för parametern creditLimit
så att den matchar parameterns minimumBalance
innebörd.
Olika regler för övertrassering
Den sista funktionen att lägga till gör det möjligt LineOfCreditAccount
att ta ut en avgift för att gå över kreditgränsen istället för att vägra transaktionen.
En teknik är att definiera en virtuell funktion där du implementerar det beteende som krävs. Klassen BankAccount
omstrukturerar MakeWithdrawal
metoden till två metoder. Den nya metoden utför den angivna åtgärden när uttaget tar saldot under minimivärdet. Den befintliga MakeWithdrawal
metoden har följande kod:
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);
}
Ersätt den med följande kod:
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;
}
}
Den tillagda metoden är protected
, vilket innebär att den endast kan anropas från härledda klasser. Den deklarationen hindrar andra klienter från att anropa metoden. Det är också virtual
så att härledda klasser kan ändra beteendet. Returtypen är en Transaction?
. Kommentaren ?
anger att metoden kan returnera null
. Lägg till följande implementering i LineOfCreditAccount
för att ta ut en avgift när uttagsgränsen överskrids:
protected override Transaction? CheckWithdrawalLimit(bool isOverdrawn) =>
isOverdrawn
? new Transaction(-20, DateTime.Now, "Apply overdraft fee")
: default;
Åsidosättningen returnerar en avgiftstransaktion när kontot övertrasseras. Om tillbakadragandet inte överskrider gränsen returnerar metoden en null
transaktion. Det tyder på att det inte finns någon avgift. Testa dessa ändringar genom att lägga till följande kod i din Main
metod i Program
klassen:
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());
Kör programmet och kontrollera resultatet.
Sammanfattning
Om du fastnade kan du se källan för den här självstudien i vår GitHub-lagringsplats.
Den här självstudien visade många av de tekniker som används i objektorienterad programmering:
- Du använde abstraktion när du definierade klasser för var och en av de olika kontotyperna. Dessa klasser beskrev beteendet för den typen av konto.
- Du använde inkapsling när du hade många detaljer
private
i varje klass. - Du använde Arv när du använde implementeringen som redan skapats i
BankAccount
klassen för att spara kod. - Du använde polymorfism när du skapade
virtual
metoder som härledda klasser kunde åsidosätta för att skapa ett specifikt beteende för den kontotypen.