воскресенье, 22 августа 2010 г.

ООП с применением средств С# – Наследование и полиморфизм (Часть 2)

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

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

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

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

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

В С# указатель на базовый класс выглядит как двоеточие (:).

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

namespace Еmрloyees

{

public class Manager : Employee {

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

private ulong numberOfOptions:

publiс u1ong NumbOpts

{

get { return numberOfptions: }

set { numberOfOptions = value: }

}

}

public class SalesPerson : Employee {

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

pri vate int numberOfSa1es;

publiс int NumbSales

{

get { return numberOfSales; }

set { numberOfSales = value: }

}

}

}

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

Работа с конструктором базового класса

Классы SalesPerson и Manager в том виде, в котором они существуют сейчас, могут быть созданы только при помощи конструкторов по умолчанию - просто потому, что другие конструкторы не определены. Но, вполне возможно, нам удобнее создавать объекты этих классов, сразу же присваивая значения их внутренним данным, например:

// Создаем объект производного класса, используя собственный конструктор

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

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

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

public Manager(string fullName, int empID, float currPay, 5tring 55П, ulong numbOfOpts)

: base(fullNarne. ernpID. currPay, ssn)

{ numberOfOption5 = numbOfOpt5:}

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

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

Хранение «семейных тайн»: ключевое слово protected

Открытые члены класса (объявленные как public) могут быть доступны откуда угодно. Закрытые члены класса (объявленные как private) могут быть доступны только из того объекта, в котором они явно определены. Однако в С#, как и в других современных объектно-ориентированных языках программирования, предусмотрен еще один модификатор области видимости: protected: (защищенное ).

При создании защищенных переменных или защищенных методов класса эти члены будут доступны напрямую как из собственного класса, так и из всех производных классов.

С точки зрения пользователей объектов защищенные данные являются закрытыми (то есть protected для них будет эквивалентно private).

Запрет наследования: классы, объявленные как sealed

Классическое наследование - это очень удобный способ определить набор характеристик для класса. Однако иногда встречаются ситуации, когда вы не просто не хотите использовать наследование, но желаете запретить производить наследование от какого-либо класса. Например, предположим, что в нашей иерархии сотрудников появился новый класс - PTSalesPerson (от part-time - продавцы на неполный рабочий день), который наследует существующему классу SalesPerson.

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

public sealed class PTSalesPerson : SalesPerson

Поскольку мы объявили PTSalesPerson как закрытый, его уже нельзя использо­вать в качестве базовых для остальных классов.

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

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

public class Radio {...}

public class Car {...}

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

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

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

public class Саr

{

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

private Radio theMusicBox;

}

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

public class Car

{

public Car()

{...

theMusicBox = new Radio();

}

}

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

public class Car

{

public void CrankTunes(bool state) {

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

theМusicBox.TurnOn(state);

}

}

Определение вложенных типов

В С# вы можете определить тип непосредственно внутри другого типа. Такие типы называются вложенными (nested):

public class MyClass

{

...

public class MyNestedClass

{...}

}

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

Внешний тип имеет возможность создавать объекты любых вложенных в него типов. Также необходимо отметить, что вложенный класс объявлен как private. В С# вложенные классы могут объявляться и как private, и как public. Однако классы, которые объявлены в пространстве имен напрямую (то есть те классы, которые не вложены ни в какой другой класс), не могут быть объявлены как private.

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

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

public class Employee

{

public void GiveBonus(float amount)

{currPay += amount;}

}

Поскольку этот метод определен в базовом классе как publiс, вы теперь можете поощрять продавцов и менеджеров. Проблема заключается в том, что унаследованный метод GiveBonus() пока работает абсолютно одинаково в отношении объектов обоих производных классов - и объектов SalesPerson, и объектов Manager. Однако, конечно, было бы лучше, чтобы для объекта каждого класса использовался свой уникальный вариант метода.

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

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

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

Полиморфизм используется не только для переопределения поведения производных классов.

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

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

abstract publiс class Еmployee {...}

Принудительный полиморфизм: абстрактные методы

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

Виртуальные методы можно замещать в производных классах при помощи ключевого слова override. Однако в С# виртуальные методы можно и не замещать в производных классах. При этом в случае вызова замещенного метода будет вызван уникальный вариант этого метода, а если мы вызовем не замещенный метод, метод будет выполнен в соответствии со своим определением в базовом классе.

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

В этом и состоит суть полиморфизма. Как мы помним, объекты абстрактных классов создавать невозможно. Однако вы вполне можете хранить ссылки на любой производный класс, используя для этого переменную абстрактного класса. Нужный вариант абстрактного метода выбирается непосредственно в момент выполнения программы в зависимости от класса, к которому принадлежит объект.

Контроль версий членов класса

В С# существует еще один прием работы с методами в производных классах, который можно противопоставить замещению методов. Этот прием называется сокрытием методов (method hiding). Рассмотрим его на примере.

У нас имеются два класса Circle и Hexagon, которые являются производными от класса Shape. Предположим, что вы создаете новый класс с именем Oval. Пусть у нас Oval является производным классом от класса Circle. Теперь предположим, что в классе Oval также определен метод Draw(). Однако представим, что, в отличие от отношений между реализациями метода Draw( ) в базовом классе, мы хотим запретить любое наследование логики Draw( ) между методами - фактически разорвать отношения наследования на уровне единственного метода. С# предлагает для этого случая средство, которое называется контролем версий (versioning). Для того чтобы им воспользоваться, достаточно определить в классе Oval метод Draw() с ключевым словом new.

Вот пожалуй и всё, эти принципы должен знать каждый программист, и я не сомневаюсь, что вы о них уже знали, а теперь, прочитав эти статьи, вы ещё и узнали как они реализованы в C#.

Комментариев нет:

Отправить комментарий