相关文章推荐
强悍的水煮肉  ·  C++类的继承_c++ ...·  1 月前    · 
强悍的水煮肉  ·  C++ 继承| 菜鸟教程·  1 月前    · 
强悍的水煮肉  ·  Msvm_VirtualHardDiskSe ...·  1 月前    · 
多態性 能夠跨多個抽象概念以不同方式實作繼承的屬性或方法。

在前面的教學課程中, 類別簡介 ,您看到了 抽象 封裝 。 類別 BankAccount 提供銀行帳戶概念的抽象概念。 您可以修改該類別的實作,而不會影響任何使用該類別的程式碼。 BankAccount Transaction 類別都提供程式碼中描述這些概念所需的元件封裝。

在本教學課程中,您將擴充該應用程式,以利用 繼承 多型 來新增功能。 您也會將功能新增至 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");
                  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
        • 當您使用多型來建立可以由衍生類別覆寫的方法時,就能為該帳戶類型創建特定行為。
  •