|
ООП С Sharp - Добавление наследования
Пока наш пример с Mortimer Phones предельно упрощен. В частности, он предусматривает только один тарифный план для всех заказчиков, что крайне далеко от реальности. Многие люди регистрируются под тарифным планом, согласно которому они платят фиксированную сумму каждый месяц, однако существует и много других планов.
На данный момент, чтобы попытаться принять во внимание все разнообразные тарифные планы, в наш метод RecordCall() придется включить несколько вложенных операторов switch, что будет выглядеть примерно так (предполагая, что поле CallPlan является перечислением).
public void RecordCall(TypeOfCall callType, uint nMinutes)
{
switch (callplan) {
case CallPlan.CallPlan1:
switch (callType)
{
case TypeOfCall.CallToLandline:
// вычислить сумму
case TypeOfCall.CallToCellPhone:
// вычислить сумму
// для других случаев
// и так далее
}
break;
case CallPlan.CallPlan2:
switch (callType)
{
// и так далее
}
break;
}
}
Это неудовлетворительное решение. Небольшие операторы switch хороши, но огромный switch с большим количеством опций — и, в частности, с вложенными операторами switch — образует код, который трудно понять. Это также означает, что всякий раз, когда будет введен в действие новый тарифный план, придется изменять код этого метода. Это чревато новыми ошибками в тех частях кода, которые отвечают за обработку существующих тарифных планов.
Проблема, которую нужно решить, заключается в том, что код для обработки разных тарифных планов смешан в пределах одного оператора switch. Если бы удалось разделить код для разных планов, она была бы решена. И это — одна из целей, для которых предназначено наследование. Нам нужно отделить код для разных типов абонентов. Начнем с определения нового класса, представляющего пользователей нового тарифного плана. Назовем этот план Nevermore60. Он будет предназначен для абонентов, интенсивно пользующихся мобильными телефонами. Такие абоненты платят по наивысшей расценке в 50 центов за минуту за первые 60 минут звонков на другие сотовые телефоны, затем расценка для них снижается до 20 центов за минуту за все последующие разговоры, так что если они делают достаточно много звонков, то экономят деньги по сравнению с предыдущим планом.
Пока мы немного отложим действительную реализацию вычисления новых платежей и определим Nevermore60Customer следующим образом:
public class Nevermore60Customer : Customer
{
}
Другими словами, класс пока не имеет ни методов, ни свойств, ни чего-либо еще. С другой стороны, он определен несколько иначе, чем до сих пор определялись все классы. После имени класса стоит двоеточие, за которым идет имя ранее созданного класса — Customer. Это сообщает компилятору, что Nevermore60Customer — это класс, унаследованный от Customer. Это значит, что каждый член Customer также существует в Nevermore60Customer. Иначе говоря, если использовать правильную терминологию, каждый член Customer унаследован в Nevermore60Customer. К тому же о Nevermore60Customer говорят, что это — класс-наследник, в то время как Customer — базовый класс. Иногда классы-наследники называют подклассами, а базовые классы — суперклассами или родительскими классами.
Поскольку мы пока не поместили ничего нового в класс Nevermore60Customer, он в действительности представляет точную копию определения класса Customer. Мы можем создавать экземпляры и вызывать методы класса Nevermore60Customer, как это делали с Customer. Чтобы убедиться в этом, модифицируем одного из заказчиков — Arabel, чтобы он стал экземпляром Nevermore60Customer:
public static void Main()
{
Nevermore60Customer arabel = new Nevermore60Customer();
...
}
В этом коде изменена всего одна строка — объявление arabel, делающее его экземпляром Nevermore60Customer. Все вызовы методов остаются прежними, и этот код выдает тот же самый результат, что и ранее использованный код. Если вы хотите в этом убедиться, обратитесь к примеру MortimerPhones2 (который можно найти на прилагаемом компакт-диске). Само по себе наличие копии определения класса Customer выглядит не особенно полезным. Польза этого подхода состоит в том, что теперь мы можем вносить модификации и дополнения в Nevermore60Customer. Можно проинструктировать компилятор следующим образом: “Nevermore60Customer — это почти то же самое, что Customer, но с определенными отличиями…”. В частности, можно модифицировать способ вычисления Nevermore60Customer стоимости каждого телефонного вызова в соответствии с новым тарифным планом.
Отличия, которые можно специфицировать, в принципе могут быть такими:
- Можно добавить новые члены (любого типа: поля, методы, свойства и так далее) к унаследованному классу, которых нет в базовом классе.
- Можно заменить реализацию существующих членов, таких как методы или свойства, которые уже есть в базовом классе.
Для данного примера мы заменим, или переопределим метод RecordCall() в классе Customer новой реализацией RecordCall() в классе Nevermore60Customer. И не только здесь, но всякий раз, когда вам понадобится добавить новый тарифный план, вы можете просто создать новый дополнительный класс, унаследованный от Customer, с новым переопределением RecordCall(). Таким образом, можно добавить код для того, чтобы справиться со многими тарифными планами, отделяя новый код от существующего, предназначенного для вычислений по старым планам. Не путайте переопределение (override) методов с их перегрузкой (overloading). Сходство терминов ни о чем не говорит, поскольку это совершенно разные и не связанные между собой концепции. Перегрузка методов не имеет никакого отношения к наследованию или к виртуальным методам.
Итак, модифицируем код класса Nevermore60Customer, чтобы он реализовывал новый тарифный план. Чтобы сделать это, нам понадобится не только переопределить метод RecordCall(), но также добавить новое поле, чтобы учитывать число израсходованных минут по повышенной цене:
public class Nevermore60Customer : Customer
{
private uint highCostMinutesUsed;
public override void RecordCall(TypeOfCall callType, uint nMinutes)
{
switch (callType)
{
case TypeOfCall.CallToLandline:
balance += (0.02M * nMinutes);
break;
case TypeOfCall.CallToCellPhone:
uint highCostMinutes, lowCostMinutes;
uint highCostMinutesToGo =
(highCostMinutesUsed < 60) ? 60 - highCostMinutesUsed : 0;
if (nMinutes > highCostMinutesToGo)
{
highCostMinutes = highCostMinutesToGo;
lowCostMinutes = nMinutes - highCostMinutes;
}
else
{
highCostMinutes = nMinutes;
lowCostMinutes = 0;
}
highCostMinutesUsed += highCostMinutes;
balance +=
(0.50M * highCostMinutes + 0.20M * lowCostMinutes);
break;
default:
break;
}
}
}
Отметим, что добавленное новое поле, highCostMinutesUsed, присутствует только в экземплярах Nevermore60Customer. Его нет в экземплярах базового класса Customer. Сам базовый класс никогда никоим образом не модифицируется неявно. И так должно быть всегда, потому что когда вы кодируете базовый класс, вы не обязательно знаете, какие дополнительные классы могут быть добавлены в будущем, и вы не хотите, чтобы ваш код был поврежден, когда кто-то добавляет класс-наследник!
Как видите, алгоритм вычисления стоимости разговора в данном случае более сложен, хотя если последовать логике, то можно увидеть, что он отвечает требованиям тарифного плана Nevermore60. Обратите внимание на ключевое слово override, добавленное к объявлению метода RecordCall(). Оно информирует компилятор о том, что этот метод в действительности переопределяет метод, ранее представленный в базовом классе, поэтому мы должны включить сюда это ключевое слово. Прежде, чем этот код откомпилировать, потребуется также внести пару изменений в базовый класс Customer:
public class Customer
{
private string name;
protected decimal balance;
// и так далее
public virtual void RecordCall(TypeOfCall callType, uint nMinutes)
{
switch (callType)
Первое изменение касается поля balance. Ранее оно было определено с ключевым словом private, что означало, что никакой код вне класса Customer не мог обращаться к нему непосредственно. К сожалению, это значит, что даже несмотря на то, что Nevermore60Customer является наследником Customer, код класса Nevermore60Customer не может напрямую обращаться к этому полю (хотя поле balance все еще присутствует внутри каждого объекта Nevermore60Customer). Это предотвращает возможность прямой модификации баланса внутри Nevermore60Customer при записи информации о телефонном звонке за счет того, что представленный код Nevermore60Customer.RecordCall() не будет компилироваться.
Ключевое слово модификатора доступа protected (защищенный) решает эту проблему. Оно указывает, что любому классу, унаследованному от Customer, как и самому Customer, будет разрешен доступ к этому полю. Поле по-прежнему будет невидимо для кода любого другого класса, который не унаследован от Customer. По сути, при этом вы предполагаете, что поскольку между классом и его наследником существуют тесные отношения, наследнику стоит знать кое-что о внутреннем устройстве и работе базового класса, по крайней мере, если речь идет о защищенных членах.
Это представляет спорный момент в отношении того, что считать хорошим стилем программирования. Многие разработчики предпочитают сохранять все поля приватными (private) и писать защищенные методы доступа, чтобы позволить классам-наследникам модифицировать balance. В данном случае снабжение поля balance модификатором protected вместо private позволяет несколько упростить код.
Второе изменение относится к объявлению класса RecordCall() базового класса. Здесь добавлено ключевое слово virtual. Это изменяет манеру вызова метода при запуске программы таким образом, что позволяет переопределять его. C# не позволит классу-наследнику переопределить метод, если только он не был объявлен как virtual в базовом классе. Виртуальные методы и их переопределение мы рассмотрим далее в этом приложении.
|