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% 的额度。
- 信用帐户:
- 可以有负余额,但绝对值不能大于信用额度。
- 利息费用将在每月月末余额不为零的情况下产生。
- 每次超过信用额度的取款都会产生费用。
- 礼品卡帐户:
- 可以在每月的最后一天用指定的金额重新填充一次。
可以看到,这三种账户类型每个月底都有一个动作。 但是,每个帐户类型执行不同的任务。 使用 多态性 来实现此代码。 在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
方法,以测试 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()
调用另一个构造函数,即具有三个参数的构造函数。 此方法允许你使用单个实现来初始化对象,即使客户端代码可以选择多个构造函数之一。
仅当初始平衡大于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
被使用了。 - 创建 方法,派生类可以重写它们来创建该帐户类型的特定行为时,你使用了多形性
virtual
。