Share via


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.