Objektově orientované programování (C#)

C# je objektově orientovaný programovací jazyk. Čtyři základní principy objektově orientovaného programování jsou:

  • Abstrakce Modelování relevantních atributů a interakcí entit jako tříd definují abstraktní reprezentaci systému.
  • Zapouzdření Skrytí vnitřního stavu a funkčnosti objektu a povolení přístupu pouze prostřednictvím veřejné sady funkcí.
  • Dědičnost Schopnost vytvářet nové abstrakce na základě existujících abstrakcí.
  • Polymorfismus Schopnost implementovat zděděné vlastnosti nebo metody různými způsoby napříč několika abstrakcemi.

V předchozím kurzu jste viděli úvod do tříd, které jste viděli abstrakci i zapouzdření. Třída BankAccount poskytla abstrakci pro koncept bankovního účtu. Můžete upravit její implementaci, aniž by to ovlivnilo jakýkoli kód, který použil BankAccount třídu. Obě třídy BankAccountTransaction poskytují zapouzdření součástí potřebných k popisu těchto konceptů v kódu.

V tomto kurzu rozšíříte tuto aplikaci tak, aby využívala dědičnost a polymorfismus k přidání nových funkcí. Do třídy také přidáte funkce BankAccount s využitím abstrakce a zapouzdření technik, které jste se naučili v předchozím kurzu.

Vytvoření různých typů účtů

Po vytvoření tohoto programu získáte žádosti o přidání funkcí. Funguje skvěle v situaci, kdy existuje pouze jeden typ bankovního účtu. V průběhu času se vyžaduje změna potřeb a související typy účtů:

  • Účet pro získání úroku, který na konci každého měsíce nabíhá úrok.
  • Řádek kreditů, který může mít záporný zůstatek, ale pokud je zůstatek, každý měsíc se účtuje úrok.
  • Předplacený účet dárkové karty, který začíná jednou vkladem, a lze ji uhradit pouze. Na začátku každého měsíce je možné ho znovu doplnit.

Všechny tyto různé účty jsou podobné BankAccount třídě definované v předchozím kurzu. Tento kód můžete zkopírovat, přejmenovat třídy a provést úpravy. Tato technika by fungovala v krátkodobém horizontu, ale v průběhu času by to bylo více práce. Všechny změny by se zkopírovaly napříč všemi ovlivněnými třídami.

Místo toho můžete vytvořit nové typy bankovních účtů, které dědí metody a data z BankAccount třídy vytvořené v předchozím kurzu. Tyto nové třídy mohou rozšířit BankAccount třídu o specifické chování potřebné pro každý typ:

public class InterestEarningAccount : BankAccount
{
}

public class LineOfCreditAccount : BankAccount
{
}

public class GiftCardAccount : BankAccount
{
}

Každá z těchto tříd dědí sdílené chování ze své sdílené základní třídy, BankAccount třídy. Napište implementace pro nové a různé funkce v každé odvozené třídy. Tyto odvozené třídy již mají veškeré chování definované ve BankAccount třídě.

Je vhodné vytvořit každou novou třídu v jiném zdrojovém souboru. V sadě Visual Studio můžete kliknout pravým tlačítkem myši na projekt a vybrat přidat třídu pro přidání nové třídy do nového souboru. V editoru Visual Studio Code vyberte Soubor a potom Nový a vytvořte nový zdrojový soubor. V obou nástrojích pojmenujte soubor tak, aby odpovídal třídě: InterestEarningAccount.cs, LineOfCreditAccount.cs a GiftCardAccount.cs.

Když vytvoříte třídy, jak je znázorněno v předchozí ukázce, zjistíte, že žádná z vašich odvozených tříd se kompiluje. Konstruktor je zodpovědný za inicializaci objektu. Konstruktor odvozené třídy musí inicializovat odvozenou třídu a poskytnout pokyny k inicializaci objektu základní třídy zahrnuté do odvozené třídy. Správné inicializace obvykle probíhá bez dalšího kódu. Třída BankAccount deklaruje jeden veřejný konstruktor s následujícím podpisem:

public BankAccount(string name, decimal initialBalance)

Kompilátor negeneruje výchozí konstruktor při definování konstruktoru sami. To znamená, že každá odvozená třída musí explicitně volat tento konstruktor. Deklarujete konstruktor, který může předat argumenty konstruktoru základní třídy. Následující kód ukazuje konstruktor pro InterestEarningAccount:

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

Parametry tohoto nového konstruktoru odpovídají typu a názvům parametru konstruktoru základní třídy. Syntaxi použijete : base() k označení volání konstruktoru základní třídy. Některé třídy definují více konstruktorů a tato syntaxe umožňuje vybrat, který konstruktor základní třídy voláte. Po aktualizaci konstruktorů můžete vyvíjet kód pro každou odvozenou třídu. Požadavky na nové třídy lze uvést takto:

  • Účet pro získání úroku:
    • Získá kredit 2 % měsíčního zůstatku končícího měsíce.
  • Řádek kreditu:
    • Může mít záporný zůstatek, ale nesmí být větší v absolutní hodnotě než limit kreditu.
    • Každý měsíc se vám bude účtovat poplatek za úrok, kdy zůstatek na konci měsíce není 0.
    • Za každý výběr, který překročí limit kreditu, se bude účtují poplatky.
  • Účet dárkové karty:
    • Lze naplnit zadanou částkou jednou za měsíc v posledním dni v měsíci.

Vidíte, že všechny tři z těchto typů účtů mají akci, která se koná na konci každého měsíce. Každý typ účtu ale dělá jiné úkoly. K implementaci tohoto kódu použijete polymorfismus . Vytvořte jednu virtual metodu BankAccount ve třídě:

public virtual void PerformMonthEndTransactions() { }

Předchozí kód ukazuje, jak pomocí klíčového virtual slova deklarovat metodu v základní třídě, pro kterou odvozená třída může poskytnout jinou implementaci. Metoda virtual je metoda, kde se každá odvozená třída může rozhodnout pro reimplement. Odvozené třídy používají override klíčové slovo k definování nové implementace. Obvykle se na to odkazuje jako "přepsání implementace základní třídy". Klíčové virtual slovo určuje, že odvozené třídy mohou přepsat chování. Můžete také deklarovat abstract metody, kde odvozené třídy musí přepsat chování. Základní třída neposkytuje implementaci metody abstract . Dále je potřeba definovat implementaci pro dvě z nových tříd, které jste vytvořili. Začněte s:InterestEarningAccount

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

Do souboru LineOfCreditAccount. Kód neguje zůstatek pro výpočet kladného úroku, který je stažen z účtu:

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

Třída GiftCardAccount potřebuje dvě změny k implementaci jeho měsíčních koncových funkcí. Nejprve upravte konstruktor tak, aby zahrnoval volitelnou částku, kterou chcete přidat každý měsíc:

private readonly decimal _monthlyDeposit = 0m;

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

Konstruktor poskytuje výchozí hodnotu pro monthlyDeposit hodnotu, aby volající mohli vynechat 0 měsíční vklad. Dále přepište metodu PerformMonthEndTransactions pro přidání měsíčního vkladu, pokud byla nastavena na nenulovou hodnotu v konstruktoru:

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

Přepsání použije měsíční vklad nastavený v konstruktoru. Do metody přidejte následující kód Main pro otestování těchto změn pro a GiftCardAccountInterestEarningAccount:

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

Ověřte výsledky. Teď přidejte podobnou sadu testovacího kódu pro 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());

Když přidáte předchozí kód a spustíte program, zobrazí se něco jako následující chyba:

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

Poznámka:

Skutečný výstup zahrnuje úplnou cestu ke složce s projektem. Názvy složek byly vynechány pro stručnost. V závislosti na formátu kódu se čísla řádků můžou mírně lišit.

Tento kód selže, protože BankAccount předpokládá, že počáteční zůstatek musí být větší než 0. Dalším předpokladem upečení do BankAccount třídy je, že zůstatek nemůže jít záporně. Místo toho se zamítne jakýkoliv výběr, který překreslí účet. Oba tyto předpoklady se musí změnit. Řádek úvěrového účtu začíná na 0 a obecně bude mít záporný zůstatek. Také, pokud si zákazník půjčí příliš mnoho peněz, zaúčtovají poplatek. Transakce je přijata, pouze stojí více. První pravidlo lze implementovat přidáním volitelného argumentu do konstruktoru BankAccount , který určuje minimální zůstatek. Výchozí hodnota je 0. Druhé pravidlo vyžaduje mechanismus, který umožňuje odvozené třídy upravit výchozí algoritmus. Základní třída "žádá" odvozený typ, co by se mělo stát, když dojde k přečertáku. Výchozí chování je odmítnout transakci vyvoláním výjimky.

Začněme přidáním druhého konstruktoru, který obsahuje volitelný minimumBalance parametr. Tento nový konstruktor provádí všechny akce provedené existujícím konstruktorem. Nastaví také vlastnost minimálního zůstatku. Můžete zkopírovat tělo existujícího konstruktoru, ale to znamená, že se v budoucnu změní dvě umístění. Místo toho můžete použít řetězení konstruktoru k tomu, aby jeden konstruktor volal jiný. Následující kód ukazuje dva konstruktory a nové další pole:

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

Předchozí kód ukazuje dvě nové techniky. minimumBalance Nejprve je pole označeno jako readonly. To znamená, že hodnotu nelze po vytvoření objektu změnit. BankAccount Po vytvoření minimumBalance se nedá změnit. Za druhé, konstruktor, který přebírá dva parametry používá : this(name, initialBalance, 0) { } jako svou implementaci. Výraz : this() volá druhý konstruktor, druhý se třemi parametry. Tato technika umožňuje mít jednu implementaci pro inicializaci objektu, i když klientský kód může zvolit jeden z mnoha konstruktorů.

Tato implementace volá MakeDeposit pouze v případě, že je počáteční zůstatek větší než 0. Tím se zachová pravidlo, že vklady musí být kladné, ale umožní to, aby se úvěrový účet otevřel s zůstatkem 0 .

Teď, když BankAccount má třída pole jen pro čtení pro minimální zůstatek, poslední změnou je změnit pevný kód 0 na minimumBalance metodu MakeWithdrawal :

if (Balance - amount < _minimumBalance)

Po rozšíření BankAccount třídy můžete upravit LineOfCreditAccount konstruktor tak, aby volal nový základní konstruktor, jak je znázorněno v následujícím kódu:

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

Všimněte si, že LineOfCreditAccount konstruktor změní znaménko parametru creditLimit tak, aby odpovídal významu parametru minimumBalance .

Různá pravidla přečertání

Poslední funkce, která se má přidat, umožňuje LineOfCreditAccount účtovat poplatek za překročení limitu kreditu místo odmítnutí transakce.

Jednou z technik je definování virtuální funkce, ve které implementujete požadované chování. Třída BankAccount refaktoruje metodu MakeWithdrawal do dvou metod. Nová metoda provede zadanou akci, když výběr přijme zůstatek nižší než minimum. MakeWithdrawal Existující metoda má následující kód:

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

Nahraďte ho následujícím kódem:

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

Přidaná metoda je protected, což znamená, že lze volat pouze z odvozených tříd. Tato deklarace brání jiným klientům v volání metody. Je to také virtual proto, aby odvozené třídy mohly změnit chování. Návratový typ je .Transaction? Poznámka ? označuje, že metoda může vrátit null. Při překročení limitu pro výběr přidejte do LineOfCreditAccount poplatku následující implementaci:

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

Přepsání vrátí transakci poplatku při překreslení účtu. Pokud výběr nepřejde přes limit, metoda vrátí null transakci. To znamená, že není žádný poplatek. Otestujte tyto změny přidáním následujícího kódu do vaší Main metody ve Program třídě:

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

Spusťte program a zkontrolujte výsledky.

Shrnutí

Pokud jste se zasekli, uvidíte zdroj tohoto kurzu v našem úložišti GitHub.

V tomto kurzu jsme si ukázali řadu technik používaných v objektově orientovaném programování:

  • Abstrakce jste použili při definování tříd pro každý z různých typů účtů. Tyto třídy popsaly chování pro tento typ účtu.
  • Zapouzdření jste použili při zachování mnoha podrobností private v každé třídě.
  • Dědičnost jste použili při využití implementace již vytvořené ve BankAccount třídě k uložení kódu.
  • Polymorfismus jste použili při vytváření virtual metod, které odvozené třídy mohou přepsat k vytvoření specifického chování pro daný typ účtu.