Objectgeoriënteerd programmeren (C#)

C# is een objectgeoriënteerde programmeertaal. De vier basisprincipes van objectgeoriënteerd programmeren zijn:

  • Abstractie modelleren van de relevante kenmerken en interacties van entiteiten als klassen om een abstracte weergave van een systeem te definiëren.
  • Inkapseling verbergt de interne status en functionaliteit van een object en staat alleen toegang toe via een openbare set functies.
  • Overnamemogelijkheid om nieuwe abstracties te maken op basis van bestaande abstracties.
  • Polymorfisme Mogelijkheid om overgenomen eigenschappen of methoden op verschillende manieren te implementeren in meerdere abstracties.

In de voorgaande zelfstudie hebt u een inleiding tot klassen gezien die u zowel abstractie als inkapseling hebt gezien. De BankAccount klasse bood een abstractie voor het concept van een bankrekening. U kunt de implementatie ervan wijzigen zonder dat dit van invloed is op een van de code die de BankAccount klasse heeft gebruikt. Zowel de als Transaction de BankAccount klassen bieden inkapseling van de onderdelen die nodig zijn om deze concepten in code te beschrijven.

In deze zelfstudie gaat u die toepassing uitbreiden om gebruik te maken van overname en polymorfisme om nieuwe functies toe te voegen. U voegt ook functies toe aan de BankAccount klasse, waarbij u gebruikmaakt van de abstractie - en inkapselingstechnieken die u in de vorige zelfstudie hebt geleerd.

Verschillende typen accounts maken

Nadat u dit programma hebt gebouwd, krijgt u aanvragen om er functies aan toe te voegen. Het werkt prima in de situatie waarin er slechts één bankrekeningtype is. Na verloop van tijd worden wijzigingen en gerelateerde accounttypen aangevraagd:

  • Een renteverdieningsrekening die aan het einde van elke maand rente oplevert.
  • Een kredietregel die een negatief saldo kan hebben, maar wanneer er een saldo is, is er elke maand een rentelast.
  • Een vooraf betaald cadeaukaartaccount dat begint met één storting en alleen kan worden betaald. Deze kan aan het begin van elke maand opnieuw worden ingevuld.

Al deze verschillende accounts zijn vergelijkbaar met BankAccount de klasse die in de eerdere zelfstudie is gedefinieerd. U kunt die code kopiëren, de klassen een andere naam geven en wijzigingen aanbrengen. Die techniek zou op korte termijn werken, maar het zou in de loop van de tijd meer werk zijn. Eventuele wijzigingen worden gekopieerd in alle betrokken klassen.

In plaats daarvan kunt u nieuwe bankrekeningtypen maken die methoden en gegevens overnemen van de BankAccount klasse die in de vorige zelfstudie is gemaakt. Deze nieuwe klassen kunnen de BankAccount klasse uitbreiden met het specifieke gedrag dat nodig is voor elk type:

public class InterestEarningAccount : BankAccount
{
}

public class LineOfCreditAccount : BankAccount
{
}

public class GiftCardAccount : BankAccount
{
}

Elk van deze klassen neemt het gedeelde gedrag over van hun gedeelde basisklasse, de BankAccount klasse. Schrijf de implementaties voor nieuwe en verschillende functionaliteit in elk van de afgeleide klassen. Deze afgeleide klassen hebben al het gedrag dat in de BankAccount klasse is gedefinieerd.

Het is een goede gewoonte om elke nieuwe klasse in een ander bronbestand te maken. In Visual Studio kunt u met de rechtermuisknop op het project klikken en klasse toevoegen selecteren om een nieuwe klasse toe te voegen aan een nieuw bestand. Selecteer in Visual Studio Code bestand en vervolgens Nieuw om een nieuw bronbestand te maken. Geef in beide hulpprogramma's het bestand een naam die overeenkomt met de klasse: InterestEarningAccount.cs, LineOfCreditAccount.cs en GiftCardAccount.cs.

Wanneer u de klassen maakt zoals wordt weergegeven in het voorgaande voorbeeld, zult u merken dat geen van uw afgeleide klassen compileert. Een constructor is verantwoordelijk voor het initialiseren van een object. Een afgeleide klasseconstructor moet de afgeleide klasse initialiseren en instructies geven voor het initialiseren van het basisklasseobject dat is opgenomen in de afgeleide klasse. De juiste initialisatie vindt normaal plaats zonder extra code. De BankAccount klasse declareert één openbare constructor met de volgende handtekening:

public BankAccount(string name, decimal initialBalance)

De compiler genereert geen standaardconstructor wanneer u zelf een constructor definieert. Dit betekent dat elke afgeleide klasse deze constructor expliciet moet aanroepen. U declareert een constructor die argumenten kan doorgeven aan de basisklasseconstructor. De volgende code toont de constructor voor het InterestEarningAccountvolgende:

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

De parameters voor deze nieuwe constructor komen overeen met het parametertype en de namen van de basisklasseconstructor. U gebruikt de : base() syntaxis om een aanroep naar een basisklasseconstructor aan te geven. Sommige klassen definiëren meerdere constructors en met deze syntaxis kunt u kiezen welke basisklasseconstructor u aanroept. Zodra u de constructors hebt bijgewerkt, kunt u de code ontwikkelen voor elk van de afgeleide klassen. De vereisten voor de nieuwe klassen kunnen als volgt worden vermeld:

  • Een renteverdieningsrekening:
    • Krijgt een tegoed van 2% van het eindsaldo van de maand.
  • Een kredietregel:
    • Kan een negatief saldo hebben, maar niet groter zijn in absolute waarde dan de kredietlimiet.
    • Er worden elke maand rentekosten in rekening gebracht waarbij het eind van de maandsaldo niet 0 is.
    • Er worden kosten in rekening gebracht voor elke opname die de kredietlimiet overschrijdt.
  • Een cadeaukaartaccount:
    • Kan op de laatste dag van de maand opnieuw worden gevuld met een opgegeven bedrag per maand.

U kunt zien dat alle drie deze accounttypen een actie hebben die aan het einde van elke maand plaatsvindt. Elk accounttype voert echter verschillende taken uit. U gebruikt polymorfisme om deze code te implementeren. Maak één virtual methode in de BankAccount klasse:

public virtual void PerformMonthEndTransactions() { }

De voorgaande code laat zien hoe u het virtual trefwoord gebruikt om een methode in de basisklasse te declareren waarvoor een afgeleide klasse een andere implementatie kan bieden. Een virtual methode is een methode waarbij elke afgeleide klasse ervoor kan kiezen om opnieuw te worden gebruikt. De afgeleide klassen gebruiken het override trefwoord om de nieuwe implementatie te definiëren. Doorgaans verwijst u hiernaar als 'de implementatie van de basisklasse overschrijven'. Het virtual trefwoord geeft aan dat afgeleide klassen het gedrag kunnen overschrijven. U kunt ook methoden declareren abstract waarbij afgeleide klassen het gedrag moeten overschrijven. De basisklasse biedt geen implementatie voor een abstract methode. Vervolgens moet u de implementatie definiëren voor twee van de nieuwe klassen die u hebt gemaakt. Begin met de InterestEarningAccount:

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

Voeg de volgende code toe aan de LineOfCreditAccount. De code ontkent het saldo om een positieve rentelast te berekenen die uit het account wordt ingetrokken:

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

De GiftCardAccount klasse heeft twee wijzigingen nodig om de functionaliteit van de maand te implementeren. Pas eerst de constructor aan om een optioneel bedrag op te nemen dat elke maand moet worden toegevoegd:

private readonly decimal _monthlyDeposit = 0m;

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

De constructor biedt een standaardwaarde voor de monthlyDeposit waarde, zodat bellers kunnen weglaten voor 0 geen maandelijkse storting. Overschrijf vervolgens de methode om de PerformMonthEndTransactions maandelijkse storting toe te voegen, als deze is ingesteld op een niet-nulwaarde in de constructor:

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

De onderdrukking past de maandelijkse storting toe die in de constructor is ingesteld. Voeg de volgende code toe aan de Main methode om deze wijzigingen voor en GiftCardAccount het InterestEarningAccountvolgende te testen:

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

Controleer de resultaten. Voeg nu een vergelijkbare set testcode toe voor het LineOfCreditAccountvolgende:

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

Wanneer u de voorgaande code toevoegt en het programma uitvoert, ziet u ongeveer de volgende fout:

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

Notitie

De werkelijke uitvoer bevat het volledige pad naar de map met het project. De mapnamen zijn weggelaten ter beknoptheid. Afhankelijk van de code-indeling kunnen de regelnummers enigszins afwijken.

Deze code mislukt omdat BankAccount wordt ervan uitgegaan dat het oorspronkelijke saldo groter moet zijn dan 0. Een andere aanname die in de BankAccount klas is gebakken, is dat het saldo niet negatief kan gaan. In plaats daarvan wordt elke intrekking die het account overschrijft, geweigerd. Beide veronderstellingen moeten veranderen. De regel van de kredietrekening begint bij 0 en heeft over het algemeen een negatief saldo. Als een klant te veel geld lenen, worden er ook kosten in rekening gebracht. De transactie wordt geaccepteerd, het kost gewoon meer. De eerste regel kan worden geïmplementeerd door een optioneel argument toe te voegen aan de BankAccount constructor waarmee het minimumsaldo wordt opgegeven. De standaardwaarde is 0. Voor de tweede regel is een mechanisme vereist waarmee afgeleide klassen het standaardalgoritmen kunnen wijzigen. In zekere zin vraagt de basisklasse het afgeleide type wat er moet gebeuren wanneer er sprake is van een overdraft. Het standaardgedrag is het weigeren van de transactie door een uitzondering te genereren.

Laten we beginnen met het toevoegen van een tweede constructor die een optionele minimumBalance parameter bevat. Deze nieuwe constructor voert alle acties uit die door de bestaande constructor worden uitgevoerd. Daarnaast wordt de minimale saldoeigenschap ingesteld. U kunt de hoofdtekst van de bestaande constructor kopiëren, maar dat betekent dat er in de toekomst twee locaties moeten worden gewijzigd. In plaats daarvan kunt u constructorchaining gebruiken om één constructor een andere aan te roepen. De volgende code toont de twee constructors en het nieuwe extra veld:

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

In de voorgaande code ziet u twee nieuwe technieken. Eerst wordt het minimumBalance veld gemarkeerd als readonly. Dit betekent dat de waarde niet kan worden gewijzigd nadat het object is samengesteld. Zodra een BankAccount is gemaakt, kan het minimumBalance niet meer worden gewijzigd. Ten tweede gebruikt de constructor die twee parameters gebruikt : this(name, initialBalance, 0) { } als implementatie. De : this() expressie roept de andere constructor aan, de constructor met drie parameters. Met deze techniek kunt u één implementatie hebben voor het initialiseren van een object, ook al kan clientcode een van de vele constructors kiezen.

Deze implementatie roept MakeDeposit alleen aan als het oorspronkelijke saldo groter is dan 0. Dat behoudt de regel dat deposito's positief moeten zijn, maar laat de tegoedrekening met een 0 saldo open.

Nu de BankAccount klasse een alleen-lezenveld heeft voor het minimale saldo, is de laatste wijziging het wijzigen van de harde code 0minimumBalance in de MakeWithdrawal methode:

if (Balance - amount < _minimumBalance)

Nadat u de BankAccount klasse hebt uitgebreid, kunt u de LineOfCreditAccount constructor wijzigen om de nieuwe basisconstructor aan te roepen, zoals wordt weergegeven in de volgende code:

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

U ziet dat de LineOfCreditAccount constructor het teken van de creditLimit parameter wijzigt, zodat deze overeenkomt met de betekenis van de minimumBalance parameter.

Verschillende regels voor overdraft

Met de laatste functie die moet worden toegevoegd, kunnen kosten LineOfCreditAccount in rekening worden gebracht voor het overschrijden van de kredietlimiet in plaats van de transactie te weigeren.

Een techniek is het definiëren van een virtuele functie waarin u het vereiste gedrag implementeert. De BankAccount klasse herstructureert de MakeWithdrawal methode in twee methoden. De nieuwe methode voert de opgegeven actie uit wanneer de intrekking het saldo onder het minimum haalt. De bestaande MakeWithdrawal methode heeft de volgende code:

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

Vervang deze door de volgende code:

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

De toegevoegde methode is protected, wat betekent dat deze alleen kan worden aangeroepen vanuit afgeleide klassen. Deze declaratie voorkomt dat andere clients de methode aanroepen. Het is ook virtual zo dat afgeleide klassen het gedrag kunnen wijzigen. Het retourtype is een Transaction?. De ? aantekening geeft aan dat de methode kan retourneren null. Voeg de volgende implementatie toe in de LineOfCreditAccount om een vergoeding in rekening te brengen wanneer de opnamelimiet wordt overschreden:

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

De onderdrukking retourneert een vergoedingstransactie wanneer het account wordt overschreven. Als de intrekking niet de limiet overschrijdt, retourneert de methode een null transactie. Dat geeft aan dat er geen kosten in rekening worden gebracht. Test deze wijzigingen door de volgende code toe te voegen aan uw Main methode in de Program klasse:

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

Voer het programma uit en controleer de resultaten.

Samenvatting

Als u vastloopt, kunt u de bron voor deze zelfstudie zien in onze GitHub-opslagplaats.

In deze zelfstudie hebt u veel van de technieken gedemonstreerd die worden gebruikt in objectgeoriënteerde programmering:

  • U hebt Abstraction gebruikt bij het definiëren van klassen voor elk van de verschillende accounttypen. Deze klassen hebben het gedrag voor dat type account beschreven.
  • U hebt Inkapseling gebruikt toen u veel details private in elke klas bewaarde.
  • U hebt Overname gebruikt toen u de implementatie die al in de BankAccount klasse is gemaakt, hebt gebruikt om code op te slaan.
  • U hebt Polymorfisme gebruikt bij het maken virtual van methoden die afgeleide klassen kunnen overschrijven om specifiek gedrag voor dat accounttype te maken.