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 BankAccount
Transaction
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 GiftCardAccount
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());
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.