共用方式為


Object-Oriented 程式設計 (C#)

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

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

在前面的教學課程中,類別簡介,您看到了抽象封裝。 類別 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% 信用額度。
  • 信用額度:
    • 可以有負餘額,但絕對值不能大於信用額度。
    • 若月底餘額不為零,每個月將會產生利息費用。
    • 將針對超過信用額度的每個取款產生費用。
  • 禮品卡帳戶:
    • 可以在每個月的最後一天,以指定的金額重新填入一次。

您可以看到這三種帳戶類型每個月底都會進行一項操作。 不過,每個帳戶類型都會執行不同的工作。 您可以使用 多型 來實作此程序代碼。 在 virtual 類別中建立單一 BankAccount 方法:

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() 呼叫另一個建構函式,也就是具有三個參數的建構函式。 這項技術可讓您有單一實作來初始化物件,即使用戶端程式代碼可以選擇許多建構函式的其中一個。

只有在初始餘額大於 MakeDeposit時,這個實作才會呼叫 0 。 這保留了存款必須是正數的規則,但允許信用帳戶以 0 餘額開立。

既然類別 BankAccount 具有最小餘額的唯讀欄位,最後一個變更是將方法 0 中的硬編碼 minimumBalance 變更為 MakeWithdrawal

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 交易。 這表示沒有費用。 將下列程式代碼新增至 類別 Main 中的 Program 方法,以測試這些變更:

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 存放庫 \(英文\) 中查看此教學課程的原始程式碼。

本教學課程示範 Object-Oriented 程式設計中使用的許多技術:

  • 當您為每個不同的帳戶類型定義類別時,會使用 抽象概念 。 這些類別描述該帳戶類型的行為。
  • 當您在每個類別中保留許多詳細數據時,會使用private
  • 當您利用 類別中已建立的實作來儲存程式代碼時,使用了BankAccount
  • 當您使用多型來建立可以由衍生類別覆寫的方法時,就能為該帳戶類型創建特定行為。