|
ООП С Sharp - Определение абстрактного класса
Мы займемся изменением дизайна Mortimer Phones не просто ради удовольствия. Дело в том, что у нашей существующей иерархии классов имеется недостаток. Класс Customer представляет абонентов, вносящих платежи по факту (pass-as-you-go) в качестве базового класса для всех прочих типов абонентов. То есть, мы трактуем этот тарифный план в качестве специального, от которого наследуются все прочие.
Это не точное представление реальной ситуации. На самом деле тарифный план “плата по факту” — всего лишь один из диапазона возможных, в нем нет ничего особенного. И правильно спроектированная иерархия классов должна отражать это. Поэтому в этом разделе мы немного переделаем пример Mortimer Phones, чтобы получить иерархию классов, показанную на рисунке.
Старый класс Customer исчез. Появился новый абстрактный базовый класс GenericCustomer. GenericCustomer реализует все, что присуще всем типам абонентов, а именно — методы и свойства, которые имеют общую реализацию для всех абонентов, а потому не являются виртуальными. Это включает получение баланса или имени абонента и запись оплаты.
Однако GenericCustomer не предоставляет никакой реализации метода RecordCall(), который вычисляет стоимость данного разговора и добавляет его к счету абонента. Реализация этого метода отличается для каждого тарифного плана, поэтому нам понадобится в каждом классе-наследнике реализовать свою версию этого метода. Поэтому RecordCall() в классе GenericCustomer будет объявлен как abstract.
После этого нам понадобится класс, представляющий абонентов, которые платят “по факту”. Это будет класс PayAsYouGoCustomer, в котором реализуется переопределенный метод RecordCall(), который в предыдущей иерархии находился в классе Customer.
Возможно, вы недоумеваете — стоило ли прилагать усилия для такого перепроектирования иерархии примера? В конце концов, и предыдущий вариант иерархии работал отлично, разве нет? Причина того, что новая иерархия предлагает лучший архитектурный дизайн, проста — она устраняет тонкий источник ошибок.
В реальном приложении, возможно, RecordCall() будет не единственным виртуальным методом, который необходимо будет реализовать отдельно для каждого тарифного плана. Что случится, если позднее кто-то добавит новый класс-наследник, представляющий новый тарифный план, но при этом забудет добавить переопределения некоторых из этих методов? При старом варианте иерархии компилятор просто автоматически подставит соответствующий метод базового класса. Там базовый класс представлял абонентов, оплачивающих разговоры по факту, поэтому в результате была вероятность появления тонких ошибок, связанных с вызовом неправильных версий методов. В нашей новой иерархии это исключено. Вместо этого мы получим ошибку времени компиляции, если компилятор обнаружит, что существенные абстрактные методы не были переопределены в новом классе. В любом случае обратимся к новому коду, который, как и следовало ожидать, представлен в примере MortimerPhones4.
При новой иерархии код GenericCustomer выглядит так, как показано ниже. Большая часть кода такая же, как и в старом классе Customer, отличающиеся строки выделены полужирным. Обратите внимание на объявление метода RecordCall() как abstract:
public abstract class GenericCustomer
{
...
public void RecordPayment(decimal amountPaid)
{
balance -= amountPaid;
}
public abstract void RecordCall(TypeOfCall callType, uint nMinutes);
}
Теперь обратимся к реализации абонентов, оплачивающих по факту. Опять-таки, отметим, что большая часть кода взята непосредственно из старого класса Customer. Единственное реальное отличие состоит в том, что RecordCall() теперь снабжен модификатором override вместо virtual:
public class PayAsYouGoCustomer : GenericCustomer
{
public override void RecordCall(TypeOfCall callType, uint nMinutes)
{
// та же реализация, что и у Customer
}
}
Вы не видите здесь кода Nevermore60Customer, потому что переопределение RecordCall() в этом классе достаточно длинное и полностью идентично тому, что было в предыдущей версии примера. Единственное отличие, которое потребуется внести в этот класс — унаследовать его от GenericCustomer вместо Customer, которого больше нет:
public class Nevermore60Customer : GenericCustomer
{
private uint highCostMinutesUsed;
public override void RecordCall(TypeOfCall callType, uint nMinutes)
{
// та же реализация, что и в старом Nevermore60Customer
}
...
И в завершение добавим некоторый новый клиентский код, чтобы продемонстрировать операции новой иерархии классов. На этот раз мы используем массив, чтобы сохранить записи о разнообразных абонентах — поэтому данный код покажет, как можно использовать массив ссылок на абстрактный базовый класс, чтобы обращаться к экземплярам различных классов-наследников с соответствующими переопределениями вызываемых методов:
public static void Main()
{
GenericCustomer arabel = new Nevermore60Customer();
arabel.Name = "Arabel Jones";
GenericCustomer mrJones = new PayAsYouGoCustomer();
mrJones.Name = "Ben Jones";
GenericCustomer [] customers = new GenericCustomer[2];
customers[0] = arabel;
customers[0].RecordCall(TypeOfCall.CallToLandline, 20);
customers[0].RecordCall(TypeOfCall.CallToCellPhone, 5);
customers[1] = mrJones;
customers[1].RecordCall(TypeOfCall.CallToLandline, 10);
foreach (GenericCustomer nextCustomer in customers)
{
Console.WriteLine("{0,-20} должен ${1:F2}", nextCustomer.Name,
nextCustomer.Balance);
}
}
Запуск этого кода выдаст корректные результаты сумм задолженностей абонентов:
MortimerPhones4 Arabel Jones должен $2.90 Ben Jones должен $0.20
|