Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
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.
- Будет взиматься плата за каждый вывод средств, который превышает кредитный лимит.
- Учетная запись подарочной карты:
- Может быть заполнено указанной суммой один раз в месяц, в последний день месяца.
Вы можете увидеть, что все три этих типа учетных записей имеют действие, которое выполняется в конце каждого месяца. Однако каждый тип учетной записи выполняет разные задачи. Для реализации этого кода используется полиморфизм . Создайте один 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
класс, заключается в том, что баланс не может быть отрицательным. Вместо этого любой вывод средств, который превышает баланс счета, отклоняется. Оба этих предположения должны измениться. Кредитный счет начинается с нулевого баланса и обычно имеет отрицательный баланс. Кроме того, если клиент занимает слишком много денег, он облагается сбором. Транзакция принимается, она просто стоит больше. Первое правило можно реализовать, добавив необязательный аргумент в 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
, что означает, что его можно вызывать только из производных классов. Это декларация запрещает другим клиентам вызывать метод. Кроме того, это сделано для того, чтобы производные классы могли изменить поведение. Тип возвращаемого 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
методов, которые производные классы могли переопределить для создания определенного поведения для этого типа учетной записи.