面向对象的编程 (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% 的额度。
  • 信用帐户:
    • 可以有负余额,但绝对值不能大于信用额度。
    • 利息费用将在每月月末余额不为零的情况下产生。
    • 每次超过信用额度的取款都会产生费用。
  • 礼品卡帐户:
    • 可以在每月的最后一天用指定的金额重新填充一次。

可以看到,这三种账户类型每个月底都有一个动作。 但是,每个帐户类型执行不同的任务。 使用 多态性 来实现此代码。 在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被使用了。
  • 创建 方法,派生类可以重写它们来创建该帐户类型的特定行为时,你使用了多形性virtual