Programowanie obiektowe (C#)

C# to język programowania zorientowany na obiekt. Cztery podstawowe zasady programowania obiektowego to:

  • Modelowanie abstrakcji odpowiednich atrybutów i interakcji jednostek jako klas w celu zdefiniowania abstrakcyjnej reprezentacji systemu.
  • Hermetyzacja Ukrywanie stanu wewnętrznego i funkcjonalności obiektu i zezwalanie na dostęp tylko za pośrednictwem publicznego zestawu funkcji.
  • Zdolność dziedziczenia do tworzenia nowych abstrakcji na podstawie istniejących abstrakcji.
  • Zdolność polimorfizmu do implementowania dziedziczone właściwości lub metod na różne sposoby w wielu abstrakcjach.

W poprzednim samouczku przedstawiono wprowadzenie do klas, które widzieliśmy zarówno abstrakcji, jak i hermetyzacji. Klasa BankAccount dostarczyła abstrakcję dla koncepcji konta bankowego. Możesz zmodyfikować jego implementację bez wpływu na kod, który używał BankAccount klasy. Zarówno klasy, jak BankAccount i Transaction zapewniają hermetyzację składników potrzebnych do opisania tych pojęć w kodzie.

W tym samouczku rozszerzysz tę aplikację, aby korzystać z dziedziczenia i polimorfizmu w celu dodania nowych funkcji. Dodasz również funkcje do BankAccount klasy, korzystając z technik abstrakcji i hermetyzacji poznanych w poprzednim samouczku.

Tworzenie różnych typów kont

Po utworzeniu tego programu otrzymasz żądania dodania do niego funkcji. Działa świetnie w sytuacji, gdy istnieje tylko jeden typ konta bankowego. W miarę upływu czasu wymagane są zmiany i powiązane typy kont:

  • Konto zarabiające odsetki, które nalicza odsetki na koniec każdego miesiąca.
  • Linia kredytowa, która może mieć ujemne saldo, ale gdy istnieje saldo, co miesiąc jest naliczana opłata odsetkowa.
  • Przedpłacone konto karty upominkowej, które rozpoczyna się od pojedynczego depozytu i można je wypłacić tylko. Można go uzupełnić raz na początku każdego miesiąca.

Wszystkie te różne konta są podobne do BankAccount klas zdefiniowanych we wcześniejszym samouczku. Możesz skopiować ten kod, zmienić nazwę klas i wprowadzić modyfikacje. Ta technika będzie działać w krótkim okresie, ale w czasie byłaby to większa praca. Wszelkie zmiany zostaną skopiowane we wszystkich klasach, których dotyczy problem.

Zamiast tego można utworzyć nowe typy kont bankowych, które dziedziczą metody i dane z BankAccount klasy utworzonej w poprzednim samouczku. Te nowe klasy mogą rozszerzać klasę BankAccount o określone zachowanie wymagane dla każdego typu:

public class InterestEarningAccount : BankAccount
{
}

public class LineOfCreditAccount : BankAccount
{
}

public class GiftCardAccount : BankAccount
{
}

Każda z tych klas dziedziczy współdzielone zachowanie ze współużytkowanej klasy bazowej BankAccount , klasy . Napisz implementacje dla nowych i różnych funkcji w każdej z klas pochodnych. Te klasy pochodne mają już wszystkie zachowania zdefiniowane w BankAccount klasie.

Dobrym rozwiązaniem jest utworzenie każdej nowej klasy w innym pliku źródłowym. W programie Visual Studio możesz kliknąć prawym przyciskiem myszy projekt i wybrać pozycję Dodaj klasę , aby dodać nową klasę w nowym pliku. W programie Visual Studio Code wybierz pozycję Plik , a następnie pozycję Nowy , aby utworzyć nowy plik źródłowy. W obu narzędziach nadaj plikowi nazwę zgodną z klasą: InterestEarningAccount.cs, LineOfCreditAccount.cs i GiftCardAccount.cs.

Podczas tworzenia klas, jak pokazano w poprzednim przykładzie, okaże się, że żadna z klas pochodnych nie jest kompilowana. Konstruktor jest odpowiedzialny za inicjowanie obiektu. Konstruktor klasy pochodnej musi zainicjować klasę pochodną i udostępnić instrukcje dotyczące inicjowania obiektu klasy bazowej zawartego w klasie pochodnej. Właściwa inicjalizacja zwykle odbywa się bez dodatkowego kodu. Klasa BankAccount deklaruje jeden publiczny konstruktor z następującym podpisem:

public BankAccount(string name, decimal initialBalance)

Kompilator nie generuje domyślnego konstruktora podczas samodzielnego definiowania konstruktora. Oznacza to, że każda klasa pochodna musi jawnie wywołać ten konstruktor. Zadeklarujesz konstruktor, który może przekazać argumenty do konstruktora klasy bazowej. Poniższy kod przedstawia konstruktor dla elementu InterestEarningAccount:

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

Parametry tego nowego konstruktora są zgodne z typem parametru i nazwami konstruktora klasy bazowej. Składnia : base() służy do wskazywania wywołania konstruktora klasy bazowej. Niektóre klasy definiują wiele konstruktorów, a ta składnia umożliwia wybranie wywoływanego konstruktora klasy bazowej. Po zaktualizowaniu konstruktorów można opracować kod dla każdej z klas pochodnych. Wymagania dotyczące nowych klas można określić w następujący sposób:

  • Konto zarabiające odsetki:
    • Otrzyma kredyt w wysokości 2% salda kończącego miesiąc.
  • Linia kredytowa:
    • Może mieć ujemne saldo, ale nie jest większe w wartości bezwzględnej niż limit kredytowy.
    • W każdym miesiącu zostanie naliczona opłata odsetkowa, w której saldo na koniec miesiąca nie jest 0.
    • Poniesie opłatę za każde wycofanie, które przekroczy limit kredytowy.
  • Konto karty upominkowej:
    • Można uzupełnić określoną kwotą raz w miesiącu, w ostatnim dniu miesiąca.

Widać, że wszystkie trzy z tych typów kont mają akcję wykonywaną na koniec każdego miesiąca. Jednak każdy typ konta wykonuje różne zadania. Aby zaimplementować ten kod, należy użyć polimorfizmu . Utwórz pojedynczą virtual metodę w BankAccount klasie:

public virtual void PerformMonthEndTransactions() { }

Powyższy kod pokazuje, jak używać słowa kluczowego virtual do deklarowania metody w klasie bazowej, dla której klasa pochodna może zapewnić inną implementację. virtual Metoda to metoda, w której każda klasa pochodna może zdecydować się na ponowne wdrożenie. Klasy pochodne używają słowa kluczowego override do zdefiniowania nowej implementacji. Zazwyczaj nazywa się to "zastępowaniem implementacji klasy bazowej". Słowo virtual kluczowe określa, że klasy pochodne mogą zastąpić zachowanie. Można również zadeklarować abstract metody, w których klasy pochodne muszą zastąpić zachowanie. Klasa bazowa nie zapewnia implementacji metody abstract . Następnie należy zdefiniować implementację dla dwóch nowo utworzonych klas. Zacznij od elementu InterestEarningAccount:

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

Dodaj następujący kod do pliku LineOfCreditAccount. Kod neguje saldo w celu obliczenia dodatniej opłaty odsetkowej, która zostanie wycofana z konta:

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

Klasa GiftCardAccount wymaga dwóch zmian w celu zaimplementowania funkcji zakończenia miesiąca. Najpierw zmodyfikuj konstruktor, aby uwzględnić opcjonalną ilość, która ma zostać dodana każdego miesiąca:

private readonly decimal _monthlyDeposit = 0m;

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

Konstruktor udostępnia wartość domyślną dla monthlyDeposit wartości, dzięki czemu osoby wywołujące mogą pominąć wartość bez miesięcznego 0 depozytu. Następnie przesłoń metodę PerformMonthEndTransactions w celu dodania miesięcznego depozytu, jeśli została ustawiona na wartość inną niż zero w konstruktorze:

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

Zastąpienie stosuje miesięczny zestaw depozytów w konstruktorze. Dodaj następujący kod do Main metody , aby przetestować te zmiany dla elementu GiftCardAccount i :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());

Sprawdź wyniki. Teraz dodaj podobny zestaw kodu testowego dla elementu 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());

Po dodaniu poprzedniego kodu i uruchomieniu programu zobaczysz podobny do następującego błędu:

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

Uwaga

Rzeczywiste dane wyjściowe zawierają pełną ścieżkę do folderu z projektem. Nazwy folderów zostały pominięte w celu zwięzłości. Ponadto w zależności od formatu kodu numery wierszy mogą być nieco inne.

Ten kod kończy się niepowodzeniem, ponieważ BankAccount przyjęto założenie, że początkowe saldo musi być większe niż 0. Innym założeniem upieczonym BankAccount w klasie jest to, że równowaga nie może pójść negatywnie. Zamiast tego wszelkie wypłaty, które przerysuje konto, zostanie odrzucone. Oba te założenia muszą ulec zmianie. Linia konta kredytowego rozpoczyna się od 0 i na ogół będzie miała ujemne saldo. Ponadto, jeśli klient pożyczy zbyt dużo pieniędzy, poniesie opłatę. Transakcja jest akceptowana, po prostu kosztuje więcej. Pierwszą regułę można zaimplementować przez dodanie opcjonalnego argumentu BankAccount do konstruktora, który określa minimalną równowagę. Wartość domyślna to 0. Druga reguła wymaga mechanizmu, który umożliwia klasom pochodnym modyfikowanie algorytmu domyślnego. W sensie klasa bazowa "pyta" typ pochodny, co powinno się zdarzyć, gdy istnieje nadwyżka. Domyślne zachowanie polega na odrzuceniu transakcji przez zgłoszenie wyjątku.

Zacznijmy od dodania drugiego konstruktora zawierającego opcjonalny minimumBalance parametr. Ten nowy konstruktor wykonuje wszystkie akcje wykonywane przez istniejący konstruktor. Ponadto ustawia właściwość minimalnego salda. Możesz skopiować treść istniejącego konstruktora, ale oznacza to, że dwie lokalizacje zmienią się w przyszłości. Zamiast tego można użyć łańcucha konstruktorów, aby jeden konstruktor wywołał inny. Poniższy kod przedstawia dwa konstruktory i nowe pole dodatkowe:

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

Powyższy kod przedstawia dwie nowe techniki. minimumBalance Najpierw pole jest oznaczone jako readonly. Oznacza to, że nie można zmienić wartości po skonstruowaniu obiektu. Po utworzeniu minimumBalance elementu BankAccount nie można go zmienić. Po drugie, konstruktor, który przyjmuje dwa parametry, używa : this(name, initialBalance, 0) { } jako implementacji. Wyrażenie : this() wywołuje drugi konstruktor, ten z trzema parametrami. Ta technika umożliwia utworzenie pojedynczej implementacji inicjowania obiektu, mimo że kod klienta może wybrać jeden z wielu konstruktorów.

Ta implementacja wywołuje tylko MakeDeposit wtedy, gdy początkowe saldo jest większe niż 0. To zachowuje regułę, że depozyty muszą być dodatnie, ale pozwala na otwarcie konta kredytowego 0 przy użyciu salda.

Teraz, gdy BankAccount klasa ma pole tylko do odczytu dla minimalnej równowagi, ostateczna zmiana polega na zmianie twardego kodu 0 na minimumBalance w metodzie MakeWithdrawal :

if (Balance - amount < _minimumBalance)

Po rozszerzeniu BankAccount klasy można zmodyfikować konstruktor w LineOfCreditAccount celu wywołania nowego konstruktora podstawowego, jak pokazano w poniższym kodzie:

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

Zwróć uwagę, że LineOfCreditAccount konstruktor zmienia znak parametru creditLimit , tak aby był zgodny z znaczeniem parametru minimumBalance .

Różne reguły w rachunku bieżącym

Ostatnia funkcja dodawania umożliwia LineOfCreditAccount naliczanie opłaty za przekroczenie limitu środków zamiast odmowy transakcji.

Jedną z technik jest zdefiniowanie funkcji wirtualnej, w której zaimplementowane jest wymagane zachowanie. Klasa BankAccount refaktoryzuje metodę MakeWithdrawal do dwóch metod. Nowa metoda wykonuje określoną akcję, gdy wypłata przyjmuje saldo poniżej minimum. Istniejąca MakeWithdrawal metoda ma następujący 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);
}

Zastąp go następującym kodem:

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

Dodawana metoda to protected, co oznacza, że może być wywoływana tylko z klas pochodnych. Ta deklaracja uniemożliwia innym klientom wywoływanie metody . Umożliwia to również virtual zmianę zachowania klas pochodnych. Zwracany Transaction?typ to . Adnotacja ? wskazuje, że metoda może zwrócić nullwartość . Dodaj następującą implementację w elemencie , LineOfCreditAccount aby pobrać opłatę po przekroczeniu limitu wypłaty:

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

Zastąpienie zwraca transakcję opłaty, gdy konto jest przerysowane. Jeśli wypłata nie przekracza limitu, metoda zwraca transakcję null . Oznacza to, że nie ma opłaty. Przetestuj te zmiany, dodając następujący kod do Main metody w Program klasie :

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

Uruchom program i sprawdź wyniki.

Podsumowanie

Jeśli utkniesz, możesz zobaczyć źródło tego samouczka w naszym repozytorium GitHub.

W tym samouczku przedstawiono wiele technik używanych w programowaniu obiektowym:

  • Użyto abstrakcji podczas definiowania klas dla każdego z różnych typów kont. Te klasy opisały zachowanie tego typu konta.
  • Użyto hermetyzacji , gdy przechowywano wiele szczegółów private w każdej klasie.
  • Podczas korzystania z implementacji utworzonej BankAccount już w klasie użyto dziedziczenia w celu zapisania kodu.
  • Podczas tworzenia virtual metod pochodnych klasy pochodne można zastąpić w celu utworzenia konkretnego zachowania dla tego typu konta.