物件導向程式設計 (C#)
C# 是物件導向程式設計語言。 物件導向程式設計的四個基本原則如下:
- 抽象概念:將實體的相關屬性和互動模組化為類別,以定義系統的抽象表示。
- 封裝:隱藏物件的內部狀態和功能,只允許透過一組公用函式進行存取。
- 繼承:能夠根據現有的抽象概念建立新的抽象概念。
- 多型:能夠跨多個抽象概念以不同方式實作繼承的屬性或方法。
在上一個教學課程 (類別簡介) 中,您已了解「抽象概念」和「封裝」。 BankAccount
類別提供銀行帳戶概念的抽象概念。 您可以修改其實作,而不會影響任何使用 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.cs、LineOfCreditAccount.cs 和 GiftCardAccount.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
方法,以測試對 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());
確認結果。 現在,為 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
方法時,會使用「多型」。