物件導向程式設計 (C#)

C# 是物件導向程式設計語言。 物件導向程式設計的四個基本原則如下:

  • 抽象概念:將實體的相關屬性和互動模組化為類別,以定義系統的抽象表示。
  • 封裝:隱藏物件的內部狀態和功能,只允許透過一組公用函式進行存取。
  • 繼承:能夠根據現有的抽象概念建立新的抽象概念。
  • 多型:能夠跨多個抽象概念以不同方式實作繼承的屬性或方法。

在上一個教學課程 (類別簡介) 中,您已了解「抽象概念」和「封裝」BankAccount 類別提供銀行帳戶概念的抽象概念。 您可以修改其實作,而不會影響任何使用 BankAccount 類別的程式碼。 BankAccountTransaction 類別都提供在程式碼中描述這些概念所需的元件封裝。

在本教學課程中,您將擴充該應用程式,以使用「繼承」和「多型」來新增功能。 您也會將功能新增至 BankAccount 類別,以利用您在上一個教學課程中學到的「抽象概念」和「封裝」技術。

建立不同的帳戶類型

建置此程式之後,您會收到將功能新增至其中的要求。 這在只有一個銀行帳戶類型的情況下會很容易進行。 但經過一段時間,需求會變更,並要求相關的帳戶類型:

  • 存款帳戶,會在每個月的月底累算利息。
  • 貸款帳戶,可以有負餘額,但有餘額時,每個月會有利息費用。
  • 預付禮品卡帳戶,一開始會有一筆存款,而且只能全額支付。 可在每個月的月初加值一次。

所有這些不同的帳戶都類似於先前教學課程中所定義的 BankAccount 類別。 您可以複製該程式碼、重新命名類別,並進行修改。 該技術在短期內會有效,但一段時間後需要執行更多工作。 任何變更都要複製到所有受影響的類別。

相反地,您可以建立新的銀行帳戶類型,從上一個教學課程中建立的 BankAccount 類別繼承方法和資料。 這些新類別可以使用每個類型所需的特定行為來擴充 BankAccount 類別:

public class InterestEarningAccount : BankAccount
{
}

public class LineOfCreditAccount : BankAccount
{
}

public class GiftCardAccount : BankAccount
{
}

上述每個類別都會從其共用「基底類別」(BankAccount 類別)「繼承」共用行為。 您可以在每個「衍生類別」中撰寫其他新功能的實作。 這些衍生類別已有 BankAccount 類別中定義的所有行為。

建議您在不同的來源檔案中建立每個新類別。 在 Visual Studio 中,您可以在專案上按一下滑鼠右鍵,然後選取 [新增類別] 在新檔案中新增類別。 在 Visual Studio Code 中選取 [檔案],然後選取 [新增] 以建立新的來源檔案。 在任一工具中,將檔案命名為符合類別:InterestEarningAccount.csLineOfCreditAccount.csGiftCardAccount.cs

當您建立如上述範例所示的類別時,您會發現不會編譯任何衍生類別。 建構函式會負責初始化物件。 衍生類別建構函式必須初始化衍生類別,並提供如何初始化衍生類別中所包含基底類別物件的指示。 通常會在沒有任何額外程式碼的情況下進行適當的初始化。 BankAccount 類別會宣告一個具有下列特徵標記的公用建構函式:

public BankAccount(string name, decimal initialBalance)

當您自行定義建構函式時,編譯器不會產生預設建構函式。 這表示每個衍生類別都必須明確呼叫此建構函式。 您會宣告建構函式來將引數傳遞至基底類別建構函式。 下列程式碼顯示 InterestEarningAccount 的建構函式:

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

這個新建構函式的參數符合基底類別建構函式的參數類型和名稱。 您可以使用 : base() 語法來表示對基底類別建構函式的呼叫。 某些類別會定義多個建構函式,此語法可讓您選擇呼叫哪個基底類別建構函式。 更新建構函式之後,您可以為每個衍生類別開發程式碼。 新類別的需求如下所述:

  • 存款帳戶:
    • 月末餘額存款會增加 2%。
  • 貸款帳戶:
    • 可以有負餘額,但絕對值不能大於信用額度。
    • 每個月會產生利息費用 (如果月末餘額不是 0)。
    • 每筆超過信用額度的提款都會產生費用。
  • 禮品卡帳戶:
    • 每個月的最後一天可加值指定金額一次。

如您所見,上述所有三種帳戶類型在每個月的月底都會執行動作。 不過,每種帳戶類型都會執行不同的工作。 您可以使用「多型」來實作此程式碼。 在 BankAccount 類別中建立單一 virtual 方法:

public virtual void PerformMonthEndTransactions() { }

上述程式碼示範如何使用 virtual 關鍵字,在基底類別中宣告衍生類別可為其提供不同實作的方法。 virtual 方法是任何衍生類別都可選擇重新實作的方法。 衍生類別會使用 override 關鍵字來定義新的實作。 您通常會將此實作稱為「覆寫基底類別實作」。 virtual 關鍵字會指定衍生類別可覆寫行為。 您也可以宣告衍生類別必須覆寫行為的 abstract 方法。 基底類別不會提供 abstract 方法的實作。 接下來,您必須定義為已建立的其中兩個新類別定義實作。 從 InterestEarningAccount 開始:

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

將下列程式碼新增至 LineOfCreditAccount。 此程式碼會變換餘額正負號,以計算從帳戶提取的正利息費用:

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

GiftCardAccount 類別需要兩項變更,才能實作其月底功能。 首先,修改建構函式以包含每個月要新增的選擇性金額:

private readonly decimal _monthlyDeposit = 0m;

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

此建構函式會提供 monthlyDeposit 值的預設值,因此呼叫端可以省略 0 (表示沒有每月存款)。 接下來,覆寫 PerformMonthEndTransactions 方法以新增每月存款 (如果已在建構函式中設定為非零值):

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

此覆寫會套用在建構函式中設定的每月存款。 將下列程式碼新增至 Main 方法,以測試對 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());

確認結果。 現在,為 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());

當您新增上述程式碼並執行程式時,您會看到類似下列錯誤:

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

注意

實際輸出包含專案資料夾的完整路徑。 為了簡潔起見,已省略資料夾名稱。 此外,視您的程式碼格式而定,行號可能稍有不同。

此程式碼會失敗,因為 BankAccount 假設初始餘額必須大於 0。 另一個納入 BankAccount 類別的假設是餘額不可為負數。 相反地,任何造成帳戶透支的提款會遭到拒絕。 這兩個假設都需要變更。 貸款帳戶會從 0 開始,而且通常會有負餘額。 此外,如果客戶借太多錢,則會產生費用。 已接受交易,只是會花費更多。 第一個規則可以透過將選擇性引數新增至 BankAccount 建構函式以指定最低餘額來實作。 預設值為 0。 第二個規則需要一個可讓衍生類別修改預設演算法的機制。 在某種意義上,基底類別會「詢問」衍生類型在超支時應該發生什麼情況。 預設行為是擲回例外狀況來拒絕交易。

讓我們先新增包含選擇性 minimumBalance 參數的第二個建構函式。 這個新的建構函式會執行現有建構函式執行的所有動作。 此外,還會設定最低餘額屬性。 您可以複製現有建構函式的主體,但這表示未來需要變更兩個位置。 相反地,您可以使用「建構函式鏈結」,讓一個建構函式呼叫另一個建構函式。 下列程式碼顯示兩個建構函式和新的額外欄位:

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

上述程式碼顯示兩項新技術。 首先,minimumBalance 欄位會標記為 readonly。 這表示此值在建構物件之後將無法變更。 建立 BankAccount 之後,minimumBalance 將無法變更。 其次,接受兩個參數的建構函式會使用 : this(name, initialBalance, 0) { } 作為其實作。 : this() 運算式會呼叫另一個建構函式,也就是具有三個參數的建構函式。 這項技術可讓您透過單一實作來初始化物件,不過用戶端程式碼可以選擇多項建構函式的其中一項。

只有在初始餘額大於 0 時,此實作才會呼叫 MakeDeposit。 這會保留存款必須是正數的規則,但可讓貸款帳戶以 0 餘額開戶。

現在 BankAccount 類別具有最低餘額的唯讀欄位,最後一項變更是將硬式編碼 0 變更為 MakeWithdrawal 方法中的 minimumBalance

if (Balance - amount < _minimumBalance)

擴充 BankAccount 類別之後,您可以修改 LineOfCreditAccount 建構函式來呼叫新的基底建構函式,如下列程式碼所示:

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

請注意,LineOfCreditAccount 建構函式會變更 creditLimit 參數的正負號,使其符合 minimumBalance 參數的意義。

不同的超支規則

最後一項新增功能可讓 LineOfCreditAccount 在超過信用額度時收取費用,而不是拒絕交易。

一項技術是定義虛擬函式,您可以在其中實作必要的行為。 BankAccount 類別會將 MakeWithdrawal 方法重構為兩個方法。 新方法會在提款導致餘額低於下限時執行指定的動作。 現有的 MakeWithdrawal 方法具有下列程式碼:

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

以下列程式碼取代:

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

新增的方法是 protected,這表示只能從衍生類別呼叫方法。 該宣告可防止其他用戶端呼叫方法。 這也是 virtual,因此衍生類別可以變更行為。 傳回型別是 Transaction?? 註解表示方法可能會傳回 null。 在 LineOfCreditAccount 中新增下列實作,以在超過提款限制時收取費用:

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

此覆寫會在帳戶超支時傳回費用交易。 如果提款未超過限制,此方法會傳回 null 交易。 這表示沒有費用。 將下列程式碼新增至 Program 類別中的 Main 方法,以測試這些變更:

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

執行程式,並檢查結果。

摘要

如果遇到問題,您可以在我們的 GitHub 存放庫 (英文) 中查看此教學課程的原始程式碼。

本教學課程示範物件導向程式設計中使用的許多技術:

  • 當您為每個不同的帳戶類型定義類別時,會使用「抽象概念」。 這些類別會描述該帳戶類型的行為。
  • 當您在每個類別中將許多詳細資料保留為 private 時,會使用「封裝」
  • 當您利用 BankAccount 類別中已建立的實作來儲存程式碼時,會使用「繼承」
  • 當您建立衍生類別可覆寫以建立該帳戶類型特定行為的 virtual 方法時,會使用「多型」