Поиск

Наследование.

Наследованием называют возможность при описании класса указывать на его происхождение (kind-of relationship) от другого класса. Наследование позволяет создать новый класс, в основу которого положен существующий. В полученный таким образом класс можно внести свои изменения, а затем создать новые объекты данного типа. Этот механизм лежит в основе создания иерархии классов. После абстрагирования наследование — наиболее значимая часть общего планирования системы. Производным (derived class) называется создаваемый класс, производный от базового (base class). Производный класс наследует все методы базового, позволяя задействовать результаты прежнего труда.

ПРИМЕЧАНИЕ Вопрос, какие члены базового класса наследуются производными классами, решается в С# через модификаторы доступа, применяемые при описании члена. Подробнее об этом см. главу 5, мы же пока будет считать, что производный класс наследует все члены своего базового класса.

Чтобы понять, когда и как применять наследование, вернемся к примеру EmployeeApp. Допустим, в компании есть служащие с разными типами оплаты труда: постоянный оклад, почасовая оплата и оплата по договору. Хотя у всех объектов Employee должен быть одинаковый интерфейс, их внутреннее функционирование может различаться. Например, метод CalculatePay для служащего на окладе будет работать не так, как для контрактника. Однако для ваших пользователей важно, чтобы интерфейс CalculatePay не зависел от того, как считается зарплата.

У новичка в ООП, вероятно, появится вопрос: "А нельзя ли здесь обойтись без объектов? Введи в структуру EMPLOYEE член, описывающий тип оплаты, и напиши функцию вроде этой:

Double CalculatePay(EMPLOYEE" pEmployee, int IHoursWorked) {
// Проверяем указатель pEmployee.
if (pEmployee->type == SALARIED) {
// Вычисляем заработок для служащего на окладе. }
else if (pEraployee->type == CONTRACTOR)
{
// Вычисляем заработок по контракту. }
else if (pEmployee->type == HOURLY) <
// Вычисляем почасовой заработок. }
else {
// Выполняем иную обработку. }
// Возвращаем значение, полученное от одного из // вышестоящих операторов. }

В этом коде есть две проблемы. Во-первых, успешное выполнение функции тесно связано со структурой EMPLOYEE. Как я уже говорил, подобная связь очень нежелательна, поскольку любое изменение структуры потребует модификации этого кода. Как объектно-ориентированный программист, вы меньше всего захотите "грузить" пользователей вашего класса подробностями, в которых непосвященному разобраться трудно. Это все равно, как если бы производитель автомата по продаже газировки, перед тем как вам набрать стакан воды, потребовал от вас знания работы внутренних механизмов автомата.

Во-вторых, такой код нельзя задействовать повторно. Тот, кто понимает, что наследование способствует повторному использованию кода, теперь по достоинству оценит классы и объекты. Так, в нашем примере достаточно описать в базовом классе те члены, которые будут функционировать независимо от типа оплаты, а любой производный класс унаследует функции базового класса, добавив к ним что-то свое. Так это выглядит на С#:

class Employee <
public Employee(string firstName, string lastName,
int age, double payRate) {
this.firstName = firstName; this.lastName = lastName;
 this.age = age; this.payRate = payRate; }
protected string firstName;
protected string lastName;
protected int age;
protected double payRate;
public double CalculatePay(int hoursWorked) {
// Здесь вычисляется зарплата.
return (payRate * (double)hoursWorked); } }
class SalariedEmployee : Employee
{
public string SocialSecurityNumber;
public void CalculatePay (int hoursWorked)
{
// Вычисляем заработок постоянного служащего.
} }
class ContractEmployee : Employee {
public string FederalTaxId;
public void CalculatePay (int hoursWorked)
{
// Вычисляем заработок для контрактника.
}
}

Отметим три важных момента, вытекающих из данного примера.

  • В базовом классе Employee описана строковая переменная Employeeld, которая наследуется и классом SalariedEmployee, и классом Contract-Employee. Оба производных класса получили эту переменную автоматически как наследники класса Employee.
  • Каждый из производных классов реализует свою версию CalculatePay. Вы видите, что они оба унаследовали этот интерфейс, и хотя реализация этих функций различна, пользовательский код остался прежним.
  • Оба производных класса в дополнение к членам, унаследованным из базового класса, имеют свои члены: в классе SalariedEmployee описана строковая переменная SocialSecurityNumber, а в класс ContractEmployee включено описание члена FederalTaxId.

Этот небольшой пример показывает, как наследование функциональных возможностей базовых классов позволяет создать повторно используемый код. Кроме того, вы можете расширить эти возможности, добавив собственные переменные и методы.

Что такое "правильное" наследование

Важнейшую проблему "правильного" наследования я начну с термина замещаемость (substitutability), взятого у Маршалла Клайна (Marshall Cline) и Грега Ломау (Greg Lomow) (C++ FAQs, Addison-Wesley, 1998). Этот термин означает, что поведение производного класса достигается путем замещения поведения, заимствованного у базового класса. Это одно из важнейших правил, которое вам нужно соблюдать при построении работающей иерархии классов. (Под "работающими" я подразумеваю системы, выдержавшие проверку временем и оправдавшие надежды на повторное использование и расширение кода.)

А вот еще одно важное правило, которому я советую следовать при создании собственной иерархии классов: любой унаследованный интерфейс производного класса не должен требовать больше и обещать меньше, чем в базовом классе. Пренебрежение этим правилом приводит к разрушению существующего кода. Интерфейс класса — это контракт между классом и пользователями, применяющими этот класс. Имея ссылку на производный класс, программист всегда может обращаться с ним, как с базовым классом. Это называется восходящим преобразованием типа (upcasting). В нашем примере клиент, имея ссылку на объект ContractEmp-loyee, обладает и неявной ссылкой на его базовый класс — объект Employee. Поэтому согласно определению объект ContractEmployee всегда должен поддерживать выполнение функций своего базового класса. Заметьте: это правило распространяется только на функциональные возможности базового класса. В производный класс можно добавить и другие функции, которые выполняют и более узкие (или более широкие) задачи, чем унаследованные функции. Поэтому данное правило применяется только к унаследованным членам, поскольку существующий код рассчитан на работу только с этими членами.