Всегда ли нужно создавать новые классы?
Начнем с вопроса, казалось бы, не имеющего никакого отношения к рассматриваемому вопросу, а именно - всегда ли нужно создавать новый класс для каждой новой задачи? Правильный ответ, конечно же, "нет". Это было бы странно и неэффективно. "Фишка" состоит в том, что мы можем использовать уже существующие классы, адаптируя их функциональность для выполнения новых задач. Таким образом появляется возможность не создавать систему классов с нуля, а задействовать уже имеющиеся решения, которые были созданы ранее, при работе над предыдущими проектами. Впрочем, наше высказывание о странности и неэффективности создания новых классов не является истиной в последней инстанции. Могут быть ситуации, когда существующие классы по каким-либо причинам не устраивают архитектора, и тогда требуется создать новый класс. Следует, однако, избегать ситуаций, когда созданный класс (а точнее, его набор операций и атрибутов) практически повторяет существующий, лишь незначительно отличаясь от него. Все-таки лучше не изобретать велосипед и стараться создавать классы на основе уже существующих, и только если подходящих классов не нашлось - создавать свои, которые, в свою очередь, могут (и должны!) служить основой для других классов. Мы уже не говорим о том, что создание классов предполагает значительный объем усилий по кодированию и тестированию. В общем случае, сказанное выше можно проиллюстрировать такой диаграммой (рис. 4.7):
Рис. 4.7.
В дополнение можно назвать несколько причин, почему стоит использовать уже существующие классы:
Во-первых, идя этим путем, мы пользуемся плодами ранее принятых решений. Действительно, если когда-то мы уже решили некоторую проблему, зачем начинать все "с нуля", повторяя уже однажды проделанные действия?
Во-вторых, таким образом мы делаем решение мобильным и расширяемым. Используя уже существующие классы и создавая на их основе новые, мы можем развивать решение практически неограниченно, добавляя лишь необходимые нам в данный момент детали - атрибуты и операции.
В-третьих, существующие классы, как правило, хорошо отлажены и показали себя в работе. Разработчику не надо тратить время на кодирование, отладку, тестирование и т. д., - мы работаем с хорошо отлаженным и проверенным временем кодом, который зарекомендовал себя в других проектах и в котором уже выявлено и исправлено большинство ошибок.
А теперь внимание - мы много говорили о том, что нужно создавать классы на основе уже существующих, но так и не сказали ни слова о том, как это сделать. Пришло время внести ясность в этот вопрос.
Тем самым мы подбираемся к понятию обобщения или генерализации, которое играет очень важную роль в ООП, являясь одним из его базовых принципов. Обобщение- это отношение между более общей сущностью, называемой суперклассом, и ее конкретным воплощением, называемым подклассом. Иногда обобщение называют отношениями типа "является", имея в виду, что одни сущности (например, круг, квадрат, треугольник) являются воплощением более общей сущности (например, класса "геометрическая фигура"). При этом все атрибуты и операции суперкласса независимо от модификаторов видимости входят в состав подкласса.
Обобщение (или, как часто говорят, наследование) на диаграммах обозначается очень просто - незакрашенной треугольной стрелкой, направленной в направлении суперкласса (рис. 4.8).
Для того чтобы научиться эффективно моделировать наследование, обратимся к классикам, а именно к Г. Бучу. Он советует проводить эту процедуру в такой последовательности:
- Найдите атрибуты, операции и обязанности, общие для двух или более классов из данной совокупности. Это позволит избежать ненужного дублирования структуры и функциональности объектов.
- Вынесите эти элементы в некоторый общий суперкласс, а если такого не существует, то создайте новый класс.
- Отметьте в модели, что подклассы наследуются от суперкласса, установив между ними отношение обобщения.
Рис. 4.8.
А вот и пример применения этого подхода (рис. 4.9):
Рис. 4.9.
На первый взгляд, кажется странным, что класс "точка" не имеет никаких атрибутов, а круг имеет только радиус. С прямоугольником, вроде бы, все понятно - ширина и высота, но вот только где он расположен в пространстве, этот прямоугольник? Давайте попробуем следовать советам Буча. Итак, положение всех трех фигур можно однозначно определить с помощью пары чисел. Для точки - это вообще единственные ее характеристики, для круга и прямоугольника - их центры (под центром прямоугольника мы понимаем точку пересечения его диагоналей). Вот они, общие атрибуты! Таким образом, мы создали суперкласс "Фигура", имеющий два атрибута - координаты центра. Все остальные классы на этой диаграмме связаны с классом "Фигура" отношением обобщения, т. е. в них нужно доопределить только "недостающие" атрибуты - радиус, ширину и высоту. Атрибуты, описывающие координаты центра, эти классы имеют изначально как потомки класса "Фигура" - они их наследуют. Заметим, что операции классов мы тут не рассматриваем: понятно, что с ними была бы та же история.
Так, с наследованием вроде бы разобрались. Пришло время для маленькой провокации с нашей стороны. Классы-потомки ведь наследуют атрибуты и операции суперкласса? Таким образом, они могут наследовать и их интерфейсы - то есть объекты абсолютно разной природы могут иметь один и тот же интерфейс! Так как же тогда определить, какого же все-таки класса объект? Да и нужно ли это вообще?
Действительно, объекты разной природы (или говоря проще, разных классов) могут поддерживать один и тот же интерфейс именно так, как того ожидает пользователь. Примером тому может служить рассмотренная выше диаграмма с геометрическими фигурами. Все рассмотренные фигуры имеют, например, операцию рисования на экране. С точки зрения пользователя в каждом случае это одно и то же действие. Однако реализованы эти операции по-разному - ведь процедура изображения прямоугольника сильно отличается от подобной процедуры для круга. Но для пользователя это неважно: ведь сигнатура-то одна и та же! А возможно это благодаря еще одному из основных принципов ООП - полиморфизму. Как мы только что упомянули, работа механизма полиморфизма основана на совпадении сигнатуры метода, объявленного в интерфейсе, и сигнатуры самого метода. Методы внутри классов-потомков могут быть (и наверняка будут!) переопределены, их реализации будут различными, а сигнатуры останутся неизменными. Таким образом (и в этом легко ощутить мощь ООП), выполняя одни и те же операции, разные объекты могут вести себя по-разному.
Полиморфизм является основой для реализации механизма интерфейсов в языках программирования. Вот, кстати, и ответ на вопрос, какого класса объект: как только пользователь обращается к некоторой операции через интерфейс, определяется фактический класс объекта и вызывается соответствующая операция класса. Примеры полиморфизма можно увидеть в самых обыденных вещах, которыми мы пользуемся в повседневной жизни. Оглянитесь вокруг - мир построен по ООП, Матрица работает! Например, всем привычная кредитная карточка, являясь интерфейсом для доступа к банковскому счету через банкомат (и не только), одинаково работает в любой стране, вот только ведет себя чуть-чуть по-разному, т. к. банкомат выдает деньги в местной валюте. Согласны, пример не очень корректный, но зато очень наглядный! Думаем, понаблюдав за окружающим миром, читатель сам сможет привести массу примеров полиморфизма.
Инкапсуляция, наследование и полиморфизм, с которыми мы только что познакомились, являются теми самыми тремя китами, на которых держится ООП. Если вы поняли суть этих базовых принципов и осознали их истинную мощь, вы прошли большую часть пути, ведущего к полному овладению ООП как наиболее адекватной методикой описания (так и тянет сказать "проектирования") окружающего нас мира.
Дата добавления: 2017-03-29; просмотров: 620;