Применение модели включения-делегирования

Set

Get

{

return empID;

}

{

// Здесь вы можете реализовать логику для проверки вводимых

// значений и выполнения других действий

empID = value;

}

}

}

 

Свойство С# состоит из двух блоков — блока доступа (get block) и блока изменения (set block). Ключевое слово value представляет правую часть выражения при присвоении значения посредством свойства. Как и все в С#, то, что представлено словом value — это также объект. Совместимость того, что передается свойству как value, с самим свойством, зависит от определения свойства. Например, свойство EmpID предназначено (согласно своему определению в классе) для работы с закрытым целочисленным empID, поэтому число 81 вполне его устроит:

Необходимо отметить, что обращаться к объекту value можно только в пределах программного блока set внутри определения свойства. Попытка обратиться к этому объекту из любого другого места приведет к ошибке компилятора.

Последнее, что мы отметим — использование свойств (по сравнению с традиционными методами доступа и изменения) делает применение ваших типов более простым. Например, предположим, что в классе Employee предусмотрена внутренняя закрытая переменная для хранения информации о возрасте сотрудника. Вы чотите, чтобы при наступлении дня рождения этого сотрудника значение этой пе-оеменной увеличивалось на единицу. При использовании традиционных методов доступа и изменения эта операция будет выглядеть так:

 

Employee joe = new Employee();

Joe.SetAge(joe.GetAge() + 1);

 

Используя свойство, вы можете сделать это проще:

 

Employee joe = new Employee()ж

joe.Age++;

 

 

Поддержка наследования в С#

Теперь, когда вы уже умеете создавать хорошо инкапсулированные классы, обратимся к созданию в С# наборов взаимосвязанных классов при помощи наследования. Как уже говорилось, наследование — это один из трех основных принципов объектно-ориентированного программирования. Главная задача наследования — обеспечить повторное использование кода. Существует два основных вида наследования: классическое наследование (отношение «быть» — is-a) и включение-делегирование (отношение «иметь» — has-a). Мы начнем с рассмотрения средств СП для реализации классического наследования.

При установлении между классами отношений классического наследования вы тем самым устанавливаете между ними зависимость. Основная идея классического наследования заключается в том, что производные классы должны получать функциональность от базового класса-предка и дополнять ее новыми возможностями. Рассмотрим это на примере. Предположим, что в дополнение к нашему классу Employee мы определили еще два производных класса: класс Salesperson (продавец) и класс Manager (администратор). В результате получится иерархия классов, которая представлена на рис. 3.9.

Рис. 3.9. Иерархия классов сотрудников

 

Как видно из рисунка, сотрудниками являются и продавец, и менеджер. В модели классического наследования базовые классы (в нашем случае Employee) используются для того, чтобы определить характеристики, которые будут общими для всех производных классов. Производные классы (такие как Salesperson и Manager) расширяют унаследованную функциональность за счет своих собственных, специфических членов.

В С# указатель на базовый класс выглядит как двоеточие ( ). Если переводить наши отношения наследования между категориями сотрудников на язык синтаксиса СП, то соответствующий код может выглядеть следующим образом:

 

// Добавляем в пространство имен Employees два новых производных класса

namespace Employees

{

public class Manager: Employee

{

// Менеджерам необходимо знать количество имеющихся у них опционов на акции

private ulong numberOfOptions;

public ulong NumbOpts

{

get { return numberOfptions; }

set { numberOfOptions = value; }

}

}

 

public class Salesperson: Employee

{

// Продавцам нужно знать объем своих продаж

private int numberOfSales;

public int NumbSales

{

get { return numberOfSales; }

set { numberOfSales = value; }

}

}

 

В нашей ситуации оба производных класса расширяют функциональность базового класса за счет добавления свойств, уникальных для каждого класса. Поскольку мы использовали отношение «быть», то классы Salesperson и Manager автоматически наследовали от класса Employee все открытые члены. Это несложно проверить:

 

// Создаем объект производного класса и проверяем его возможности

public static int Main(string[ ] args)

{

// Создаем объект «продавец»

SalesPerson stan = new SalesPerson();

 

// Эти члены унаследованы от базового класса Employee

stan.EmpID = 100;

stan.SetFullName("Stan the Man");

 

// А это - уникальный член, определенный только в классе Salesperson

stan.NumbSales = 42;

return 0;

}

 

Конечно же, через объект производного класса нельзя напрямую обратиться • закрытым членам, определенным в базовом классе:

 

// Ошибка! Через объект производного класса нельзя обращаться к закрытым членам,

// определенным в базовом классе

Salesperson stan = newSalesPersonn();

stan.currPay;

 

Как уже говорилось, в объектно-ориентированных языках программирования исполъзуются две главные разновидности наследования. Первая из них называется классическим наследованием (моделью «быть» — is-a), и эта модель была рассмотрена в предыдущем разделе. Вторая разновидность — это модель включения-делегирования (модель «иметь» — has-a), и именно ей посвящен настоящий раздел.

Для начала нам потребуется простой класс, представляющий автомобильный радиоприемник:

// Этот класс будет внутренним, включенным в другой класс - Саг

pubic class Radio

{

public Radio( ){}

 

public void TurnOn(bool on)

{

if(on)

Console.WriteLine(“Jamming. ");

else

Console.WriteLine(“Quiet time . ");

}

}

 

Теперь предположим, что вам потребовалось разработать программную модель автомобиля. Класс Саг, который мы будет использовать для этой цели, должен содержать сведения об автомобиле (как мы его именуем, его скорость в настоящий момент и максимально допустимую скорость). Все эти данные можно задавать при помощи пользовательского конструктора. Определение класса Саг может выглядеть следующим образом:

//Этот класс будет выступать в роли внешнего класса, класса-контейнера для Radio

public class Car

{

private int currSpeed;

private int maxSpeed;

private string petName;

bool dead; // Жива ли машина или уже нет

 

public Саг()

{

maxSpeed = 100;

dead = false;

}

 

public Car(string name, int max, int curr)

{

currSpeed = curr;

maxSpeed = max;

petName = name;

dead = false;

}

 

public void SpeedUp (int delta)

{ …… }

 

Сейчас в нашем распоряжении есть два независимых класса — Radio для авто мобильного радиоприемника и Саг для самого автомобиля Понятно, что эти два класса должны взаимодействовать друг с другом и эти отношения желательно как то зафиксировать Однако вряд ли нам удаст ся применить в этом случае класси ческое наследование трудно производить машину от радиоприемника ичи радио приемник от машины В этой ситуации, конечно больше подойдет отношение включения-делегирования пусть машина включает в себя радио и передает этому классу необходимые команды В терминологии объектно ориентированного про граммирования контейнерный класс (в нашем случае Саг) называется родитель ским (parent), а внутренний класс, который помещен внутрь контейнерного (это конечно Radio), называется дочерним (child).

Помещение радиоприемника внутрь автомобиля влечет за собой внесение в опре деление класса Саг следующих изменений:

 

// Автомобиль «имеет» (has а) радио

public class Car

{

. . . . . .

 

// Внутреннее радио

private Radio theMusicBox;

}

 

Обратите внимание, что внутренний класс Radi о был объявлен как private С точки зрения инкапсуляции мы делаем все правильно Однако при этом неизбежно возникает вопрос: а как нам включить радио? Переводя на язык программировая — а как внешний мир будет взаимодействовать с внутренним классом? Понятнo, что ответственность за создание объекта внутреннего класса несет внешний контейнерный класс. В принципе код для создания объектов внутреннего класса можно помещать куда угодно, но обычно он помещается среди конструкторов контейнерного класса:

 

// За создание объектов внутренних классов ответственны контейнерные классы

public class Car

{

. . .

// Встроенное радио

private Radio theMusicBox;

 

public Car( )

{

maxSpeed = 100;

dead = false;

// Объект внешнего класса создаст необходимые объекты

//внутреннего класса при собственном создании

theMusicBox = new Radio( ); // Если мы этого не сделаем theMusicBox

// начнет свою жизнь с нулевой ссылки

}

 

public Car(string name, int max, int curr)

{

currSpeed = curr;

maxSpeed = max;

petName = name;

dead = fales;

theMusicBox = new RadioO;

}

 

. . . .

}

 

Произвести инициализацию средствами С# можно и так:

// Автомобиль «имеет» (has-a) радио

public class Car

{

. . . .

// Встроенное радио

private Radio theMusicBox = new Radio( );

. . . .

}

 

Таким образом, радиоприемник внутри автомобиля у нас теперь создается есте с автомобилем. Однако вопрос о том, как именно можно включить этот раприемник, остался нерешенным. Ответ на него выглядит так:- для того чтобы воспользоваться возможностями внутреннего класса, необходимо делегирование (delegation). Делегирование заключается в простом добавлении во внешний контейнерный класс методов для обращения ко внутреннему классу, шример:

 

// Во внешний класс добавляются дополнительные открытые методы и другие члены

// которые обеспечивают доступ к внутреннему классу

public class Car

{

. . . .

public void CrankTunes(bool state)

{

// Передаем (делегируем) запрос внутреннему объекту

theMusicBox.TurnOn(state);

}

}

 

В приведенном ниже коде обратите внимание на то, что пользователь косвенно обращается к скрытому внутреннему объекту, даже не подозревая о том, что в недрах объекта Саг существует закрытый (определенный как private) объект Radio:

 

// Выводим автомобиль на пробную поездку

public class CarApp

{

public static int Main(string[ ] args)

{

// Создаем автомобиль (который, в свою очередь, создаст радио)

Саг c1;

c1 = new Car( “SlugBug”, 100, 10);

 

// Включаем радио (запрос будет передан внутреннему объекту)

c1.CrankTunes(true);

 

// Ускоряемся

for(int i=0; i < 10; i++)

c1.SpeedUp(20);

 

// Выключаем радио (запрос будет вновь передан внутреннему объекту

c1.CrankTunes(false);

return 0;

}

}

 

 

Поддержка полиморфизма в С#

Предположим, что в базовом классе Employee определен метод GiveBonus( ) — поощрить:

 

// В классе Employee определен новый метод для поощрения сотрудников

public class Employee

{

public void GiveBonus(float amount)

{

currPay += amount;

}

. . . .

}

 

Поскольку этот метод определен в базовом классе как publiс, вы теперь можете поощрять продавцов и менеджеров:

 

// Поощрения объектам производных классов

Manager chucky = new Manager(“Chucky", 92, 100000, "333-23-2322", 9000);

Chucky.GiveBonus(300);

Chucky.DisplayStats( );

 

Salesperson fran = new SalesPerson("Fran", 93, 3000, "932-32-3232", 31);

Fran.GiveBonus(200);

Fran.DisplayStats( );

.

Проблема заключается в том, что унаследованный метод GiveBonus( ) пока работает абсолютно одинаково в отношении объектов обоих производных клас-— и объектов Salesperson, и объектов Manager. Однако, конечно, было бы лучше, чтобы для объекта каждого класса использовался свой уникальный вариант метода. Например, при поощрении продавцов можно учитывать их объем продаж. Менеджерам помимо денежного поощрения можно выдавать дополнительные опционы на акции. Поэтому задачу можно сформулировать так: «Как заставить один и тот же метод по-разному реагировать на объекты разных классов?»

Для решения этой задачи в С# предусмотрено понятие полиморфизма. Полиморфизм позволяет переопределять реакцию объекта производного класса на метод, определенный в базовом классе. Для реализации полиморфизма в нашем приложении мы воспользуемся ключевыми словами С#: virtual и override. Если базовый класс определяет метод, который должен быть замещен в производном классе, этот метод должен быть объявлен как виртуальный (конечно, при помощи ключевого слова virtual):

 

public class Employee

{

// Для метода GiveBonus( ) предусмотрена реализация по умолчанию.

// однако он может быть замещен в производных классах

publicvirtualvoid GiveBonus(float amount)

{

currPay += amount;

}

. . . .

}

 

Если вы хотите переопределить виртуальный метод, необходимо заново определить метод в производном классе, использовав ключевое слово override:

 

public class Salesperson : Employee

{

// На размер поощрения продавцу будет влиять объем его продаж

public overridevoid GiveBonus(float amount)

{

int salesBonus = 0;

if(numberOfSales >= 0 && numberOfSales <=100)

salesBonus = 10;

else if(numberOfSales >= 101 && numberOfSales <= 200)

salesBonus = 15;

else

salesBonus = 20; // Для объема продаж больше 200

base.GiveBonus(amount * salesBonus);

}

. . . .

}

public class Manager Employee

{

private Random r = new Random( );

 

// Помимо денег менеджеры также получают некоторое количество опционов

// на акции

 

public overridevoid GiveBonus(float amount)

{

// Деньги: увеличиваем зарплату

base.GiveBonus(amount);

// Опционы на акции: увеличиваем их количество

numberOfOptions += (ulong)r.Next(500);

}

. . . .

}

 

Обратите внимание, что в определении каждого из замещенных методов используется вызов этого же метода базового класса. Таким образом, нет необходимости заново определять всю логику замещенного метода в производном классе: вполне достаточно воспользоваться вариантом метода по умолчанию, определенном в базовом классе, дополнив его нужными вам действиями.

Метод Employee.DisplayStats( ) у нас также определен как virtual, и он замещен в производных классах таким образом, чтобы показывать текущий объем продаж, если речь идет о продавце, или количество опционов,-имеющееся в настоящее время в распоряжении менеджера. Теперь, когда для каждого из производных классов определены собственные варианты этих двух методов, объекты разных классов ведут себя по-разному:

 

// Улучшенная система поощрений!

Manager chucky = new Manager("Chucky", 92, 100000, "333-23-2322", 9000);

chucky.GiveBonus(300);

chucky.DisplayStats( );

 

Salesperson fran = new SalesPerson(“Fran", 93, 3000, "932-32-3232", 31);

fran.GiveBonus(200);

fran.DisplayStats( );

 

Мы с вами научились использовать полиморфизм для переопределения поведения производных классов. Однако, как, наверное, вы догадываетесь, возможности полиморфизма этим далеко не исчерпываются.

Абстрактные классы

В настоящее время базовый класс Employee выполняет в нашей программе понятные и логичные функции: он обеспечивает производные классы набором защищенных переменных, а также предоставляет реализации по умолчанию двух виртуальных методов— GiveBonus( ) и DisplayStats( ), замещенных в производных классах. Однако в нашей программе до сих пор существует потенциальный источник ошибок: мы можем создавать объекты базового класса Employee:

 

// А это кто такой?

Employee X = new Employee( );

 

«Просто сотрудников» у нас быть не должно — каждой из возможных категорий сотрудников у нас соответствует производный класс. Поэтому вполне логичным будет просто запретить создание объектов класса Employee. В С# для этого достаточно объявить класс абстрактным:

 

// Объявляем класс Employee абстрактным, запрещая создание объектов этого класса abstractpublic class Employee

{

// Открытые интерфейсы и внутренние данные класса

}

 

Теперь при попытке создания объекта класса Employee компилятор будет выдавать сообщение об ошибке:

 

// Ошибка! Нельзя создавать экземпляры абстрактного класса

Employee X = new Employee( );